SSD Nodes Learn Hosting plans →
Gidsen Matt ConnorDoor Matt Connor · Bijgewerkt 2026-08-27

Docker Compose installeren op Ubuntu 24.04: handleiding

Installeer Docker Engine en Compose v2 op Ubuntu 24.04. Leer hoe u een compose.yml schrijft, de ufw-poortval vermijdt en veilig volumes back-upt voor uw eigen VPS-services.

Wat u gaat bouwen

Docker Compose vormt de basis voor vrijwel alles op deze site. Nextcloud, Vaultwarden, n8n, Immich, Rocket.Chat; elke handleiding begint met "schrijf dit compose-bestand", en dit is de pagina die uitlegt wat dat bestand daadwerkelijk betekent. U installeert Docker Engine en de Compose v2-plugin vanuit de officiële apt-repository van Docker op Ubuntu 24.04. Vervolgens zet u een stack met twee services op: Miniflux, een compacte RSS-lezer, en PostgreSQL. Dit duo maakt gebruik van alle patronen die grotere applicaties ook hanteren: vastgezette images, een database met een healthcheck, een named volume, geheimen in een .env-bestand en een poort die alleen naar localhost is gepubliceerd.

De installatie duurt vijf minuten. De rest van deze handleiding behandelt de onderdelen die later voor problemen kunnen zorgen: de docker-groep die in feite root-rechten verleent, gepubliceerde poorten die uw ufw-regels omzeilen, en de specifieke vlag in docker compose down die uw database verwijdert zonder om bevestiging te vragen.

Vereisten: een verse Ubuntu 24.04 KVM VPS, een gebruiker met sudo-rechten en minimaal een gigabyte aan RAM. Een bestaande Docker-installatie is ook prima; de eerste sectie beschrijft wat u moet verwijderen.

Installatie via de repository van Docker, niet die van Ubuntu

Er zijn twee veelgemaakte fouten die u moet vermijden voordat u het eerste commando uitvoert. Het docker.io-pakket van Ubuntu werkt wel, maar loopt achter op de releases van Docker en mist de plugin-structuur waar alle andere software vanuit gaat. Daarnaast is het losse docker-compose-binary, het bestand met het koppelteken, Compose v1: dit is gebaseerd op Python, heeft sinds 2023 het einde van zijn levenscyclus bereikt en is de reden waarom oudere handleidingen niet meer werken. De huidige versie van Compose is docker compose met een spatie; dit is een CLI-plugin die vanuit dezelfde repository als de engine wordt geïnstalleerd.

Als een van deze onderdelen al op de server aanwezig is, verwijder deze dan eerst, inclusief docker-compose-v2, de eigen verpakking van de plugin door Ubuntu, zodat alles uit één repository afkomstig is:

sudo apt remove -y docker.io docker-compose docker-compose-v2 docker-doc podman-docker containerd runc

Package 'docker.io' is not installed, so not removed is de normale uitvoer op een nieuwe VPS. Voeg daarna de repository van Docker toe en voer de installatie uit:

sudo apt update
sudo apt install -y ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

Controleer alle drie de lagen:

docker --version
docker compose version
sudo docker run --rm hello-world

De eerste twee commando's tonen versiestrings; Docker Compose version v2.x.x bevestigt dat u de plugin gebruikt en niet het verouderde v1-binary. De hello-world-run moet eindigen met Hello from Docker!. Het pakket schakelt de service automatisch in bij het opstarten; systemctl is-enabled docker geeft enabled weer.

De docker-groep is root, beslis met open ogen

Op dit moment vereist elk docker-commando sudo, omdat de socket van de daemon op /var/run/docker.sock eigendom is van root en de docker-groep. Zonder lidmaatschap krijgt u de meest gegoogelde Docker-foutmelding:

permission denied while trying to connect to the Docker daemon socket at
unix:///var/run/docker.sock

De standaardoplossing:

sudo usermod -aG docker $USER

