← Terug naar tutorials

Permission Denied in Docker Volumes oplossen op Linux en macOS

dockervolumeslinuxmacosbestandsrechtenuid gidbind mountsselinuxdevopstroubleshooting

Permission Denied in Docker Volumes oplossen op Linux en macOS

Docker-volumes zijn een van de meest gebruikte mechanismen om data persistent te maken en om broncode of configuratiebestanden te delen tussen je host en containers. Tegelijkertijd zijn “Permission denied”-fouten veruit de meest voorkomende frustratie bij bind mounts en volumes, vooral wanneer je werkt met Linux-permissies, UID/GID-mapping, SELinux, of macOS-bestandsdeling via Docker Desktop.

Deze tutorial legt diepgaand uit waarom permissieproblemen ontstaan en geeft praktische, echte commando’s om ze op te lossen. We behandelen zowel Linux (met aandacht voor SELinux en rootless Docker) als macOS (Docker Desktop, bestandsdeling en performance/permissie-implicaties).


Inhoud


1. Begrippen: volume vs bind mount

Bind mount: je mount een pad van je host direct in de container.

docker run --rm -v "$PWD:/app" alpine ls -la /app

Named volume: Docker beheert de opslaglocatie (meestal onder /var/lib/docker/volumes/... op Linux). Je verwijst naar de volumenaam, niet naar een hostpad.

docker volume create mydata
docker run --rm -v mydata:/data alpine sh -c "echo test > /data/hello && cat /data/hello"

Waarom relevant voor permissies?


2. Symptomen herkennen: typische foutmeldingen

Veelvoorkomende errors:

Voorbeeld:

docker run --rm -v "$PWD:/app" node:20-alpine sh -c "cd /app && npm ci"

Als je projectmap op de host niet schrijfbaar is voor de user in de container, kan dit falen met EACCES.


3. De kern: UID/GID en eigenaarschap

Linux-permissies zijn gebaseerd op:

Een container heeft zijn eigen /etc/passwd, maar de kernel kijkt bij bestandstoegang naar numerieke UID/GID. Dus:

Belangrijk: root in een container is niet altijd almachtig, zeker niet bij:


4. Snelle diagnose-checklist (Linux en macOS)

  1. Welke user draait in de container?
docker run --rm alpine id

Of in een draaiende container:

docker exec -it <container> id
  1. Wat zijn de rechten van het gemounte pad in de container?
docker exec -it <container> sh -c "ls -la /pad && stat -c '%u %g %a %n' /pad"
  1. Wat zijn de rechten op de host?

Linux:

ls -la .
stat -c '%u %g %a %n' .

macOS:

ls -la .
stat -f '%u %g %Lp %N' .
  1. Gebruik je SELinux? (Linux)
getenforce 2>/dev/null || echo "SELinux tools niet aanwezig"
  1. Is dit een bind mount of named volume?
docker inspect <container> --format '{{json .Mounts}}' | jq

5. Oplossingen op Linux

5.1 Check welke user in de container draait

Veel officiële images draaien niet als root (bijv. node, nginx, sommige postgres setups). Check:

docker run --rm node:20-alpine id
docker run --rm nginx:alpine id

Je kunt ook de default user van een image inspecteren:

docker image inspect node:20-alpine --format '{{.Config.User}}'

Leeg betekent vaak “root” (UID 0), maar dat is niet gegarandeerd in alle varianten.


5.2 Eigenaarschap en rechten corrigeren (chown/chmod)

Snelste fix: maak de hostdirectory eigendom van jouw user (of van de UID die in de container gebruikt wordt).

Stel: je mount ./data naar /var/lib/postgresql/data en Postgres draait als UID 999 (voorbeeld). Dan:

sudo mkdir -p data
sudo chown -R 999:999 data
sudo chmod -R u+rwX,g+rwX data

Daarna:

docker run --rm -v "$PWD/data:/var/lib/postgresql/data" postgres:16

Let op: dit verandert eigenaarschap op je host. In teams kan dit onwenselijk zijn.

Alternatief: geef write-permissie via group of “others”, maar dat is vaak minder veilig:

chmod -R o+rwX data

Gebruik dit alleen als je begrijpt wat je openzet.


5.3 Werk met dezelfde UID/GID als de host

Voor development is het vaak het beste om processen in de container te draaien met jouw host-UID/GID, zodat bestanden die de container aanmaakt op de host “van jou” zijn.

Je host-UID/GID op Linux:

id -u
id -g

Run container met die user:

docker run --rm \
  -u "$(id -u):$(id -g)" \
  -v "$PWD:/app" \
  -w /app \
  alpine sh -c "id && touch testfile && ls -la testfile"

Als dit werkt, is de kern van je probleem opgelost: UID/GID matchen.

