Digitalocean

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
  • doctl CLI 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

  1. Log in to DigitalOcean and navigate to Apps
  2. Click Create App
  3. Select your Git repository and branch
  4. App Platform auto-detects Node.js and configures the build
  5. Review the detected settings:
    • Build Command: npm install (automatic)
    • Run Command: npm start (from package.json)
    • HTTP Port: 8080 (default)
  6. Choose a plan (Basic, Professional, or Pro)
  7. 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 /health endpoint 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.

References

Powered by Contentful