Groepslidmaatschap wordt toegepast bij het inloggen, dus de fout blijft bestaan in uw huidige shell. Voer newgrp docker uit voor deze sessie, of log uit en weer in; id zou daarna docker in uw groepen moeten tonen.

Nu het eerlijke deel, duidelijk gesteld: lidmaatschap van de docker-groep is root op de host. Niet "bijna root", niet "verhoogde rechten", maar root. Iedereen in die groep kan docker run --rm -it -v /:/host alpine chroot /host uitvoeren en het volledige bestandssysteem overnemen, zonder dat er om een wachtwoord wordt gevraagd. De groep bestaat voor het gemak, niet voor isolatie.

De rootless-modus van Docker is het echte alternatief; de daemon zelf draait dan als uw gebruiker zonder privileges. Dit heeft nadelen: poorten onder 1024 vereisen extra configuratie, netwerkverkeer verloopt via een userspace-shim met meetbare overhead, en sommige images werken niet correct zonder echte rootrechten. Op een VPS met één beheerder die al over sudo-rechten beschikt, verandert het groepslidmaatschap in de praktijk niets, en dit is wat elke handleiding hier aanneemt. Verstrek deze rechten echter nooit alsof het minder is dan sudo.

Anatomie van een compose-bestand

Geef elke stack een eigen map. De mapnaam wordt de projectnaam, die als voorvoegsel dient voor containers, netwerken en volumes:

sudo mkdir -p /opt/miniflux && sudo chown $USER /opt/miniflux && cd /opt/miniflux

Maak compose.yml aan (de moderne naam; docker-compose.yml werkt ook nog). Sla de oude version:-sleutel over; deze is verouderd en Compose geeft een waarschuwing als het deze tegenkomt.

services:
  miniflux:
    image: miniflux/miniflux:2.2.9
    restart: unless-stopped
    ports:
      - "127.0.0.1:8080:8080"
    environment:
      - DATABASE_URL=postgres://miniflux:${POSTGRES_PASSWORD}@db/miniflux?sslmode=disable
      - RUN_MIGRATIONS=1
      - CREATE_ADMIN=1
      - ADMIN_USERNAME=admin
      - ADMIN_PASSWORD=${ADMIN_PASSWORD}
    depends_on:
      db:
        condition: service_healthy

  db:
    image: postgres:16-alpine
    restart: unless-stopped
    environment:
      - POSTGRES_USER=miniflux
      - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
      - POSTGRES_DB=miniflux
    volumes:
      - db-data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD", "pg_isready", "-U", "miniflux", "-d", "miniflux"]
      interval: 10s
      timeout: 5s
      retries: 5

volumes:
  db-data:

Elke regel hierboven is een bewuste keuze. Neem ze één voor één door.

Pin image-versies, :latest plus een pull is een onbeheerde upgrade

Gebruik postgres:16-alpine, niet postgres:latest. Een tag is niet bevroren: :latest verwijst bij elke pull opnieuw naar wat de beheerder als laatste heeft gepusht. Combineer dit met de gewoonte om regelmatig upgrades uit te voeren die u hier leert, docker compose pull && docker compose up -d, en :latest betekent dat major-versies worden doorgevoerd zodra de upstream-bron ze uitbrengt, niet wanneer u dat wilt. Bij PostgreSQL is dit geen hypothetisch scenario: een onverwachte sprong van 16 naar 17 zorgt ervoor dat de container in een crash-loop belandt vanwege een incompatibele datamap. Postgres major-upgrades vereisen namelijk een dump en restore, geen herstart.

Pin ten minste de major-versie (postgres:16-alpine volgt 16.x patch-releases) en pin applicaties aan een exacte release zoals miniflux/miniflux:2.2.9. Controleer de releases-pagina van het project en gebruik de versie die actueel is op het moment dat u het bestand schrijft. Een upgrade wordt dan een bewuste wijziging van één regel, zichtbaar in git diff.

Publiceer naar 127.0.0.1, omdat Docker om ufw heen loopt