Valkuil: sommige images verwachten root om packages te installeren of directories aan te maken. Oplossing: maak directories vooraf aan of gebruik een entrypoint dat init als root doet en daarna dropt naar een user.


5.4 Dockerfile: user aanmaken met host-UID

Voor een eigen image kun je een user creëren die overeenkomt met de host-UID. Vaak doe je dit via build args:

Dockerfile (voorbeeld)

FROM alpine:3.20

ARG UID=1000
ARG GID=1000

RUN addgroup -g ${GID} appgroup \
 && adduser -D -u ${UID} -G appgroup appuser

WORKDIR /app
USER appuser

CMD ["sh", "-lc", "id && ls -la && sleep 3600"]

Build met jouw UID/GID:

docker build -t myapp \
  --build-arg UID="$(id -u)" \
  --build-arg GID="$(id -g)" \
  .

Run met bind mount:

docker run --rm -v "$PWD:/app" myapp

Dit voorkomt dat je hostbestanden “root-owned” worden.


5.5 Compose: user: en group_add:

In docker-compose.yml (of compose.yaml) kun je de user instellen:

id -u
id -g

Dan in Compose:

services:
  app:
    image: node:20-alpine
    user: "1000:1000"
    working_dir: /app
    volumes:
      - ./:/app
    command: sh -lc "id && npm ci && npm test"

Je kunt dit dynamisch maken met environment variables:

export UID="$(id -u)"
export GID="$(id -g)"
docker compose up

En in Compose:

services:
  app:
    image: node:20-alpine
    user: "${UID}:${GID}"
    volumes:
      - ./:/app

group_add is nuttig als je toegang nodig hebt tot een extra group (bijv. docker group of een shared group op de host):

services:
  app:
    image: alpine
    user: "${UID}:${GID}"
    group_add:
      - "998"

5.6 Named volumes vs bind mounts: waarom named volumes vaak “makkelijker” zijn

Bij databases is een named volume vaak de beste keuze:

docker volume create pgdata
docker run --rm \
  -e POSTGRES_PASSWORD=secret \
  -v pgdata:/var/lib/postgresql/data \
  postgres:16

Waarom dit vaak werkt zonder gedoe:

Wil je toch data exporteren? Gebruik docker cp of een tijdelijke container:

docker run --rm -v pgdata:/data -v "$PWD:/backup" alpine \
  sh -c "tar -czf /backup/pgdata.tgz -C /data ."

5.7 SELinux (Fedora/RHEL/CentOS): :Z en :z

Op SELinux-systemen kan een bind mount correct lijken qua UID/GID, maar toch falen door labels.

Check SELinux status:

getenforce

Als dit Enforcing is, en je mount een hostpad, gebruik dan vaak :Z of :z:

Voorbeeld:

docker run --rm \
  -v "$PWD/data:/data:Z" \
  alpine sh -c "touch /data/test && ls -la /data"

In Compose:

services:
  app:
    image: alpine
    volumes:
      - ./data:/data:Z

Als je zonder :Z werkt, kan je error krijgen zoals:

Je kunt ook labels bekijken:

ls -Z .
ls -Z data

5.8 Rootless Docker: subuid/subgid en id-mapping

Bij rootless Docker draait de Docker daemon en containers zonder root privileges. Dit is veiliger, maar permissies kunnen complexer worden door user namespaces.

Check of je rootless gebruikt:

docker info | grep -i rootless

Of:

ps -ef | grep dockerd

Rootless gebruikt vaak subuid/subgid ranges (bijv. in /etc/subuid en /etc/subgid). Daardoor kan UID 0 in de container gemapt zijn naar een niet-root UID op de host.

Bekijk ranges:

cat /etc/subuid
cat /etc/subgid

Gevolg: chown in de container kan “Operation not permitted” geven op bind mounts, omdat de mapping niet overeenkomt of omdat het host filesystem het niet toestaat.

Praktische aanpak:


5.9 NFS/SMB mounts op de host: extra valkuilen

Als je projectdirectory op Linux zelf al een network mount is (NFS, CIFS/SMB), kunnen chown en Unix-permissies anders werken.

Diagnose:

mount | grep -E 'nfs|cifs|smb'
df -T .

Bij CIFS kan chmod/chown genegeerd worden afhankelijk van mount options. Mogelijke oplossingen:


6. Oplossingen op macOS (Docker Desktop)

6.1 Begrijp de macOS-architectuur: VM en file sharing

Op macOS draait Docker meestal via Docker Desktop, waarbij containers in een Linux VM draaien. Jouw macOS-bestanden worden gedeeld met die VM via een file sharing mechanisme (gRPC FUSE, VirtioFS, of een vergelijkbare laag, afhankelijk van je Docker Desktop versie en instellingen).

