Skip to content

πŸ› οΈ Hands-On β€” Setting Up a Multi-Service App with docker-compose.yml

Let’s build and run a real multi-service app from scratch using Docker Compose. We’ll set up a Node.js Express API that connects to a PostgreSQL database β€” fully containerized and running with one command.


πŸ“ Step 1 β€” Create the Project Structure

Terminal window
mkdir compose-demo && cd compose-demo
mkdir backend

Your folder should look like this:

compose-demo/
β”œβ”€β”€ backend/
β”‚ β”œβ”€β”€ index.js
β”‚ β”œβ”€β”€ package.json
β”‚ └── Dockerfile
└── docker-compose.yml

πŸ“ Step 2 β€” Create the Express App

backend/package.json

{
"name": "compose-demo-backend",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"start": "node index.js"
},
"dependencies": {
"express": "^5.2.1",
"pg": "^8.13.1"
}
}

backend/index.js

const express = require('express');
const { Pool } = require('pg');
const app = express();
const port = process.env.PORT || 4000;
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
});
app.get('/', async (req, res) => {
try {
const result = await pool.query('SELECT NOW() AS time');
res.json({ message: 'Hello from Docker Compose! πŸ™', time: result.rows[0].time });
} catch (err) {
res.status(500).json({ error: err.message });
}
});
app.listen(port, () => {
console.log(`Backend running on port ${port}`);
});

🐳 Step 3 β€” Create the Backend Dockerfile

backend/Dockerfile

FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
ENV PORT=4000
CMD ["node", "index.js"]

πŸ™ Step 4 β€” Write the docker-compose.yml

Create docker-compose.yml at the project root (not inside backend/):

version: '3.8'
services:
backend:
build: ./backend
ports:
- "4000:4000"
environment:
- DATABASE_URL=postgres://postgres:secret@db:5432/demodb
depends_on:
- db
restart: on-failure
db:
image: postgres:16-alpine
restart: always
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: secret
POSTGRES_DB: demodb
volumes:
- pgdata:/var/lib/postgresql/data
ports:
- "5432:5432"
volumes:
pgdata:

Breaking it down:

KeyExplanation
build: ./backendBuild image from backend/Dockerfile
ports: "4000:4000"Expose the API on your machine’s port 4000
DATABASE_URLPoints to db (the service name) β€” Docker resolves it automatically
depends_on: dbStart db before backend
restart: on-failureRestart backend if it crashes (useful if DB isn’t ready yet)
restart: always (db)Always restart Postgres if it ever goes down
POSTGRES_USERSets the default database username
image: postgres:16-alpineUse the official Postgres image β€” no Dockerfile needed
volumes: pgdataPersist DB data in a named volume

πŸš€ Step 5 β€” Start Everything

Terminal window
docker compose up --build

Watch the logs β€” you’ll see both db and backend start up. Once ready, open:

πŸ‘‰ http://localhost:4000

You should see:

{
"message": "Hello from Docker Compose! πŸ™",
"time": "2026-02-27T08:00:00.000Z"
}

If the backend crashes on first start (because Postgres isn’t ready yet), restart: on-failure will automatically bring it back up once the DB is available.


πŸ” Step 6 β€” Useful Commands While Running

Open a new terminal and try these:

Terminal window
# See what's running
docker compose ps
# View live logs for a specific service
docker compose logs -f backend
# Get a shell inside the backend container
docker compose exec backend sh
# Get a psql shell inside the database
docker compose exec db psql -U postgres -d demodb

πŸ›‘ Step 7 β€” Shut It Down

Terminal window
# Stop containers (data is preserved in the volume)
docker compose down
# Stop AND delete the database volume (clean slate)
docker compose down -v

βœ… What You Built

LayerTechnologyAccess
APINode.js + Expresshttp://localhost:4000
DatabasePostgreSQL 16localhost:5432 (from your machine)
Internal connectionpg via DATABASE_URLdb:5432 (inside Docker network)

You now have a fully containerized multi-service app that starts with a single command. πŸŽ‰