"127.0.0.1:8080:8080", host-adres, host-poort, container-poort. De meeste handleidingen schrijven "8080:8080", wat een afkorting is voor 0.0.0.0:8080:8080: luisteren op elke interface, inclusief de publieke.

Dit is de valkuil waar bijna iedereen een keer in trapt. Docker publiceert een poort door een DNAT-regel te schrijven die de bestemmings-IP van het pakket herschrijft naar het interne IP van de container voordat de filtering plaatsvindt. Het pakket volgt daardoor het FORWARD-pad en raakt nooit INPUT, waar uw ufw-regels staan. sudo ufw deny 8080 rapporteert succes, ufw status toont dat de poort geweigerd wordt, en de service antwoordt nog steeds aan het hele internet. Uw firewall is niet defect; hij wordt bewust omzeild. Waarom Docker ufw omzeilt en hoe u containerverkeer echt filtert legt het mechanisme uit en biedt de DOCKER-USER-oplossing voor poorten die publiek toegankelijk moeten blijven.

De gewoonte die dit probleem volledig wegneemt: bind gepubliceerde poorten aan 127.0.0.1, tenzij u een specifieke reden heeft om dat niet te doen, en plaats een reverse proxy voor alles wat naar de buitenwereld moet kijken. Dat is precies wat de Traefik reverse proxy handleiding opbouwt als volgende stap na deze pagina: één container die poorten 80 en 443 beheert en alles op basis van hostnaam doorstuurt, inclusief TLS. (Komt u van een oudere Traefik v2-opstelling? De Traefik v2 naar v3 migratiehandleiding behandelt de naamswijzigingen en regelveranderingen.)

Controleer de binding na het starten van de stack: sudo ss -tlnp | grep 8080 hoort 127.0.0.1:8080 te tonen, niet 0.0.0.0:8080 of *:8080.

Benoemde volumes versus bind mounts

db-data:/var/lib/postgresql/data is een benoemd volume: Docker maakt en beheert een map onder /var/lib/docker/volumes/ en koppelt deze aan de container. Het alternatief is een bind mount, ./data:/var/lib/postgresql/data, die een door u gekozen pad op de host koppelt.

De scheidslijn die in de praktijk standhoudt: benoemde volumes voor data waar alleen containers bij kunnen, vooral databases, omdat Docker het volume initialiseert met de eigendomsrechten die de image verwacht en de bestandsrechten direct correct zijn. Bind mounts voor bestanden die u vanaf de host beheert, zoals configuratiebestanden die u met een teksteditor bewerkt, een mediabibliotheek die u via rsync synchroniseert, of alles waarvan u het pad duidelijk wilt hebben. De klassieke fout bij bind mounts is eigendom: de container draait als UID 999, uw host-map is eigendom van UID 1000, en de app crasht bij het opstarten met permission denied in de logs. Benoemde volumes laten dat type bug grotendeels verdwijnen, ten koste van het feit dat de data op een door Docker beheerd pad staat, zoals hieronder beschreven.

environment en .env, houd geheimen uit git

${POSTGRES_PASSWORD} wordt niet gelezen vanuit uw shell; Compose interpoleert waarden vanuit een bestand genaamd .env dat naast compose.yml staat. Maak het aan:

cat > .env <<'EOF'
POSTGRES_PASSWORD=change-me-to-something-long
ADMIN_PASSWORD=change-me-too
EOF
chmod 600 .env
echo ".env" >> .gitignore

Genereer echte waarden met openssl rand -hex 24. Gebruik hex, geen base64, met een reden: dit wachtwoord komt in de DATABASE_URL-verbindingsreeks terecht, en de /, + en =-tekens die base64 produceert, verstoren URL-parsing. Dit leidt tot een authenticatiefout in plaats van een syntaxfout, wat u een avond aan uitzoekwerk kan kosten. De .gitignore-regel moet worden toegevoegd voordat de eerste commit plaatsvindt: het compose-bestand is veilig om te publiceren en te versioneren, het .env-bestand is dat nooit, en een geheim dat in de git-geschiedenis is beland, is een geheim dat u moet roteren. Als u de stack start terwijl een variabele ontbreekt, waarschuwt Compose luid en gaat verder met een lege string, wat voor een Postgres-wachtwoord een defecte implementatie betekent:

