Module 9 · 9.3
Docker compose en de firewall
Module 9: Docker en docker compose · Leestijd: 12 minuten
Leerdoel: Na deze les kun je diensten beschrijven in een compose-bestand, ze starten, bijwerken en stoppen, en leg je uit waarom gepubliceerde poorten van Docker langs ufw heen gaan en hoe je dat voorkomt.
Kernbegrippen: compose.yaml, services, image, ports, volumes, environment, .env, restart, networks, depends_on, healthcheck, docker compose up -d, pull, logs, down, vervallen version:, Docker en ufw, DOCKER-USER, publiceren op 127.0.0.1, geheimen, docker.sock
Waarom compose?
Met docker run (les 9.2) start je één container met een lange regel opties. Een echte toepassing bestaat vaak uit meerdere containers: een programma, een database, een reverse proxy. Die regels onthouden en na een herstart of update opnieuw intypen, gaat een keer mis.
Docker Compose beschrijft alle containers van een toepassing in één bestand: compose.yaml. Je zet dat bestand in een eigen map, bijvoorbeeld /srv/status/compose.yaml, en beheert alles met docker compose. Het bestand is meteen je documentatie: wat draait er, welk image, welke poorten, waar staan de gegevens. Zet die map in je back-up (les 8.1), en je kunt de toepassing op een nieuwe server in een paar minuten terugzetten.
De plug-in heet docker compose (met een spatie, versie 2). Het oude losse programma docker-compose (met streepje) is vervangen; je ziet het nog in oude handleidingen. Ook het veld version: "3.8" bovenaan oude bestanden is vervallen: Compose gebruikt altijd het nieuwste schema en geeft bij version: alleen een waarschuwing.
Een compose-bestand lezen
Een voorbeeld met een programma en een PostgreSQL-database:
# /srv/notities/compose.yaml
services:
app:
image: ghcr.io/example/notities:1.4
restart: unless-stopped
ports:
- "127.0.0.1:8081:8080"
environment:
DB_HOST: db
DB_USER: notities
DB_PASSWORD: ${DB_PASSWORD}
depends_on:
db:
condition: service_healthy
db:
image: postgres:17
restart: unless-stopped
environment:
POSTGRES_USER: notities
POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes:
- db-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U notities"]
interval: 10s
retries: 5
volumes:
db-data:Stap voor stap:
- services: elke dienst wordt een container. De naam (
app,db) is ook de hostnaam waarmee de containers elkaar vinden. Compose maakt voor de toepassing een eigen netwerk, dusappverbindt met de database opdb:5432. - image: altijd met een versie-tag (les 9.1).
- restart: wat Docker doet als de container stopt of de server herstart.
unless-stoppedstart hem weer, behalve als jij hem bewust stopte. Zonder restart-beleid komt je toepassing na een herstart (les 4.2) niet terug. - ports: alleen de app publiceert een poort, en alleen op
127.0.0.1. De database publiceert niets: de app bereikt hem via het interne netwerk. Een database metports: - "5432:5432"is een van de meest gemaakte fouten (zie hieronder). - environment: instellingen voor het programma. Het wachtwoord staat er niet letterlijk in, maar als
${DB_PASSWORD}. - volumes: het benoemde volume
db-datahoudt de database vast als de container vervangen wordt. - depends_on met healthcheck: de app start pas als de database echt klaar is, niet alleen gestart.
Geheimen in een .env-bestand
Compose leest variabelen als ${DB_PASSWORD} uit een bestand .env in dezelfde map:
# /srv/notities/.env
DB_PASSWORD=Kies-hier-een-lang-willekeurig-wachtwoordanna@web1:/srv/notities$ sudo chmod 600 .envZo staat het wachtwoord niet in het compose-bestand dat je misschien in Git bewaart of op een forum plakt om hulp te vragen. Zet .env in .gitignore. Maak het wachtwoord met een wachtwoordmanager of met openssl rand -base64 24.
Dagelijks gebruik
Alle commando's voer je uit in de map met het compose-bestand:
anna@web1:/srv/notities$ sudo docker compose config # bestand controleren, variabelen ingevuld
anna@web1:/srv/notities$ sudo docker compose up -d # alles starten (of bijwerken naar het bestand)
anna@web1:/srv/notities$ sudo docker compose ps # status per dienst
anna@web1:/srv/notities$ sudo docker compose logs -f app
anna@web1:/srv/notities$ sudo docker compose pull # nieuwe images ophalen
anna@web1:/srv/notities$ sudo docker compose up -d # containers vervangen door de nieuwe images
anna@web1:/srv/notities$ sudo docker compose down # stoppen en containers verwijderendown laat je volumes staan. down -v verwijdert ook de volumes van de toepassing, en daarmee je database. Typ dat alleen als je echt opnieuw wilt beginnen.
docker compose config is het equivalent van nginx -t en sshd -t: het laat zien hoe Compose je bestand begrijpt, met alle variabelen ingevuld, voordat je iets start. Let op dat je de uitvoer dan niet ergens plakt, want de wachtwoorden staan erin.
Docker gaat langs ufw heen
Nu het belangrijkste veiligheidspunt van deze module. In les 5.2 zette je ufw op "alles weigeren, behalve SSH, 80 en 443". Je zou verwachten dat een container met ports: - "5432:5432" dan niet bereikbaar is. Dat is niet zo.
De Docker-documentatie zegt het letterlijk: Docker en ufw gebruiken firewallregels op een manier die niet met elkaar samengaat. Als je een poort publiceert, zet Docker zelf regels in de firewall van de kernel. Die regels sturen het verkeer door naar de container (adresvertaling, DNAT) voordat de regels van ufw worden toegepast. Het pakketje gaat dus om ufw heen. ufw status toont niets bijzonders, terwijl de poort van heel internet bereikbaar is. Hetzelfde geldt voor firewalld.
Bovendien publiceert Docker zonder adres op 0.0.0.0 en [::], dus op IPv4 én IPv6 (les 9.1).
Drie manieren om het goed te doen, van eenvoudig naar ingewikkeld:
- Publiceer niets wat niet publiek hoeft. Databases, caches en interne diensten krijgen geen
ports:. Containers in dezelfde compose-toepassing bereiken elkaar via het interne netwerk. - Publiceer op loopback.
"127.0.0.1:8081:8080". Dan kan alleen de server zelf erbij, en zet je een reverse proxy (nginx of Caddy, module 7) ervoor die op 443 luistert. Docker kan dit ook als standaard krijgen: een daemon-instelling laat alle gepubliceerde poorten zonder adres op127.0.0.1landen. - Regels in de keten DOCKER-USER. Docker maakt een lege keten
DOCKER-USERdie vóór zijn eigen regels wordt doorlopen. Daar kun je zelf regels zetten, bijvoorbeeld "alleen dit IP-adres mag naar de gepubliceerde poorten". Dat is gevorderd werk met iptables of nftables, en makkelijk fout te doen. Voor de meeste kleine servers zijn 1 en 2 genoeg.
Controleer altijd van buitenaf, zoals in les 5.1: nc -vz -w 3 <ip> 5432 vanaf je laptop, of de cursuscontrole van opdracht Z5.1 en Z10.4.
Nog twee dingen die je niet doet
- Geen
/var/run/docker.sockin een container mounten, tenzij je precies weet waarom. Een container met toegang tot de Docker-socket kan zelf containers starten, ook een met de hele schijf van de host (les 9.2). Sommige hulpprogramma's (dashboards, automatische updaters) vragen erom; dat is in feite root geven aan dat programma. - Geen
privileged: trueofnetwork_mode: hostomdat een handleiding het zegt. Beide halen de afscherming van de container grotendeels weg. Alleen gebruiken als de maker uitlegt waarom het nodig is.
Samenvatting
- Een compose-bestand (
compose.yaml) beschrijft alle containers van een toepassing; het is meteen documentatie en hoort in je back-up. - Gebruik versie-tags, een restart-beleid, benoemde volumes, een healthcheck waar het kan, en geheimen in een
.envmet rechten 600. docker compose config,up -d,ps,logs,pull,down;down -vverwijdert ook de volumes.- Gepubliceerde poorten gaan langs ufw en firewalld heen. Publiceer alleen wat publiek moet, en anders op
127.0.0.1met een reverse proxy ervoor. - Geen docker.sock,
privilegedofnetwork_mode: hostzonder goede reden.
Quiz
Opdracht
Opdracht Z9.3: Een compose-bestand nakijken. In het voorbeeldbestand compose-nakijken.txt staat (als tekstbestand) het compose-bestand van een webwinkeltje met een app, een database, een cache en een beheertool. Er zitten zes fouten in die in deze les of in les 9.1 en 9.2 behandeld zijn. Geef per genummerd regelblok aan of het goed of fout is, en kies bij elke fout de juiste verbetering. Het programma vergelijkt met de vaste sleutel. Heb je zelf een compose-bestand, loop het daarna langs dezelfde punten.
Verder lezen
- Docker-documentatie, How Compose works (compose.yaml): https://docs.docker.com/compose/intro/compose-application-model/
- Docker-documentatie, Version and name top-level elements: https://docs.docker.com/reference/compose-file/version-and-name/
- Docker-documentatie, Packet filtering and firewalls (Docker and ufw): https://docs.docker.com/engine/network/packet-filtering-firewalls/
- Docker-documentatie, Port publishing and mapping: https://docs.docker.com/engine/network/port-publishing/
- Docker-documentatie, Interpolation (variabelen en .env): https://docs.docker.com/reference/compose-file/interpolation/
