World map

 

BM: Baca dalam Bahasa Melayu

World D5: Ship & Shield

Least privilege containers

You will learn: to run a container as non-root, with a read-only filesystem and no extra powers.

Checked with Docker Engine 29 on 31 Oct 2026

The problem

Inside a plain alpine container, id prints uid=0(root). Docker transcript: $ docker run --rm alpine:3.22 id uid=0(root) gid=0(root) groups=0(root),0(root),1(bin),2(daemon),3(sys),4(adm),6(disk),10(wheel),11(floppy),20(dialout),26(tape),27(video)

Your laptop~/kedai-api

  • No open hatches

Lunchboxes (containers)

  • No containers yet

Frozen packs (images)

  • alpine:3.221 layers · 12.9MB
  • kedai-api:1.06 layers · 244MB
  • node:24-alpine4 layers · 244MB

0 containers (0 running), 3 images, 0 lockers.

Inside the lunchbox, your app runs as root: the head chef, holding every key. If someone breaks in, they hold every key too.

Watch

least privilege: Give each worker only the keys their station needs. Nothing extra. means the box gets only the keys it needs. Docker has three shields. Match each one to what it does.

Drag a card into a box. Or tap the card, then tap the box.

        Watch

        With --read-only, touch fails: Read-only file system. With --cap-drop ALL, chown fails: Operation not permitted. Docker transcript: $ docker run --rm --read-only alpine:3.22 touch /hello touch: /hello: Read-only file system $ docker run --rm --cap-drop ALL alpine:3.22 chown nobody /tmp chown: /tmp: Operation not permitted

        Your laptop~/kedai-api

        • No open hatches

        Lunchboxes (containers)

        • No containers yet

        Frozen packs (images)

        • alpine:3.221 layers · 12.9MB
        • kedai-api:1.06 layers · 244MB
        • node:24-alpine4 layers · 244MB

        0 containers (0 running), 3 images, 0 lockers.

        Real errors, plain words. Read-only file system: the lid is sealed. Operation not permitted: that knife, a capability: One special root power, like changing who owns a file. Drop the ones your app never uses., was put away.

        Try it

        Run the API with all three shields. Its image already says USER node. Add the other two.

        Your laptop~/kedai-api

        • No open hatches

        Lunchboxes (containers)

        • No containers yet

        Frozen packs (images)

        • alpine:3.221 layers · 12.9MB
        • kedai-api:1.06 layers · 244MB
        • node:24-alpine4 layers · 244MB

        0 containers (0 running), 3 images, 0 lockers.

        Predict

        nginx starts as root and writes cache files first. You run it with --read-only. What happens?

        Pick one answer.

        Check

        bash
        docker run --rm --read-only nginx:1.30-alpine
        …
        nginx: [emerg] mkdir() "/var/cache/nginx/client_temp" failed (30: Read-only file system)
        docker run -d --name web --read-only --tmpfs /var/cache/nginx --tmpfs /run nginx:1.30-alpine
        …
        docker ps --filter name=web --format '{{.Status}}'
        Up 2 seconds
        nginx with --read-only stops: mkdir of /var/cache/nginx/client_temp failed, Read-only file system. With two tmpfs folders it runs: Up 2 seconds.

        The fix: two small scratch folders in memory, with --tmpfs. Everything else stays sealed.

        You did it!

        You can now lock a container down: non-root, read-only, no extra powers.

        Go deeper (optional)

        By default Docker keeps 14 capabilities, like CHOWN and NET_BIND_SERVICE. Add back only what you need: --cap-add CHOWN. --security-opt no-new-privileges stops a program from gaining more power. Never use --privileged, and never mount the Docker socket: both hand over the whole host. Compose has user, read_only and cap_drop.

        Real rep (optional), on your own machine:

        Real mission

        1. docker run --rm alpine:3.22 id
        2. docker run --rm --user 1000 --read-only --cap-drop ALL alpine:3.22 id

        0 of 3 steps done · Your ticks are saved.