WARN[0000] The "POSTGRES_PASSWORD" variable is not set. Defaulting to a blank string.

docker compose config toont het volledig geïnterpoleerde bestand; dit is de snelste manier om te controleren wat de containers daadwerkelijk ontvangen. Vergeet niet dat de uitvoer uw geheimen bevat.

depends_on wacht op niets, tenzij u een healthcheck toevoegt

Een kale depends_on: [db] regelt alleen de opstartvolgorde: Compose start Postgres eerst en de app een moment later, terwijl Postgres nog seconden verwijderd is van het accepteren van verbindingen. De app benadert de database, faalt en crasht of probeert het opnieuw, afhankelijk van hoe goed deze is geschreven.

De betrouwbare versie is wat het bestand hierboven gebruikt: de db-service definieert een healthcheck (Postgres levert pg_isready precies hiervoor), en de app declareert depends_on met condition: service_healthy. Compose start de database, controleert elke 10 seconden de status en start Miniflux pas zodra de check slaagt. Als de database nooit "healthy" wordt, bijvoorbeeld door een fout wachtwoord of corrupt volume, start de app nooit en vertelt Compose u welke afhankelijkheid faalde:

dependency failed to start: container miniflux-db-1 is unhealthy

Dat bericht wijst u naar docker compose logs db, waar de werkelijke fout zich bevindt.

restart: unless-stopped

restart: unless-stopped op beide services betekent dat de containers terugkeren na een crash en na een VPS-herstart, maar uitgeschakeld blijven als u bewust docker compose stop heeft uitgevoerd. Het alternatief always brengt containers zelfs na een handmatige stop weer tot leven, wat zelden de bedoeling is. Zonder restart-beleid zorgt een kernel-update herstart om 4 uur 's ochtends ervoor dat uw services stilletjes offline gaan totdat u het opmerkt.

De dagelijkse commando's

Alles wat dagelijks nodig is, bestaat uit vijf commando's die u uitvoert vanuit de projectmap.

docker compose up -d        # create and start; idempotent, recreates only what changed
docker compose ps           # status, ports, and health of this project's containers
docker compose logs -f miniflux   # follow one service's logs; --tail 100 for recent history
docker compose pull && docker compose up -d   # upgrade to the pinned tags
docker compose down         # stop and remove containers and the network

up -d is veilig om herhaaldelijk uit te voeren; het vergelijkt het bestand met de huidige status en past alleen services aan waarvan de configuratie of image is gewijzigd. Het upgrade-paar haalt de versies op waar uw vastgezette tags naar verwijzen: patch-releases onder postgres:16-alpine, en niets voor een exacte pin totdat u deze zelf aanpast, wat precies de bedoeling is. Oude images hopen zich op na upgrades; maak schijfruimte vrij met docker image prune -f.

Nu het destructieve commando, met nadruk: docker compose down is veilig, containers en het netwerk zijn vervangbaar, en uw data staat in de volume. docker compose down -v verwijdert ook de benoemde volumes. Dat is uw database, direct verwijderd, zonder bevestigingsvraag en zonder mogelijkheid tot herstel. De -v-vlag bestaat voor het afbreken van experimenten; behandel deze bij een stack met echte data op dezelfde manier als u rm -rf behandelt. Er is geen prullenbak onder /var/lib/docker/volumes/.

Voor een eenmalige shell in een draaiende container: docker compose exec db psql -U miniflux brengt u in de database, en docker compose exec miniflux sh geeft u een shell in de applicatie.

Waar uw data daadwerkelijk staat

