Deploy a stack with Docker Compose

2 min read07 November 2026

Docker Compose describes several containers in one file and starts them together. For a web application with its database, that is clearer than a series of docker run commands. This example deploys a Node.js application and PostgreSQL, keeps data in a volume and exposes only the application to Nginx.

File layout

Work in /srv/app with a deployment user:

Bash
sudo mkdir -p /srv/app
sudo chown deploy:www-data /srv/app

The folder contains compose.yaml, the application Dockerfile and a .env file with chmod 600. The .env never goes into Git. The application listens on 0.0.0.0 and reads its port from process.env.PORT.

The compose.yaml file

YAML
services:
  app:
    build: .
    restart: unless-stopped
    env_file: .env
    ports:
      - "127.0.0.1:3000:3000"
    depends_on:
      db:
        condition: service_healthy

  db:
    image: postgres:17
    restart: unless-stopped
    environment:
      POSTGRES_DB: app
      POSTGRES_USER: app
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    volumes:
      - db-data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app -d app"]
      interval: 10s
      timeout: 5s
      retries: 5

volumes:
  db-data:

The version key is no longer needed. ports binds the application to 127.0.0.1 only: Nginx will proxy it. The database has no ports, so it is reachable only on the Compose network. depends_on with condition: service_healthy waits for PostgreSQL to answer. The named volume db-data survives docker compose down.

Variables and secrets

.env holds POSTGRES_PASSWORD and application variables. compose.yaml references it with ${POSTGRES_PASSWORD}. docker compose config prints the resolved configuration and reports a missing variable before startup.

Variables remain visible through docker inspect and in the container environment. For a highly sensitive secret, use Compose secrets or an external manager. Never commit .env, even to a private repository.

Start and verify

Bash
docker compose config
docker compose up -d --build
docker compose ps
docker compose logs -f app
curl -I http://127.0.0.1:3000/

up -d builds the image if needed, creates the network and starts the services. ps should show the application running and the database healthy. A container restarting in a loop is diagnosed with logs, not by repeating restart. Then test the domain through Nginx.

Update and back up

Bash
docker compose pull
docker compose up -d
docker compose exec -T db pg_dump -U app app | gzip > /var/backups/app-$(date +%F).sql.gz

Pin postgres:17 instead of latest to control major upgrades. docker compose down -v deletes volumes and therefore data: never use it by reflex. Before a major update, back up the volume and test the restore in another directory or on a test VPS.

Advertisement

Was this article helpful?

Read next