Docker Compose 101: Multi-container apps lokaal draaien (voor beginners)
Docker Compose is een hulpmiddel waarmee je meerdere containers samen als één applicatie kunt definiëren en starten. In plaats van losse docker run-commando’s te onthouden voor een database, een backend en eventueel een reverse proxy, beschrijf je alles in één Compose-bestand en beheer je de hele “stack” met een paar commando’s.
In deze tutorial leer je wat Docker Compose is, hoe je het installeert en gebruikt, en je bouwt stap voor stap een multi-container applicatie (een webapp + database) die je lokaal kunt draaien. Je krijgt ook uitleg over netwerken, volumes, omgevingsvariabelen, logs, debugging, herstartbeleid en veelgemaakte fouten.
Inhoud
- 1. Wat is Docker Compose?
- 2. Begrippen: container, image, service, netwerk, volume
- 3. Vereisten en installatie checken
- 4. Projectstructuur opzetten
- 5. Eerste Compose-bestand: web + database
- 6. De applicatie starten, stoppen en opruimen
- 7. Data bewaren met volumes (persistente opslag)
- 8. Omgevingsvariabelen en
.env - 9. Netwerken en service discovery
- 10.
depends_onen “wachten tot de database klaar is” - 11. Logs, shells en debugging
- 12. Herstartbeleid en gezondheidchecks
- 13. Profielen en extra services (optioneel)
- 14. Veelgemaakte fouten en oplossingen
- 15. Handige Compose-commando’s (spiekbriefje)
1. Wat is Docker Compose?
Docker Compose is een tool die:
- meerdere containers als services definieert in één bestand;
- automatisch een netwerk aanmaakt zodat services elkaar kunnen vinden;
- volumes kan beheren voor persistente data;
- je in staat stelt om met één commando alles te starten of te stoppen.
Zonder Compose zou je bijvoorbeeld dit moeten doen:
docker run ... postgresdocker run ... backend- een netwerk maken en containers eraan koppelen
- poorten mappen
- volumes aanmaken
- environment variables doorgeven
Met Compose beschrijf je dat declaratief en herhaalbaar.
2. Begrippen: container, image, service, netwerk, volume
Image
Een image is een “template” (een pakket) met een bestandssysteem en metadata om een container te starten. Voorbeelden: postgres:16, node:20-alpine, nginx:alpine.
Container
Een container is een draaiende instantie van een image. Containers zijn in principe tijdelijk: als je ze verwijdert, ben je data kwijt tenzij je volumes gebruikt.
Service (Compose)
In Compose is een service een beschrijving van “hoe een container moet draaien”: welk image, welke poorten, welke volumes, welke environment variables, enz. Compose kan meerdere replicas draaien, maar voor beginners is één container per service het meest gebruikelijk.
Netwerk
Compose maakt standaard een eigen netwerk aan per project. Services kunnen elkaar bereiken via de servicenaam als hostname. Dus een service db is bereikbaar als db op de interne poort (bijvoorbeeld 5432 voor Postgres).
Volume
Een volume is persistente opslag beheerd door Docker. Hiermee blijft data bestaan als je containers opnieuw maakt. Voor databases is dit essentieel.
3. Vereisten en installatie checken
Je hebt nodig:
- Docker Desktop (Windows/macOS) of Docker Engine (Linux)
- Docker Compose (meestal inbegrepen in Docker Desktop en moderne Docker-installaties)
Controleer je installatie:
docker version
docker compose version
Let op: het moderne commando is docker compose (met spatie). Soms bestaat ook docker-compose (met streepje), maar deze tutorial gebruikt de moderne variant.
4. Projectstructuur opzetten
We maken een simpele Node.js webapp die een teller in een Postgres database bijhoudt. Dit is bewust klein, maar realistisch: je ziet meteen hoe services samenwerken.
Maak een map:
mkdir compose-101
cd compose-101
Maak deze structuur:
compose-101/
app/
package.json
server.js
docker-compose.yml
.env
Maak de map app:
mkdir app
5. Eerste Compose-bestand: web + database
5.1 De Node.js app maken
Maak app/package.json:
{
"name": "compose-101-app",
"version": "1.0.0",
"main": "server.js",
"type": "commonjs",
"scripts": {
"start": "node server.js"
},
"dependencies": {
"pg": "^8.11.3"
}
}
Maak app/server.js:
const http = require("http");
const { Client } = require("pg");
const PORT = process.env.PORT || 3000;
const dbConfig = {
host: process.env.DB_HOST || "db",
port: Number(process.env.DB_PORT || 5432),
user: process.env.DB_USER || "postgres",
password: process.env.DB_PASSWORD || "postgres",
database: process.env.DB_NAME || "postgres",
};
async function ensureSchema() {
const client = new Client(dbConfig);
await client.connect();
await client.query(`
CREATE TABLE IF NOT EXISTS counter (
id INTEGER PRIMARY KEY,
value INTEGER NOT NULL
);
`);
await client.query(`
INSERT INTO counter (id, value)
VALUES (1, 0)
ON CONFLICT (id) DO NOTHING;
`);
await client.end();
}
async function incrementAndGet() {
const client = new Client(dbConfig);
await client.connect();
const res = await client.query(`
UPDATE counter
SET value = value + 1
WHERE id = 1
RETURNING value;
`);
await client.end();
return res.rows[0].value;
}
const server = http.createServer(async (req, res) => {
try {
if (req.url === "/") {
const value = await incrementAndGet();
res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
res.end(`Hallo! Teller staat nu op: ${value}\n`);
return;
}
if (req.url === "/health") {
res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
res.end("ok\n");
return;
}
res.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
res.end("Niet gevonden\n");
} catch (err) {
res.writeHead(500, { "Content-Type": "text/plain; charset=utf-8" });
res.end(`Fout: ${err.message}\n`);
}
});
(async () => {
await ensureSchema();
server.listen(PORT, () => {
console.log(`Web luistert op poort ${PORT}`);
console.log(`DB host: ${dbConfig.host}:${dbConfig.port}`);
});
})();
Belangrijk om te begrijpen:
- De app gebruikt
DB_HOST=dbals default. Dat werkt omdat Compose services elkaar kunnen bereiken via de servicenaamdb. - De app maakt bij start de tabel aan en initialiseert een rij.
- Elke request naar
/verhoogt de teller.
5.2 Compose-bestand maken
Maak docker-compose.yml in de root:
services:
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_USER: ${DB_USER}
POSTGRES_DB: ${DB_NAME}
volumes:
- pgdata:/var/lib/postgresql/data
ports:
- "5432:5432"
web:
image: node:20-alpine
working_dir: /app
volumes:
- ./app:/app
command: sh -c "npm install && npm start"
environment:
PORT: 3000
DB_HOST: db
DB_PORT: 5432
DB_USER: ${DB_USER}
DB_PASSWORD: ${DB_PASSWORD}
DB_NAME: ${DB_NAME}
ports:
- "3000:3000"
depends_on:
- db
volumes:
pgdata:
Wat gebeurt hier precies?
services:bevat twee services:dbenweb.dbgebruikt de officiëlepostgres:16image.dbkrijgt environment variables om een database te initialiseren.dbgebruikt een volumepgdatazodat je data niet verdwijnt.dbmappt poort5432naar je host zodat je lokaal ook met tools kunt verbinden.webgebruiktnode:20-alpineen bind-mount de lokale map./appnaar/appin de container.commandinstalleert dependencies en start de server.depends_onstartdbeerst, maar let op: dit betekent niet automatisch dat Postgres al klaar is (daarover later meer).
5.3 .env maken
Maak .env:
DB_USER=postgres
DB_PASSWORD=postgres
DB_NAME=compose101
Compose leest standaard een .env in dezelfde map als docker-compose.yml. Variabelen zoals ${DB_PASSWORD} worden dan ingevuld.
6. De applicatie starten, stoppen en opruimen
6.1 Starten
Start alles:
docker compose up
Je ziet logs van beide services. Wil je het in de achtergrond draaien:
docker compose up -d
Controleer draaiende containers:
docker compose ps
6.2 Testen in de browser of met curl
In een andere terminal:
curl http://localhost:3000/
curl http://localhost:3000/
curl http://localhost:3000/
Je zou zien dat de teller oploopt.
6.3 Stoppen
Stop services (containers blijven bestaan):
docker compose stop
Stop en verwijder containers, maar behoud volumes:
docker compose down
Stop en verwijder containers én volumes (data weg):
docker compose down -v
7. Data bewaren met volumes (persistente opslag)
Waarom is het volume belangrijk?
Een database schrijft data in /var/lib/postgresql/data. Als je geen volume gebruikt, staat die data in de containerlaag. Verwijder je de container, dan is je database leeg.
In het Compose-bestand:
volumes:
- pgdata:/var/lib/postgresql/data
En onderaan:
volumes:
pgdata:
Dit maakt een named volume. Je kunt volumes bekijken:
docker volume ls
docker volume inspect compose-101_pgdata
Let op: Compose geeft volumes vaak een projectprefix. Als je map compose-101 heet, kan het volume compose-101_pgdata heten.
Wil je testen dat data echt blijft?
- Start stack en doe een paar requests.
- Doe
docker compose down(zonder-v). - Start opnieuw met
docker compose up -d. - Doe weer
curl http://localhost:3000/en kijk of de teller doorgaat.
8. Omgevingsvariabelen en .env
Je hebt twee soorten variabelen om uit elkaar te houden:
- Compose-variabelen (interpolatie):
${DB_PASSWORD}indocker-compose.ymlwordt ingevuld vanuit.envof je shell. - Container-variabelen (runtime):
environment:bepaalt wat er in de container beschikbaar is.
In ons bestand gebeurt beide tegelijk: ${DB_PASSWORD} wordt door Compose vervangen en als waarde doorgegeven aan de container.
Handig commando om te zien wat Compose ervan maakt:
docker compose config
Dit toont de “uitgewerkte” configuratie met ingevulde variabelen.
Tip: zet geen echte productie-wachtwoorden in .env als je dit project deelt. Voor lokaal leren is het prima.
9. Netwerken en service discovery
Compose maakt standaard een netwerk aan, bijvoorbeeld compose-101_default. Daardoor:
- kan
webde database bereiken via hostnamedb - hoef je geen IP-adressen te gebruiken
- kun je services herstarten zonder dat adressen veranderen
Belangrijk: binnen het Compose-netwerk gebruik je de containerpoort, niet de hostpoort. Dus:
webpraat metdb:5432(intern)- jij op je laptop praat met
localhost:5432(extern, via port mapping)
Bekijk netwerken:
docker network ls
docker network inspect compose-101_default
10. depends_on en “wachten tot de database klaar is”
depends_on regelt de startvolgorde, niet de gereedheid. Postgres kan een paar seconden nodig hebben om te initialiseren. Onze app probeert direct tabellen aan te maken; soms gaat dat te snel en krijg je een verbindingsfout.
10.1 Simpele oplossing: retry-logica in de app
In echte projecten bouw je vaak retry’s in (bijvoorbeeld 10 pogingen met wachttijd). Dat is robuust, want ook in productie kan een database tijdelijk niet bereikbaar zijn.
10.2 Compose-oplossing: healthcheck en conditionele afhankelijkheid
Je kunt een healthcheck toevoegen aan db en dan web laten wachten tot db “healthy” is. Dit werkt in veel omgevingen, maar gedrag kan verschillen per Compose-versie. Toch is het nuttig om te kennen.
Pas docker-compose.yml aan:
services:
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_USER: ${DB_USER}
POSTGRES_DB: ${DB_NAME}
volumes:
- pgdata:/var/lib/postgresql/data
ports:
- "5432:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_USER} -d ${DB_NAME}"]
interval: 5s
timeout: 3s
retries: 10
web:
image: node:20-alpine
working_dir: /app
volumes:
- ./app:/app
command: sh -c "npm install && npm start"
environment:
PORT: 3000
DB_HOST: db
DB_PORT: 5432
DB_USER: ${DB_USER}
DB_PASSWORD: ${DB_PASSWORD}
DB_NAME: ${DB_NAME}
ports:
- "3000:3000"
depends_on:
db:
condition: service_healthy
Als jouw Compose-variant condition: service_healthy niet accepteert, dan is de beste aanpak: retry’s in de app of een klein “wait-for” script.
11. Logs, shells en debugging
11.1 Logs bekijken
Alle logs volgen:
docker compose logs -f
Alleen web:
docker compose logs -f web
Alleen db:
docker compose logs -f db
11.2 Een shell openen in een container
In de webcontainer:
docker compose exec web sh
In de db-container:
docker compose exec db bash
Let op: sommige images (zoals alpine) hebben sh maar geen bash.
11.3 Verbinden met Postgres via psql
In de db-container:
docker compose exec db psql -U postgres -d compose101
Dan kun je queries doen:
SELECT * FROM counter;
Stop psql met:
\q
11.4 Containers inspecteren
Bekijk details:
docker compose ps
docker inspect compose-101-web-1
(De exacte containernaam kan verschillen.)
12. Herstartbeleid en gezondheidchecks
12.1 Restart policy
Voor lokale ontwikkeling wil je vaak dat containers automatisch herstarten bij een crash. Voeg toe:
services:
web:
restart: unless-stopped
db:
restart: unless-stopped
Uitleg:
no(standaard): niet herstartenalways: altijd herstartenunless-stopped: herstarten tenzij je hem bewust stopton-failure: herstarten bij fout exit code
12.2 Healthchecks
Een healthcheck is een periodieke test. Voor web kun je bijvoorbeeld:
services:
web:
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://localhost:3000/health >/dev/null 2>&1 || exit 1"]
interval: 10s
timeout: 3s
retries: 5
Waarom nuttig?
- Je ziet in
docker compose psof een service “healthy” is. - Andere tooling kan op health status reageren.
13. Profielen en extra services (optioneel)
Soms wil je extra tools alleen af en toe starten, zoals een database-admin UI. Met profielen kun je dat netjes scheiden.
Voorbeeld: voeg Adminer toe (web UI voor databases):
services:
adminer:
image: adminer:4
ports:
- "8080:8080"
profiles:
- tools
Start normaal (zonder tools):
docker compose up -d
Start met tools-profiel:
docker compose --profile tools up -d
Ga dan naar:
http://localhost:8080
Login:
- Systeem:
PostgreSQL - Server:
db - Gebruiker:
postgres - Wachtwoord:
postgres - Database:
compose101
14. Veelgemaakte fouten en oplossingen
14.1 “Port is already allocated”
Fout: poort 5432 of 3000 is al in gebruik.
Oplossingen:
- Stop de andere service die die poort gebruikt.
- Of wijzig de hostpoort:
ports:
- "5433:5432"
Dan verbind je lokaal met localhost:5433, maar intern blijft het db:5432.
14.2 Database blijft leeg na herstart
Waarschijnlijk heb je docker compose down -v gebruikt, waardoor het volume is verwijderd. Gebruik docker compose down zonder -v als je data wilt behouden.
14.3 “Web kan db niet vinden”
Controleer:
- Gebruik je
DB_HOST=db? - Draait de db-service?
- Zit je in hetzelfde Compose-project?
Check:
docker compose ps
docker compose logs db
docker compose exec web sh -c "cat /etc/resolv.conf && nslookup db || true"
14.4 Permissieproblemen met bind mounts
Op sommige systemen kunnen permissies lastig zijn. Als npm install faalt door rechten, kun je:
- een named volume gebruiken voor
node_modules - of draaien als een specifieke gebruiker
Eenvoudige aanpak: voeg een volume toe voor node_modules:
services:
web:
volumes:
- ./app:/app
- node_modules:/app/node_modules
volumes:
pgdata:
node_modules:
15. Handige Compose-commando’s (spiekbriefje)
Starten (foreground):
docker compose up
Starten (background):
docker compose up -d
Herbouwen (als je een eigen Dockerfile gebruikt):
docker compose up --build
Stoppen:
docker compose stop
Down (containers weg, netwerk weg):
docker compose down
Down inclusief volumes:
docker compose down -v
Status:
docker compose ps
Logs:
docker compose logs -f
docker compose logs -f web
Commando in container:
docker compose exec web sh
docker compose exec db psql -U postgres -d compose101
Configuratie uitgeschreven:
docker compose config
Afronding: wat je nu kunt
Na deze tutorial kun je:
- een multi-container applicatie lokaal draaien met Compose;
- begrijpen hoe services elkaar vinden via netwerken;
- data persistent maken met volumes;
- omgevingsvariabelen beheren met
.env; - logs lezen en debugging doen met
exec; - basis-robustheid toevoegen met restart policies en healthchecks.
Als je hierna een stap verder wilt, zijn logische vervolgstappen:
- een eigen
Dockerfilevoor de webapp maken (in plaats vannode:20-alpine+ bind mount) - verschillende omgevingen maken (development vs. productie) met overrides of profielen
- secrets en betere wachtwoordafhandeling gebruiken
- een reverse proxy toevoegen (bijvoorbeeld Nginx) voor meerdere webservices
Wil je dat ik een variant toevoeg met een eigen Dockerfile en een productie-achtige setup (zonder npm install bij elke start), dan kan dat met dezelfde basisstructuur.