Named volumes krijgen het projectvoorvoegsel, dus db-data in een map genaamd miniflux wordt miniflux_db-data:

docker volume ls
docker volume inspect miniflux_db-data

De inspect-output bevat de regel die ertoe doet:

"Mountpoint": "/var/lib/docker/volumes/miniflux_db-data/_data"

Die map is de database, eigendom van root, op het bestandssysteem van de host, en deze blijft behouden na down, upgrades en het opnieuw opbouwen van containers. Dit is tevens precies wat uw back-ups moeten bevatten.

Back-up maken van een named volume

Het standaardpatroon is een tijdelijke container die het volume read-only koppelt naast een host-directory, en vervolgens alles archiveert met tar:

docker run --rm \
  -v miniflux_db-data:/data:ro \
  -v "$PWD":/backup \
  alpine:3.22 tar czf /backup/miniflux-db-$(date +%F).tar.gz -C /data .

Geen installatie vereist, niets blijft draaien, en het herstel is het spiegelbeeld: tar xzf naar een nieuw, leeg volume met dezelfde mounts in omgekeerde volgorde.

Eén waarschuwing voor databases: het archiveren van een actieve Postgres data-directory kan een status tijdens het schrijven vastleggen die niet correct opstart. Stop de container docker compose stop voor de seconden die de tar-actie in beslag neemt, of, beter nog, maak een logische dump, die per definitie consistent is:

docker compose exec -T db pg_dump -U miniflux miniflux | gzip > miniflux-$(date +%F).sql.gz

De -T schakelt de pseudo-terminal uit die Compose standaard toewijst; het doorsturen van dump-output via een TTY kan deze beschadigen. Plaats een van deze commando's in cron en kopieer het resultaat van de VPS; een back-up op dezelfde schijf als de data die het beschermt is een kopie, geen back-up. De Nextcloud-handleiding bouwt een volledig geplande routine rondom precies deze twee patronen.

Foutmodi en de bijbehorende meldingen

permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock, u zit nog niet in de docker-groep, of u zit er wel in maar de sessie is van daarvoor. id toont uw effectieve groepen; newgrp docker herstelt de huidige shell, uitloggen en opnieuw inloggen herstelt ze allemaal.

Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?, een ander probleem: de daemon zelf is niet actief. sudo systemctl status docker en sudo journalctl -u docker -n 50 geven de reden aan. Op een VPS is een volle schijf de klassieke oorzaak, controleer dit met df -h /var/lib/docker.

Bind for 127.0.0.1:8080 failed: port is already allocated, een andere container heeft die hostpoort al gepubliceerd. docker ps laat zien welke; een verouderde container van een experiment docker run weken geleden is meestal de boosdoener. Als docker ps leeg is, houdt een niet-Docker-proces de poort bezet: sudo ss -tlnp | grep 8080 benoemt dit proces.

yaml: line 14: did not find expected key, een inspringfout op of net boven de genoemde regel. Compose-bestanden zijn YAML: twee spaties inspringing, uitsluitend spaties, en een tab-karakter op welke plek dan ook is fataal. docker compose config valideert het bestand zonder iets op te starten; het uitvoeren hiervan na elke wijziging is een goede gewoonte.

De ufw-verrassing geeft helemaal geen foutmelding, wat het gevaarlijk maakt: de deploy slaagt, ufw status ziet er goed uit, en een poortscan van buitenaf vindt uw database alsnog. Lees de sectie over poorten hierboven opnieuw, controleer elk ports:-item op een ontbrekend 127.0.0.1:-voorvoegsel, en bevestig vanaf een andere machine met curl http://your-vps-ip:8080 dat connection refused het antwoord is dat u verwacht.

Vanaf hier verandert de Traefik-handleiding deze enkele stack in vele applicaties achter één HTTPS-toegangspunt, en wat de moeite waard is om zelf te hosten in 2026 is de boodschappenlijst om doorheen te lopen. Zodra er verschillende van die stacks draaien en elk zijn eigen inlogformulier heeft bedacht, brengt een zelfgehoste SSO-server zoals Authentik ze weer samen tot één account achter diezelfde proxy.

