Deploying Express.js Apps on DigitalOcean App Platform
A complete guide to deploying Express.js applications on DigitalOcean App Platform covering configuration, environment variables, databases, custom domains, and scaling.
Deploying Express.js Apps on DigitalOcean App Platform
DigitalOcean App Platform is a Platform-as-a-Service (PaaS) that deploys applications directly from Git repositories. You push code, App Platform builds it, runs it, manages SSL, and scales it. No server configuration, no Docker files required, no infrastructure to maintain.
For Express.js applications, App Platform handles the common deployment pain points: Node.js version management, npm dependency installation, environment variables, HTTPS certificates, and zero-downtime deployments. This guide covers the full deployment workflow from first push to production-ready configuration.
Prerequisites
- A DigitalOcean account
- An Express.js application in a Git repository (GitHub, GitLab, or Bitbucket)
- Node.js installed locally for development
doctlCLI installed (optional, for command-line management)
Preparing Your Express App
Required Configuration
App Platform needs to know how to start your application and which port to listen on.
// server.js
var express = require("express");
var app = require("./app");
var port = process.env.PORT || 8080;
app.listen(port, function() {
console.log("Server running on port " + port);
});
App Platform sets the PORT environment variable. Your application must listen on it.
Package.json Configuration
{
"name": "my-express-app",
"version": "1.0.0",
"engines": {
"node": "20.x"
},
"scripts": {
"start": "node server.js",
"build": "npm run build:css"
},
"dependencies": {
"express": "^4.18.0"
}
}
Key fields:
- engines.node — specifies the Node.js version. App Platform uses this to select the runtime.
- scripts.start — App Platform runs this to start your application.
- scripts.build — if present, App Platform runs this during the build phase.
Health Check Endpoint
App Platform monitors your application's health. Add a health check endpoint:
// app.js
app.get("/health", function(req, res) {
res.status(200).json({ status: "ok", uptime: process.uptime() });
});
Deploying via the Dashboard
- Log in to DigitalOcean and navigate to Apps
- Click Create App
- Select your Git repository and branch
- App Platform auto-detects Node.js and configures the build
- Review the detected settings:
- Build Command:
npm install(automatic) - Run Command:
npm start(from package.json) - HTTP Port: 8080 (default)
- Build Command:
- Choose a plan (Basic, Professional, or Pro)
- Click Create Resources
App Platform clones your repository, runs npm install, then npm start, and assigns a URL like https://your-app-xxxxx.ondigitalocean.app.
App Spec Configuration
The App Spec is a YAML file that defines your application's configuration. It is more powerful and reproducible than dashboard configuration.
Basic App Spec
# .do/app.yaml
name: my-express-app
region: nyc
services:
- name: api
github:
repo: your-username/your-repo
branch: main
deploy_on_push: true
build_command: npm install
run_command: npm start
http_port: 8080
instance_count: 1
instance_size_slug: basic-xxs
envs:
- key: NODE_ENV
value: production
- key: DATABASE_URL
scope: RUN_TIME
type: SECRET
Deploying with App Spec
# Using doctl CLI
doctl apps create --spec .do/app.yaml
# Or update an existing app
doctl apps update YOUR_APP_ID --spec .do/app.yaml
Environment Variables
Setting Variables via Dashboard
Navigate to your app > Settings > Environment Variables. Add key-value pairs. Variables marked as "Secret" are encrypted and hidden from logs.
Setting Variables via App Spec
services:
- name: api
envs:
# Plain text variable
- key: NODE_ENV
value: production
# Secret variable (set via dashboard or doctl)
- key: DATABASE_URL
scope: RUN_TIME
type: SECRET
# Build-time variable
- key: API_VERSION
scope: BUILD_TIME
value: "2.0"
# Both build and run time
- key: SITE_URL
scope: RUN_AND_BUILD_TIME
value: "https://myapp.example.com"
Setting Variables via CLI
doctl apps update YOUR_APP_ID \
--env "NODE_ENV=production" \
--env "SECRET_KEY=your-secret-value"
Adding a Database
Managed PostgreSQL
# .do/app.yaml
name: my-express-app
services:
- name: api
github:
repo: your-username/your-repo
branch: main
build_command: npm install
run_command: npm start
http_port: 8080
instance_size_slug: basic-xxs
envs:
- key: NODE_ENV
value: production
- key: DATABASE_URL
scope: RUN_TIME
value: ${db.DATABASE_URL}
databases:
- name: db
engine: PG
version: "15"
size: db-s-dev-database
num_nodes: 1
The ${db.DATABASE_URL} syntax references the database component by name and injects its connection string automatically.
Using the Database in Express
// db.js
var { Pool } = require("pg");
var pool = new Pool({
connectionString: process.env.DATABASE_URL,
ssl: process.env.NODE_ENV === "production" ? { rejectUnauthorized: false } : false
});
module.exports = {
query: function(text, params) {
return pool.query(text, params);
}
};
Running Migrations on Deploy
Add a build command that runs migrations:
services:
- name: api
build_command: npm install && npm run db:migrate
run_command: npm start
{
"scripts": {
"db:migrate": "node scripts/migrate.js",
"start": "node server.js"
}
}
Custom Domains
Adding a Custom Domain
services:
- name: api
routes:
- path: /
domains:
- domain: myapp.example.com
type: PRIMARY
- domain: www.myapp.example.com
type: ALIAS
DNS Configuration
Add a CNAME record pointing to your App Platform URL:
Type: CNAME
Name: myapp (or @ for root domain)
Value: your-app-xxxxx.ondigitalocean.app
TTL: 3600
For root domains (example.com without www), use DigitalOcean DNS or a provider that supports ALIAS records.
App Platform automatically provisions and renews SSL certificates through Let's Encrypt.
Scaling
Horizontal Scaling
services:
- name: api
instance_count: 3
instance_size_slug: professional-xs
Auto-Scaling
services:
- name: api
instance_count: 1
instance_size_slug: professional-xs
autoscaling:
min_instance_count: 1
max_instance_count: 5
metrics:
cpu:
percent: 70
App Platform adds instances when CPU usage exceeds 70% and removes them when it drops below.
Instance Sizes
| Slug | vCPU | RAM | Monthly Cost |
|---|---|---|---|
| basic-xxs | 1 | 256MB | $5 |
| basic-xs | 1 | 512MB | $10 |
| basic-s | 1 | 1GB | $15 |
| professional-xs | 1 | 1GB | $12 |
| professional-s | 1 | 2GB | $25 |
| professional-m | 2 | 4GB | $50 |
Professional plans support horizontal scaling and auto-scaling. Basic plans run a single instance.
Static Assets
Serving Static Files
If your Express app serves static files, ensure they are built during the build phase:
services:
- name: api
build_command: npm install && npm run build
run_command: npm start
Separate Static Site Component
For better performance, serve static files from a separate component:
name: my-app
services:
- name: api
github:
repo: your-username/your-repo
branch: main
source_dir: /
build_command: npm install
run_command: npm start
http_port: 8080
routes:
- path: /api
static_sites:
- name: frontend
github:
repo: your-username/your-repo
branch: main
source_dir: /frontend
build_command: npm run build
output_dir: dist
routes:
- path: /
Logging and Monitoring
Viewing Logs
# Via CLI
doctl apps logs YOUR_APP_ID --type=run
doctl apps logs YOUR_APP_ID --type=build
doctl apps logs YOUR_APP_ID --type=deploy
# Follow logs in real time
doctl apps logs YOUR_APP_ID --type=run --follow
Structured Logging
App Platform captures stdout and stderr. Use structured logging for better searchability:
function log(level, message, data) {
console.log(JSON.stringify({
level: level,
message: message,
data: data,
timestamp: new Date().toISOString()
}));
}
app.use(function(req, res, next) {
var start = Date.now();
res.on("finish", function() {
log("info", "request", {
method: req.method,
path: req.path,
status: res.statusCode,
duration: Date.now() - start
});
});
next();
});
Health Checks
Configure health check settings in the app spec:
services:
- name: api
health_check:
http_path: /health
initial_delay_seconds: 10
period_seconds: 30
timeout_seconds: 5
success_threshold: 1
failure_threshold: 3
Deployment Strategies
Zero-Downtime Deployments
App Platform deploys new versions alongside the running version. Traffic switches only after the new version passes health checks. No configuration needed — this is the default behavior.
Deploy on Push
services:
- name: api
github:
branch: main
deploy_on_push: true
Every push to the main branch triggers a new deployment automatically.
Manual Deployments
Disable deploy_on_push and trigger deployments manually:
doctl apps create-deployment YOUR_APP_ID
Branch-Based Environments
Create separate apps for staging and production:
# .do/app-staging.yaml
name: my-app-staging
services:
- name: api
github:
branch: develop
deploy_on_push: true
envs:
- key: NODE_ENV
value: staging
# .do/app-production.yaml
name: my-app-production
services:
- name: api
github:
branch: main
deploy_on_push: true
envs:
- key: NODE_ENV
value: production
Complete App Spec Example
name: grizzlypeak-app
region: nyc
services:
- name: web
github:
repo: grizzlypeaksoftware/website
branch: master
deploy_on_push: true
build_command: npm install
run_command: npm start
http_port: 8080
instance_count: 1
instance_size_slug: professional-xs
health_check:
http_path: /health
initial_delay_seconds: 15
period_seconds: 30
routes:
- path: /
envs:
- key: NODE_ENV
value: production
- key: PORT
value: "8080"
- key: DATABASE_URL
scope: RUN_TIME
value: ${db.DATABASE_URL}
- key: SESSION_SECRET
scope: RUN_TIME
type: SECRET
databases:
- name: db
engine: PG
version: "15"
size: db-s-dev-database
num_nodes: 1
alerts:
- rule: DEPLOYMENT_FAILED
- rule: DOMAIN_FAILED
Common Issues and Troubleshooting
Build fails with "npm ERR! Missing script: start"
Your package.json does not have a start script:
Fix: Add "start": "node server.js" to the scripts section. Or specify the run_command in your app spec.
Application crashes with "Error: listen EACCES"
The application is trying to listen on a restricted port:
Fix: Use process.env.PORT for the listen port. App Platform sets this automatically. Do not hardcode port 80 or 443.
Database connection fails
The DATABASE_URL is not set or the SSL configuration is wrong:
Fix: Verify the environment variable is correctly referencing the database component (${db.DATABASE_URL}). Enable SSL in your database connection with ssl: { rejectUnauthorized: false } for managed databases.
Custom domain shows "Certificate pending"
DNS changes have not propagated yet:
Fix: Verify the CNAME record points to your App Platform URL. DNS propagation can take up to 48 hours. Check with dig myapp.example.com CNAME. Ensure no conflicting A records exist.
Deployment succeeds but app returns 502
The application is crashing after starting:
Fix: Check runtime logs with doctl apps logs YOUR_APP_ID --type=run. Common causes: missing environment variables, failed database connections, uncaught exceptions during startup. Add error handling to your application startup.
Best Practices
- Use an app spec file. Version control your infrastructure configuration alongside your code. The app spec is reproducible and reviewable.
- Set NODE_ENV to production. This enables Express production optimizations, disables verbose error messages, and signals npm to skip devDependencies.
- Use health check endpoints. App Platform uses health checks for zero-downtime deployments. A simple
/healthendpoint that returns 200 is sufficient. - Store secrets as encrypted environment variables. Never commit secrets to your repository. Use App Platform's secret variables for database URLs, API keys, and session secrets.
- Start with the smallest instance size. Scale up based on actual usage, not anticipated usage. Basic-xxs at $5/month handles more traffic than most new applications need.
- Enable deploy on push for staging, manual deploy for production. Automatic deployments to staging catch issues early. Manual deployments to production give you control over release timing.
- Use structured JSON logging. App Platform captures stdout. JSON logs are searchable and parseable by monitoring tools.
- Separate static assets from the API. Serving static files through Express works but is slower than a dedicated static site component with CDN.