Belangrijk gevolg:

Je kunt nog steeds Permission denied krijgen, zelfs als ls -la op macOS “goed” lijkt.


6.2 File Sharing instellingen controleren

Controleer in Docker Desktop:

Als het pad niet gedeeld is, zie je vaak errors bij mounten of “empty directory” gedrag, maar permissies kunnen ook vreemd uitpakken.

Test met een simpele container:

docker run --rm -v "$PWD:/mnt" alpine sh -c "ls -la /mnt && touch /mnt/.write-test && ls -la /mnt/.write-test"

Als touch faalt met Permission denied, dan is het een write-permission issue in de sharing laag of UID/GID mismatch.


6.3 UID/GID op macOS vs Linux container

Op macOS heeft je user vaak UID 501 en GID 20 (staff), maar dit kan verschillen.

Check:

id -u
id -g

In de container is de default user vaak root (0) of een app user (bijv. node = 1000). Als je bind mount gebruikt, kan het helpen om de container als jouw macOS UID/GID te draaien:

docker run --rm \
  -u "$(id -u):$(id -g)" \
  -v "$PWD:/app" \
  -w /app \
  alpine sh -c "id && touch mac-test && ls -la mac-test"

Dit werkt vaak goed voor development, maar niet altijd (afhankelijk van Docker Desktop’s mapping).


6.4 Praktische fixes: user mapping, chown, en alternatieven

Fix A: draai containerprocessen als jouw UID/GID

Voor development stacks (Node, Python, PHP) is dit vaak de meest stabiele oplossing:

docker run --rm \
  -u "$(id -u):$(id -g)" \
  -v "$PWD:/app" \
  -w /app \
  node:20-alpine sh -lc "npm ci"

In Compose:

services:
  app:
    image: node:20-alpine
    user: "${UID}:${GID}"
    volumes:
      - ./:/app
    working_dir: /app

En start met:

export UID="$(id -u)"
export GID="$(id -g)"
docker compose run --rm app sh -lc "id && npm ci"

Fix B: vermijd chown op bind mounts (zeker op macOS)

Veel entrypoints doen chown -R op startup. Op macOS shared mounts kan dit:

Oplossing: pas je image/entrypoint aan zodat:

Fix C: zet write-heavy directories in named volumes

Klassiek voorbeeld: node_modules in een bind mount op macOS is vaak traag en kan permissiegedoe geven. Gebruik:

Compose-voorbeeld:

services:
  app:
    image: node:20-alpine
    working_dir: /app
    volumes:
      - ./:/app
      - node_modules:/app/node_modules
    command: sh -lc "npm ci && npm test"

volumes:
  node_modules:

Dit voorkomt dat de container node_modules op je host hoeft te beheren.


6.5 Wanneer je beter een named volume gebruikt op macOS

Gebruik named volumes voor:

Voorbeeld Postgres op macOS:

docker volume create pgdata
docker run --rm -p 5432:5432 \
  -e POSTGRES_PASSWORD=secret \
  -v pgdata:/var/lib/postgresql/data \
  postgres:16

Als je toch een bind mount wil voor backups, mount dan een aparte map alleen voor export/import.


7. Debugging: zo inspecteer je mounts en permissies

Inspecteer mounts in een container

docker inspect <container> --format '{{range .Mounts}}{{println .Type .Source "->" .Destination}}{{end}}'

Check filesystem type en mount options (in container)

docker exec -it <container> sh -c "mount | head -n 50"

Check of je kunt schrijven

docker exec -it <container> sh -c "touch /pad/can-i-write && echo OK"

Check owner en mode bits

Linux in container:

docker exec -it <container> sh -c "stat -c '%u %g %a %n' /pad /pad/* 2>/dev/null | head"

macOS host:

stat -f '%u %g %Lp %N' .

Check ACLs (Linux)

getfacl -p data | sed -n '1,80p'

ACLs kunnen toegang blokkeren of juist verlenen, los van chmod.


8. Voorbeeldscenario’s (met echte commando’s)

8.1 Node.js schrijft naar node_modules en faalt

Symptoom: npm ci geeft EACCES: permission denied, mkdir '/app/node_modules'.

Diagnose:

docker run --rm -it -v "$PWD:/app" -w /app node:20-alpine sh -lc "id; ls -la; npm ci"

Als id toont dat je als node (UID 1000) draait, maar je projectmap is eigendom van een andere UID en niet group-writable, dan faalt het.

Oplossing 1 (UID/GID match):

docker run --rm -it \
  -u "$(id -u):$(id -g)" \
  -v "$PWD:/app" -w /app \
  node:20-alpine sh -lc "id; npm ci"