Een gameserver, zoals een Minecraft-server op een VPS, is een geschikt eerste Compose-project om mee te oefenen. Als u liever leert met iets dat u dagelijks opent, is openGym, een zelfgehoste trainingstracker, een kleine stack die aan een git-tag in plaats van aan een imagetag is vastgezet. Plaats daarvoor TLS vóór de applicatie voordat u de eerste passkey registreert. Foto's zijn meestal het eerste dat mensen uit de cloud van een andere partij willen terughalen. Een vergelijking van PhotoPrism en Immich maakt duidelijk hoeveel RAM u minimaal nodig hebt en welke back-uproutine u aangaat voordat u een volume aan een van beide toewijst. Wanneer twee services niet meer voldoende zijn, volgt u met AFFiNE opzetten als een Notion-achtige werkruimte dezelfde patronen met vier containers. Het is een goede test om te bepalen of de vastgezette tags, healthchecks en named volumes hierboven inmiddels een vaste werkwijze zijn geworden.

FAQ

Waarom krijg ik de melding "permission denied while trying to connect to the Docker daemon socket"?

Uw gebruiker zit niet in de docker-groep, of is pas toegevoegd nadat de huidige sessie was gestart; groepslidmaatschap wordt pas bij het inloggen actief. Voer sudo usermod -aG docker $USER uit, gevolgd door newgrp docker, of log uit en weer in, en controleer het resultaat met id. De groep verleent root-equivalente toegang tot de host, dus voeg alleen gebruikers toe aan wie u ook sudo-rechten zou verlenen.

Verwijdert docker compose down mijn data?

Een standaard docker compose down doet dit niet; het verwijdert alleen containers en het projectnetwerk. Benoemde volumes blijven behouden en worden bij de volgende up -d opnieuw gekoppeld. docker compose down -v is de destructieve variant: deze verwijdert ook de benoemde volumes, inclusief uw database, zonder bevestiging en zonder mogelijkheid tot herstel. Voer nooit -v uit op een stack met actieve data, tenzij u beschikt over een geverifieerde back-up.

Wat is het verschil tussen docker-compose en docker compose?

docker-compose (met koppelteken) is Compose v1, een zelfstandig Python-binary dat in 2023 het einde van zijn levenscyclus heeft bereikt en niet meer op nieuwe servers moet worden geïnstalleerd. docker compose (met spatie) is Compose v2, een Go-plugin voor de Docker CLI, die als docker-compose-plugin wordt geïnstalleerd vanuit de apt-repository van Docker. Commando's en YAML-bestanden zijn vrijwel volledig compatibel; als een oude handleiding docker-compose up vermeldt, typ dan docker compose up.

Waarom is mijn Docker-container bereikbaar vanaf internet, terwijl ufw de poort blokkeert?

Omdat Docker poorten publiceert met DNAT-regels in de PREROUTING-chain van iptables, en de herschreven pakketten het FORWARD-pad door de eigen chains van Docker volgen, bereiken ze nooit de INPUT-chain waar de regels van ufw van toepassing zijn. ufw deny 8080 heeft daarom geen effect op een gepubliceerde containerpoort. Los dit bij de bron op: publiceer naar 127.0.0.1: en stel services beschikbaar via een reverse proxy.

Moet ik een benoemd volume of een bind mount gebruiken?

Gebruik benoemde volumes voor data waar alleen de container mee werkt, in het bijzonder databases, omdat Docker de eigendomsrechten instelt die de image verwacht en de permissies direct correct zijn. Gebruik bind mounts voor bestanden die u ook vanaf de host beheert: configuraties die u bewerkt, media die u uploadt, of alles waarvan u de locatie op de host duidelijk wilt hebben. Als een container bij het opstarten faalt met permission denied op een bind mount, is een mismatch tussen de UID van de host en de container het eerste wat u moet controleren.