← Terug naar tutorials

Stateful Containers goed aanpakken: dataverlies debuggen en volume-mount fouten voorkomen

stateful-containersdocker-volumeskubernetes-storagedata-persistencedebuggingvolume-mountsdata-lossdevops

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

  1. Mentale modellen: container filesystem vs volumes
  2. Typische oorzaken van dataverlies
  3. Inspecteren wat er écht gemount is
  4. De grootste valkuil: een lege host-map mount over containerdata
  5. Named volumes vs bind mounts: wanneer welke?
  6. Permissies, UID/GID en rootless: waarom “permission denied” ineens verschijnt
  7. Data veilig initialiseren: entrypoints, init-scripts en “copy-on-first-run”
  8. Backups, restore en forensische recovery
  9. Debug-playbook: stap-voor-stap dataverlies onderzoeken
  10. Kubernetes: PV/PVC, mount issues en CrashLoop door storage
  11. Hardening: best practices om fouten te voorkomen

Mentale modellen: container filesystem vs volumes

Een container heeft een eigen filesystemlaag (meestal overlayfs). Die laag is:

Een volume daarentegen is opslag die buiten de container lifecycle bestaat. In Docker zijn er grofweg twee vormen:

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:

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?

  1. Check of je bind mount leeg is:
ls -lah ./pgdata
  1. 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.

  1. 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:

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

Nadelen

Bind mounts (goed voor config, code, logs)

Voordelen

Nadelen

Vuistregel


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:

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 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:

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}}'

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:

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:

Mount debug in Kubernetes

  1. Check Pod events:
kubectl describe pod mypod

Let op meldingen als:

  1. Check PVC status:
kubectl get pvc
kubectl describe pvc myclaim
  1. 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:

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

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

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”

  1. Is de app wel verbonden met de juiste host/port?
  2. Is de DB container opnieuw aangemaakt met een ander volume?
  3. Staat POSTGRES_DB correct?

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)

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.