Deploy a stack with Docker Compose
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:
sudo mkdir -p /srv/app
sudo chown deploy:www-data /srv/appThe 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
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
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
docker compose pull
docker compose up -d
docker compose exec -T db pg_dump -U app app | gzip > /var/backups/app-$(date +%F).sql.gzPin 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
- Install Docker on DebianInstall Docker from the official repository, verify the daemon and understand the limits of a system container.
- Keep a Node.js app online with PM2Run a Node.js application under PM2, restart it at boot and read its logs.
- Install PostgreSQL on DebianInstall PostgreSQL, create a role and a database and understand pg_hba.conf without exposing the database.