Stateful Containers goed aanpakken: dataverlies debuggen en volume-mount fouten voorkomen
Stateful containers zijn verraderlijk: je app draait “prima”, tot je een container herstart, een image update, of een deployment doet—en ineens is data weg, zijn permissies kapot, of mount je per ongeluk een lege directory over je bestaande data. In deze tutorial leer je systematisch dataverlies debuggen en structureel volume-mount fouten voorkomen, met echte commando’s en diepgaande uitleg. Voorbeelden gebruiken vooral Docker (en waar relevant ook Kubernetes), omdat de concepten hetzelfde zijn: containers zijn vluchtig, volumes zijn je waarheid.
Inhoud
- Mentale modellen: container filesystem vs volumes
- Typische oorzaken van dataverlies
- Inspecteren wat er écht gemount is
- De grootste valkuil: een lege host-map mount over containerdata
- Named volumes vs bind mounts: wanneer welke?
- Permissies, UID/GID en rootless: waarom “permission denied” ineens verschijnt
- Data veilig initialiseren: entrypoints, init-scripts en “copy-on-first-run”
- Backups, restore en forensische recovery
- Debug-playbook: stap-voor-stap dataverlies onderzoeken
- Kubernetes: PV/PVC, mount issues en CrashLoop door storage
- Hardening: best practices om fouten te voorkomen
Mentale modellen: container filesystem vs volumes
Een container heeft een eigen filesystemlaag (meestal overlayfs). Die laag is:
- Ephemeral: bij
docker rmben je het kwijt. - Image-afhankelijk: een nieuwe image betekent vaak een nieuwe basislaag.
- Niet bedoeld als database: je kunt er wel schrijven, maar het is geen duurzame opslag.
Een volume daarentegen is opslag die buiten de container lifecycle bestaat. In Docker zijn er grofweg twee vormen:
- Named volume: beheerd door Docker, locatie onder
/var/lib/docker/volumes/.... - Bind mount: jij wijst een pad op de host aan, bv.
-v /srv/data:/var/lib/postgresql/data.
Belangrijk: een mount “vervangt” de directory in de container. Als je /var/lib/postgresql/data mount, zie je niet langer wat in de image op dat pad zat; je ziet de inhoud van het volume/bind mount.
Typische oorzaken van dataverlies
1) Data staat per ongeluk in de containerlaag
Je app schrijft naar /app/data, maar je mount alleen /app/config. Dan is /app/data weg na herstart/replace.
Controleer waar je app schrijft:
docker exec -it myapp sh -lc 'ls -lah /app && ls -lah /app/data || true'
2) Mount naar het verkeerde pad
Je denkt dat je /var/lib/mysql mount, maar MySQL gebruikt /var/lib/mysql én /var/run/mysqld voor sockets. Of je mount /data maar de app gebruikt /var/data.
3) Lege host-map bind mount overschrijft bestaande containerdata
Dit is de klassieker: je mount ./data:/var/lib/postgresql/data, maar ./data bestaat nog niet of is leeg. Resultaat: de directory in de container lijkt leeg, en init scripts maken een nieuwe database.
4) Permissies/ownership mismatch (UID/GID)
Container draait als user 1001, maar je bind mount is owned door root:root met 700. Dan faalt schrijven, applicatie start opnieuw, en je krijgt “reset” gedrag of corruptie.
5) Verkeerde “init” logica
Sommige images initialiseren data als de target directory leeg is. Als jij per ongeluk een leeg volume mount, triggert dat init en overschrijft het.
6) Kubernetes: PVC opnieuw aangemaakt / StorageClass dynamisch
PVC verwijderd → PV kan gerecycled of verwijderd worden afhankelijk van reclaimPolicy. Data kan dan echt weg zijn.
Inspecteren wat er écht gemount is
Docker: mounts bekijken
docker inspect mycontainer --format '{{json .Mounts}}' | jq
Je ziet per mount:
Type:volumeofbindSource: hostpad of volume padDestination: pad in containerRW: read-write?
Ook handig:
docker container inspect mycontainer --format '{{range .Mounts}}{{println .Type .Source "->" .Destination "rw=" .RW}}{{end}}'
In de container: mountinfo
docker exec -it mycontainer sh -lc 'cat /proc/self/mountinfo | sed -n "1,20p"'
Zo zie je of /var/lib/... echt een mount is, en welk filesystem.
Docker volumes opsommen en lokaliseren
docker volume ls
docker volume inspect myvolume
De Mountpoint is waar Docker het volume op de host bewaart.
De grootste valkuil: een lege host-map mount over containerdata
Scenario
Je draait Postgres:
docker run -d --name pg \
-e POSTGRES_PASSWORD=secret \
-v ./pgdata:/var/lib/postgresql/data \
postgres:16
Als ./pgdata nog niet bestaat, maakt Docker die map aan (als bind mount) en mount die leeg. Postgres ziet een lege datadir en initialiseert een nieuwe cluster. Als je eerder data in de containerlaag had (of in een ander volume), lijkt die “weg”.
Hoe detecteer je dit?
- Check of je bind mount leeg is:
ls -lah ./pgdata
- Check of Postgres net ge-initialiseerd heeft (logs):
docker logs pg --tail=200
Je ziet vaak regels als “initdb: …” of “database system is ready to accept connections” na init.
- Check mount type:
docker inspect pg --format '{{range .Mounts}}{{println .Type .Source "->" .Destination}}{{end}}'
Hoe voorkom je dit?
Gebruik named volumes voor databases, tenzij je een sterke reden hebt voor bind mounts.
docker volume create pgdata
docker run -d --name pg \
-e POSTGRES_PASSWORD=secret \
-v pgdata:/var/lib/postgresql/data \
postgres:16
Named volumes hebben een belangrijk gedrag: als het volume nog leeg is en de image bevat data in de mount directory, dan kan Docker bij eerste mount een copy-up doen (afhankelijk van image/driver). Bij officiële DB-images is de init meestal via entrypoint, maar named volumes zijn in de praktijk minder foutgevoelig dan “per ongeluk lege host directory”.
Als je toch bind mounts gebruikt:
- Maak de directory expliciet aan
- Zet ownership en permissies correct
- Overweeg een “guard” script dat weigert te starten als de directory leeg is terwijl je data verwacht
Voorbeeld guard:
test -d ./pgdata || { echo "pgdata ontbreekt"; exit 1; }
test "$(ls -A ./pgdata 2>/dev/null | wc -l)" -gt 0 || echo "WAARSCHUWING: pgdata is leeg"
Named volumes vs bind mounts: wanneer welke?
Named volumes (aanrader voor state)
Voordelen
- Minder kans op path-fouten
- Docker beheert locatie en permissions vaak consistenter
- Makkelijk te backuppen via tijdelijke container
- Werkt goed met Docker Desktop (macOS/Windows) zonder performance-drama van bind mounts
Nadelen
- Minder “zichtbaar” in je projectdirectory
- Je moet expliciet backup/restore regelen
Bind mounts (goed voor config, code, logs)
Voordelen
- Direct zichtbaar in filesystem
- Handig voor development (hot reload)
- Je kunt tools op de host gebruiken om data te inspecteren
Nadelen
- Permissions/SELinux issues
- Kans op “lege map overschrijft data”
- Op macOS/Windows vaak trager (filesystem sync)
Vuistregel
- Database / queue / object store data: named volume of managed storage (K8s PV)
- Configuratiebestanden: bind mount read-only
- Applicatiecode in dev: bind mount
- Logs: liever stdout/stderr; anders volume
Permissies, UID/GID en rootless: waarom “permission denied” ineens verschijnt
Veel images draaien niet als root. Bijvoorbeeld postgres draait als user postgres (UID vaak 999). Als je bind mount op de host owned is door jouw user of root, kan dat misgaan.
Check welke user de container gebruikt
docker exec -it pg id
docker exec -it pg sh -lc 'whoami && id && ls -ld /var/lib/postgresql/data'
Fix ownership op host (bind mount)
Stel Postgres draait als UID 999:
sudo chown -R 999:999 ./pgdata
sudo chmod -R u+rwX,go-rwx ./pgdata
Rootless Docker
Bij rootless Docker worden UID/GID mappings gebruikt. Dan kan “chown naar 999” op de host niet hetzelfde betekenen. In dat geval:
- Gebruik liever named volumes
- Of zorg dat de container user overeenkomt met jouw host user (
--user $(id -u):$(id -g)), als de image dat ondersteunt
Voorbeeld:
docker run --rm -it --user "$(id -u):$(id -g)" alpine:3.20 id
SELinux (Fedora/RHEL/CentOS)
Bind mounts kunnen geblokkeerd worden door SELinux labels. Gebruik :Z of :z:
docker run -d --name pg \
-e POSTGRES_PASSWORD=secret \
-v ./pgdata:/var/lib/postgresql/data:Z \
postgres:16
Z = private label, z = shared label.
Data veilig initialiseren: entrypoints, init-scripts en “copy-on-first-run”
Veel stateful images hebben init-mechanismen:
- Postgres: voert init uit als datadir leeg is
- MySQL/MariaDB: idem
- Redis: schrijft snapshot/AOF afhankelijk van config
- Elasticsearch: data path en lockfiles
Postgres init scripts
Bij officiële Postgres image kun je scripts plaatsen in /docker-entrypoint-initdb.d/. Die draaien alleen bij eerste init (lege datadir).
Voorbeeld:
mkdir -p initdb
cat > initdb/001-create.sql <<'SQL'
CREATE DATABASE appdb;
SQL
docker volume create pgdata
docker run -d --name pg \
-e POSTGRES_PASSWORD=secret \
-v pgdata:/var/lib/postgresql/data \
-v "$PWD/initdb:/docker-entrypoint-initdb.d:ro" \
postgres:16
Belangrijk: als je per ongeluk een leeg volume mount, draaien init scripts opnieuw. Daarom is “volume per omgeving” en “nooit zomaar volume wisselen” cruciaal.
Copy-on-first-run patroon (voor eigen apps)
Als je eigen container een default dataset/config heeft in de image (/usr/share/app/default-data) en je wilt die naar een volume kopiëren als het volume leeg is:
#!/bin/sh
set -eu
DATA_DIR=/data
DEFAULT_DIR=/usr/share/app/default-data
if [ -z "$(ls -A "$DATA_DIR" 2>/dev/null || true)" ]; then
echo "Data directory leeg; initialiseer..."
cp -a "$DEFAULT_DIR"/. "$DATA_DIR"/
else
echo "Data directory niet leeg; sla init over."
fi
exec /usr/local/bin/app
Waarom dit helpt: je voorkomt dat een lege mount leidt tot “app start met lege state” zonder dat je het merkt.
Backups, restore en forensische recovery
Backup van een named volume (tar via tijdelijke container)
docker run --rm \
-v pgdata:/volume:ro \
-v "$PWD/backups:/backup" \
alpine:3.20 sh -lc \
'cd /volume && tar -czf /backup/pgdata-$(date +%F).tar.gz .'
Restore naar een volume
Let op: restore overschrijft bestaande data. Stop eerst de consumer container.
docker stop pg
docker run --rm \
-v pgdata:/volume \
-v "$PWD/backups:/backup" \
alpine:3.20 sh -lc \
'rm -rf /volume/* && tar -xzf /backup/pgdata-2026-04-12.tar.gz -C /volume'
docker start pg
Forensisch: “data is weg” maar misschien bestaat het nog
Mogelijkheden:
- Je gebruikt een ander volume dan je dacht
- Je mount nu een lege bind mount over de oude data
- De oude containerlaag bestaat nog (als container niet verwijderd is)
Check:
docker ps -a
docker inspect pg --format '{{.Id}}'
docker container diff pg | sed -n '1,120p'
Als je container verwijderd is, is de containerlaag meestal weg. Daarom: volumes en backups.
Debug-playbook: stap-voor-stap dataverlies onderzoeken
Gebruik dit als checklist wanneer data “verdwenen” lijkt.
Stap 1: Welke container en welke image draait?
docker ps --no-trunc
docker inspect mycontainer --format 'Name={{.Name}} Image={{.Config.Image}} Created={{.Created}}'
Stap 2: Welke mounts zijn actief?
docker inspect mycontainer --format '{{range .Mounts}}{{println .Type .Source "->" .Destination}}{{end}}'
- Zie je
bindnaar een onverwacht pad? - Zie je een nieuw named volume met random naam?
Stap 3: Bestaat het volume nog?
docker volume ls
docker volume inspect pgdata
Stap 4: Inspecteer volume-inhoud zonder de app
docker run --rm -it -v pgdata:/v alpine:3.20 sh -lc 'ls -lah /v | sed -n "1,200p"'
Als het volume leeg is, is het echt leeg—of je kijkt naar het verkeerde volume.
Stap 5: Check logs op re-init of migraties
docker logs mycontainer --since=24h | sed -n '1,200p'
Zoek naar:
- “init”
- “migrat”
- “creating database”
- “permission denied”
- “could not open file”
- “FATAL”
Stap 6: Check ownership/perms in volume
docker run --rm -it -v pgdata:/v alpine:3.20 sh -lc 'stat -c "%u:%g %a %n" /v && ls -ldn /v'
Stap 7: Controleer of je per ongeluk een andere omgeving gebruikt
Bijvoorbeeld: compose project name verandert → volume namen veranderen.
Bekijk compose volumes:
docker compose ls
docker compose -p myproj ps
docker volume ls | grep myproj
Kubernetes: PV/PVC, mount issues en CrashLoop door storage
In Kubernetes is het principe hetzelfde, maar de objecten zijn anders:
- PVC (PersistentVolumeClaim): “ik wil opslag”
- PV (PersistentVolume): “hier is opslag”
- StorageClass: dynamische provisioning regels
- Reclaim policy: wat gebeurt er met data als claim weg is (
DeleteofRetain)
Mount debug in Kubernetes
- Check Pod events:
kubectl describe pod mypod
Let op meldingen als:
MountVolume.SetUp failedpermission deniedfailed to provision volume
- Check PVC status:
kubectl get pvc
kubectl describe pvc myclaim
- Check PV en reclaim policy:
kubectl get pv
kubectl describe pv <pv-name>
Als Reclaim Policy: Delete en je verwijdert de PVC, kan de onderliggende disk verwijderd worden.
“Data weg” door nieuwe PVC
Als je per ongeluk een andere claimnaam gebruikt (bijv. in een nieuwe release), krijg je een nieuwe lege volume. Symptoom: app start “fresh”.
Check welke PVC gemount is:
kubectl get pod mypod -o jsonpath='{.spec.volumes[*].persistentVolumeClaim.claimName}{"\n"}'
Permissions in K8s: fsGroup en runAsUser
Veel storage drivers mounten met root ownership. Als je container als non-root draait, kan dat falen. Dan gebruik je securityContext met fsGroup en runAsUser (conceptueel; de exacte manifest-invulling hangt van je setup af). Debug vanuit de container:
kubectl exec -it mypod -- sh -lc 'id && ls -ld /data && touch /data/testfile'
Als touch faalt: permissions of read-only mount.
Hardening: best practices om fouten te voorkomen
1) Leg paden vast en documenteer ze
Schrijf in je README:
- Welke directories state bevatten
- Welke mounts verplicht zijn
- Welke UID/GID verwacht wordt
2) Gebruik read-only mounts voor config
Voorkomt dat je app config “terugschrijft” op onverwachte plekken.
Docker voorbeeld:
docker run -d --name myapp \
-v "$PWD/config:/app/config:ro" \
-v myapp-data:/app/data \
myimage:1.2.3
3) Gebruik named volumes voor databases
En geef ze expliciete namen, niet auto-generated.
docker volume create myapp_pgdata
4) Zet guards in je entrypoint
- Weiger te starten als data dir leeg is terwijl je een “production” flag hebt
- Log duidelijk welke directory gebruikt wordt
Voorbeeld check:
if [ "${REQUIRE_EXISTING_DATA:-0}" = "1" ] && [ -z "$(ls -A /data 2>/dev/null || true)" ]; then
echo "FOUT: /data is leeg maar REQUIRE_EXISTING_DATA=1"
exit 2
fi
5) Maak backups onderdeel van je routine
- Dagelijks tar van volumes
- Of database-native backups (pg_dump, mysqldump, snapshots)
Postgres logical backup:
docker exec -t pg pg_dumpall -U postgres > backups/pg_dumpall_$(date +%F).sql
Restore (voorbeeld):
cat backups/pg_dumpall_2026-04-12.sql | docker exec -i pg psql -U postgres
6) Test restore, niet alleen backup
Een backup die je niet getest hebt, is een gevoel.
7) Let op compose project names
Compose prefix kan volumes “veranderen”. Forceer een projectnaam:
docker compose -p myapp up -d
En controleer volumes:
docker volume ls | grep myapp
8) Monitor disk space en inode gebruik
“Dataverlies” is soms “writes falen” door volle disk.
Host checks:
df -h
df -i
docker system df
Praktisch voorbeeld: Postgres + applicatie, correct gemount en debugbaar
Setup met named volume
docker volume create app_pgdata
docker run -d --name app-pg \
-e POSTGRES_PASSWORD=secret \
-e POSTGRES_DB=appdb \
-v app_pgdata:/var/lib/postgresql/data \
postgres:16
Check:
docker exec -it app-pg sh -lc 'ps aux | grep postgres | head'
docker exec -it app-pg sh -lc 'ls -lah /var/lib/postgresql/data | sed -n "1,50p"'
Backup:
mkdir -p backups
docker run --rm \
-v app_pgdata:/volume:ro \
-v "$PWD/backups:/backup" \
alpine:3.20 sh -lc \
'cd /volume && tar -czf /backup/app_pgdata-$(date +%F).tar.gz .'
Debug: “app ziet lege DB”
- Is de app wel verbonden met de juiste host/port?
- Is de DB container opnieuw aangemaakt met een ander volume?
- Staat
POSTGRES_DBcorrect?
Bekijk mounts en env:
docker inspect app-pg --format '{{range .Mounts}}{{println .Name .Source "->" .Destination}}{{end}}'
docker inspect app-pg --format '{{range $k,$v := .Config.Env}}{{println $v}}{{end}}' | sort
Samenvatting (wat je moet onthouden)
- Container filesystem is tijdelijk; alles wat je wilt behouden moet in een volume.
- De meest voorkomende fout is een lege bind mount die je bestaande data “verbergt” en init triggert.
- Debug mount-problemen door inspect (Docker/K8s), logs, en het volume los te bekijken met een tijdelijke container.
- Voorkom ellende met named volumes voor state, read-only config mounts, UID/GID discipline, en geteste backups.
Als je wilt, kun je je concrete docker run of docker compose config (zonder secrets) delen; dan kan ik exact aanwijzen waar dataverlies of mount-overschrijvingen kunnen optreden en hoe je het robuust maakt.