Oplossing 2 (node_modules in named volume):

docker volume create nm
docker run --rm -it \
  -v "$PWD:/app" -v nm:/app/node_modules \
  -w /app \
  node:20-alpine sh -lc "npm ci"

8.2 PostgreSQL data directory “permission denied”

Fout (typisch): Postgres start niet en klaagt over /var/lib/postgresql/data niet schrijfbaar.

Slechte aanpak (bind mount zonder juiste owner):

mkdir -p pgdata
docker run --rm -e POSTGRES_PASSWORD=secret -v "$PWD/pgdata:/var/lib/postgresql/data" postgres:16

Oplossing A (named volume):

docker volume create pgdata
docker run --rm -e POSTGRES_PASSWORD=secret -v pgdata:/var/lib/postgresql/data postgres:16

Oplossing B (Linux bind mount + chown naar juiste UID)

Achterhaal UID in image (vaak postgres user). Je kunt dit checken:

docker run --rm postgres:16 id

Stel dit geeft UID 999. Dan:

sudo chown -R 999:999 pgdata
sudo chmod -R u+rwX pgdata
docker run --rm -e POSTGRES_PASSWORD=secret -v "$PWD/pgdata:/var/lib/postgresql/data" postgres:16

SELinux variant (Fedora/RHEL):

docker run --rm -e POSTGRES_PASSWORD=secret -v "$PWD/pgdata:/var/lib/postgresql/data:Z" postgres:16

8.3 Nginx kan geen logs schrijven

Stel je mount een logdirectory:

mkdir -p logs
docker run --rm -p 8080:80 -v "$PWD/logs:/var/log/nginx" nginx:alpine

Nginx draait vaak als nginx user. Als /var/log/nginx niet schrijfbaar is, krijg je errors.

Diagnose:

docker run --rm nginx:alpine id
ls -la logs

Fix (Linux): maak logs eigendom van de nginx UID/GID, of draai Nginx als jouw user (voor development).

Eigenaarschap aanpassen:

# voorbeeld: stel nginx user is 101
sudo chown -R 101:101 logs
sudo chmod -R u+rwX logs

Of run als root (niet ideaal voor productie):

docker run --rm -u 0:0 -p 8080:80 -v "$PWD/logs:/var/log/nginx" nginx:alpine

Beter: in productie schrijf je logs naar stdout/stderr en laat je Docker logging doen, i.p.v. naar een bind mount.


9. Best practices om het structureel te voorkomen

  1. Gebruik named volumes voor stateful data (databases, queues, caches).
  2. Gebruik bind mounts vooral voor broncode (development).
  3. Laat containerprocessen draaien met host UID/GID bij development:
    • Linux: bijna altijd ideaal
    • macOS: vaak goed, maar combineer met named volumes voor heavy directories
  4. Vermijd recursieve chown -R op bind mounts (traag, foutgevoelig, soms onmogelijk).
  5. Check SELinux op Linux en gebruik :Z/:z waar nodig.
  6. Maak permissies expliciet:
    • Maak directories aan op de host met juiste owner/mode
    • Gebruik umask bewust
  7. Documenteer UID/GID verwachtingen in je project:
    • .env met UID/GID
    • Compose die user: "${UID}:${GID}" gebruikt
  8. Debug systematisch:
    • id in container
    • stat op host en in container
    • docker inspect mounts
    • getenforce (SELinux)

Extra: compacte “recepten” (copy/paste)

Recept 1: snel testen of UID/GID mismatch het probleem is

docker run --rm -it -u "$(id -u):$(id -g)" -v "$PWD:/app" -w /app alpine sh -lc "id; touch .permtest; ls -la .permtest"

Recept 2: SELinux bind mount fix

docker run --rm -v "$PWD/data:/data:Z" alpine sh -lc "touch /data/x"

Recept 3: code bind mount + dependencies in named volume (macOS-friendly)

docker volume create node_modules
docker run --rm -it \
  -v "$PWD:/app" \
  -v node_modules:/app/node_modules \
  -w /app \
  node:20-alpine sh -lc "npm ci"

Samenvatting

“Permission denied” bij Docker-volumes is bijna altijd terug te voeren op één (of een combinatie) van:

Door systematisch te debuggen (wie ben ik in de container, wie is eigenaar van het pad, welke mount is het, en speelt SELinux mee?) kun je het probleem snel isoleren. Kies vervolgens de passende oplossing: UID/GID matchen, permissies aanpassen, SELinux labels zetten, of overstappen op named volumes voor data en heavy directories.

Als je wilt, kun je een concreet voorbeeld delen (je docker run/Compose snippet + de exacte foutmelding + output van id in container en stat op host), dan kan ik gericht aangeven welke fix in jouw situatie het meest robuust is.