π οΈ 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
mkdir compose-demo && cd compose-demomkdir backendYour 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-alpineWORKDIR /appCOPY package*.json ./RUN npm installCOPY . .ENV PORT=4000CMD ["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:
| Key | Explanation |
|---|---|
build: ./backend | Build image from backend/Dockerfile |
ports: "4000:4000" | Expose the API on your machineβs port 4000 |
DATABASE_URL | Points to db (the service name) β Docker resolves it automatically |
depends_on: db | Start db before backend |
restart: on-failure | Restart backend if it crashes (useful if DB isnβt ready yet) |
restart: always (db) | Always restart Postgres if it ever goes down |
POSTGRES_USER | Sets the default database username |
image: postgres:16-alpine | Use the official Postgres image β no Dockerfile needed |
volumes: pgdata | Persist DB data in a named volume |
π Step 5 β Start Everything
docker compose up --buildWatch the logs β youβll see both db and backend start up. Once ready, open:
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-failurewill automatically bring it back up once the DB is available.
π Step 6 β Useful Commands While Running
Open a new terminal and try these:
# See what's runningdocker compose ps
# View live logs for a specific servicedocker compose logs -f backend
# Get a shell inside the backend containerdocker compose exec backend sh
# Get a psql shell inside the databasedocker compose exec db psql -U postgres -d demodbπ Step 7 β Shut It Down
# 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
| Layer | Technology | Access |
|---|---|---|
| API | Node.js + Express | http://localhost:4000 |
| Database | PostgreSQL 16 | localhost:5432 (from your machine) |
| Internal connection | pg via DATABASE_URL | db:5432 (inside Docker network) |
You now have a fully containerized multi-service app that starts with a single command. π