## Intro

This is a practical, step-by-step guide to hardening Docker with examples you can run in a controlled pilot. You will establish a known-good baseline, apply safe configuration changes, verify results, and recover if anything breaks. The focus is on least privilege, secrets, permissions, and limiting network exposure while keeping changes measurable and reversible.

Scope note: examples below are constructed to illustrate safe patterns you can adapt to your environment.

---

## Version and Environment Inventory

Before any changes, capture the current state so you can compare, verify, and roll back if needed. Keep this scoped to a small pilot container or service that you can inspect locally.

- Record core versions and settings

Constructed example commands:

# Record Docker and kernel info
 sudo docker version
 sudo docker info
 uname -a

 # Save to a dated snapshot
 sudo docker version > baseline-$(date +%F).txt
 sudo docker info >> baseline-$(date +%F).txt 

- Identify the pilot and topology

- Choose one simple service for the pilot (for example, a small HTTP server bound to localhost).

- Decide the minimum host paths that must be mounted.

- Decide the smallest set of exposed ports and networks required.

- Confirm prerequisites

- Linux host with a supported Docker Engine.

- Ability to edit /etc/docker/daemon.json and restart Docker.

- Sudo privileges for administrative steps.

 Why start small? A narrow, measurable pilot is easy to inspect, lets you quantify effects of each control, and reduces rework when issues arise later.

---

## Safe Configuration Path

Apply hardening in layers and verify after each layer. The goal is least privilege and minimal exposure without breaking required functionality.

### 1. Access control around the Docker daemon

- Do not broadly grant membership in the docker group; it effectively provides root on the host.

- Prefer command execution via sudo for admins. For developers, consider rootless Docker on dev machines if feasible.

Optional pilot: rootless Docker on a developer workstation (constructed example)

# Prereqs: ensure 'uidmap' is installed, then run:
sudo apt-get install -y uidmap
export XDG_RUNTIME_DIR=/run/user/$(id -u)
export PATH=/usr/bin:$PATH

# Set up rootless (constructed example)
dockerd-rootless-setuptool.sh install
systemctl --user enable --now docker
export DOCKER_HOST=unix:///run/user/$(id -u)/docker.sock 
 Verify with docker info that it reports a rootless mode if used.

### 2. Daemon defaults that reduce blast radius

Edit /etc/docker/daemon.json and restart Docker. Back up the file first.

Constructed example daemon.json :

{
 "icc": false,
 "live-restore": true,
 "userns-remap": "default",
 "log-driver": "local",
 "log-opts": {
 "max-size": "10m",
 "max-file": "3"
 }
} 

- icc: false blocks inter-container communication on the default bridge unless explicitly networked together.

- live-restore: true helps containers survive a daemon restart.

- userns-remap: default maps container root to an unprivileged UID/GID on the host.

- Local logging with size limits helps avoid disk fill.

 Apply and restart:

sudo cp /etc/docker/daemon.json /etc/docker/daemon.json.bak.$(date +%F)
sudo systemctl restart docker 
 Note on user namespace remap: after enabling, host file ownership for bind mounts may need to match the remapped range. See Recovery for details.

### 3. Hardened container run pattern

Start with a minimal container and gradually add only what is required. The flags below demonstrate a strong default stance.

Constructed example: hardened web container

# Constructed example image and digest; replace with your own
IMAGE="mycorp/web:1.2.3@sha256:deadbeef..."

sudo docker network create --driver bridge --subnet 172.30.0.0/24 --internal app_net || true

sudo docker run --rm -d \
 --name web \
 --network app_net \
 --read-only \
 --tmpfs /tmp:rw,noexec,nosuid,nodev,size=64m \
 --pids-limit=200 \
 --memory=256m --memory-swap=256m --cpus=1.0 \
 --cap-drop=ALL --cap-add=NET_BIND_SERVICE \
 --security-opt no-new-privileges \
 --security-opt seccomp=default \
 --user 10001:10001 \
 -v /srv/web/static:/srv/web/static:ro \
 -p 127.0.0.1:8080:8080 \
 --health-cmd='curl -sSf http://127.0.0.1:8080/health || exit 1' \
 --health-interval=30s --health-retries=3 \
 "$IMAGE" 
 Why these choices:

- --read-only prevents unexpected writes to the root filesystem.

- --tmpfs /tmp:... offers a safe write area for temporary files.

- --cap-drop=ALL then --cap-add=NET_BIND_SERVICE enforces least privilege.

- --security-opt no-new-privileges blocks privilege escalation.

- --security-opt seccomp=default keeps the default syscall filter in place.

- --user 10001:10001 avoids running as root inside the container.

- -v ...:ro binds data read-only from the host.

- --network app_net --internal combined with local port publish to 127.0.0.1 keeps network exposure tight.

If an app needs write paths beyond /tmp , mount those paths explicitly with suitable options instead of disabling --read-only globally.

### 4. Secrets handling

Goals: avoid secrets in images and environment variables; prefer files with restricted permissions.

- Docker Swarm secrets (constructed example):

echo "constructed-db-password" | docker secret create db_password -

docker service create \
 --name app \
 --secret source=db_password,target=/run/secrets/db_password,mode=0400 \
 "$IMAGE" 

- Standalone containers using a host file (constructed example):

 # Create a file readable only by the in-container user (UID 10001 here)
sudo install -o 10001 -g 10001 -m 0400 /root/db_password /srv/app/db_password

sudo docker run --rm \
 --user 10001:10001 \
 -v /srv/app/db_password:/run/secrets/db_password:ro \
 "$IMAGE" 
 Prefer file mounts over environment variables for secrets. Ensure correct UID/GID and mode (0400/0440) on the host file.

### 5. Avoiding dangerous flags and modes

- Do not use --privileged unless absolutely necessary.

- Avoid --pid=host , --net=host , and --ipc=host in the pilot; they expand the blast radius.

- Publish ports explicitly and bind to loopback if the service is meant to be local only, e.g. -p 127.0.0.1:8080:8080 .

### 6. Remote API exposure

- Do not expose the Docker API unauthenticated on tcp://0.0.0.0:2375 .

- If remote access is required, enable TLS and strict firewall rules.

Constructed example daemon.json snippet for TLS:

{
 "hosts": ["unix:///var/run/docker.sock", "tcp://0.0.0.0:2376"],
 "tls": true,
 "tlscert":
 "tlskey":
 "tlscacert":
 "tlsverify": true
} 

### 7. Image hygiene and pinning

- Pin images by digest: repo:tag@sha256: to ensure reproducibility.

- Keep base images minimal and remove unneeded packages in your own images.

- Optionally set DOCKER_CONTENT_TRUST=1 when pulling and running images in environments where content trust is configured.

### 8. Resource limits

- Memory and CPU limits: --memory , --memory-swap , --cpus .

- Process count limit: --pids-limit . These prevent a single container from exhausting host resources.

 ---

## Hardening controls quick map

The table shows where each control is set and the expected outcome.

<div class="my-stack-md overflow-x-auto">
<table class="w-full min-w-[42rem] border-collapse text-left">
<thead><tr><th scope="col" class="border border-outline-variant bg-surface-container-low px-4 py-3 text-left font-label-md font-semibold text-on-surface">Area</th><th scope="col" class="border border-outline-variant bg-surface-container-low px-4 py-3 text-left font-label-md font-semibold text-on-surface">Control</th><th scope="col" class="border border-outline-variant bg-surface-container-low px-4 py-3 text-left font-label-md font-semibold text-on-surface">Where to set</th><th scope="col" class="border border-outline-variant bg-surface-container-low px-4 py-3 text-left font-label-md font-semibold text-on-surface">Example</th><th scope="col" class="border border-outline-variant bg-surface-container-low px-4 py-3 text-left font-label-md font-semibold text-on-surface">Expected outcome</th></tr></thead>
<tbody><tr><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Access</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Restrict docker group</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Host</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Manage membership with sudo</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Fewer users with root-equivalent access</td></tr>
<tr><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Daemon</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">userns-remap</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">/etc/docker/daemon.json</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant"><code class="font-mono text-[0.9em] bg-surface-container px-1 py-0.5 rounded">&quot;userns-remap&quot;: &quot;default&quot;</code></td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Container root mapped to unprivileged host UID</td></tr>
<tr><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Runtime</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Non-root user</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Dockerfile/run flags</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant"><code class="font-mono text-[0.9em] bg-surface-container px-1 py-0.5 rounded">--user 10001:10001</code></td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">App runs without root inside container</td></tr>
<tr><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Runtime</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Capabilities</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Run flags</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant"><code class="font-mono text-[0.9em] bg-surface-container px-1 py-0.5 rounded">--cap-drop=ALL --cap-add=NET_BIND_SERVICE</code></td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Least-privilege capabilities</td></tr>
<tr><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Runtime</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">No new privileges</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Run flags</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant"><code class="font-mono text-[0.9em] bg-surface-container px-1 py-0.5 rounded">--security-opt no-new-privileges</code></td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Blocks privilege escalation</td></tr>
<tr><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Runtime</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Seccomp profile</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Run flags</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant"><code class="font-mono text-[0.9em] bg-surface-container px-1 py-0.5 rounded">--security-opt seccomp=default</code></td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Syscall filtering active</td></tr>
<tr><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Filesystem</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Read-only root</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Run flags</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant"><code class="font-mono text-[0.9em] bg-surface-container px-1 py-0.5 rounded">--read-only; --tmpfs /tmp</code></td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Prevents unexpected writes to rootfs</td></tr>
<tr><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Secrets</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">File mounts or Swarm</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Run/service flags</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant"><code class="font-mono text-[0.9em] bg-surface-container px-1 py-0.5 rounded">-v secret:...:ro; --secret ...</code></td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Secrets not in env or image layers</td></tr>
<tr><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Network</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Minimize exposure</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Run flags/networks</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant"><code class="font-mono text-[0.9em] bg-surface-container px-1 py-0.5 rounded">-p 127.0.0.1:8080:8080; --internal</code></td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Reduced external attack surface</td></tr>
<tr><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Limits</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Memory/CPU/PIDs</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Run flags</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant"><code class="font-mono text-[0.9em] bg-surface-container px-1 py-0.5 rounded">--memory 256m --cpus 1 --pids-limit 200</code></td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Contained resource usage</td></tr></tbody>
</table>
</div>
---

## Verification and Diagnostics

Verify each control with targeted checks and record results.

- Daemon settings

sudo docker info | grep -E "userns|Live Restore|Security Options"
cat /etc/docker/daemon.json 
 Expected: userns enabled, Live Restore true, security options include apparmor/selinux where applicable.

- Container runtime options (constructed container name: web)

sudo docker inspect web \
 --format 'User={{.Config.User}} ReadonlyRootfs={{.HostConfig.ReadonlyRootfs}} CapAdd={{.HostConfig.CapAdd}} CapDrop={{.HostConfig.CapDrop}} NoNewPrivs={{.HostConfig.SecurityOpt}} PidsLimit={{.HostConfig.PidsLimit}} Memory={{.HostConfig.Memory}} CPUs={{.HostConfig.NanoCpus}}' 
 Expected: non-root user ID, read-only true, CapDrop includes ALL, NoNewPrivs present, limits nonzero.

- In-container checks

sudo docker exec web id -u
sudo docker exec web sh -c 'test -w / || echo "rootfs is read-only"'
sudo docker exec web sh -c 'grep NoNewPrivs /proc/1/status || true'
sudo docker exec web sh -c 'grep CapEff /proc/1/status' 
 Expected: user ID not 0, rootfs not writable, NoNewPrivs: 1 line present (kernel dependent), capabilities effective mask small.

- Secrets

sudo docker exec web sh -c 'stat -c "%U %G %a %n" /run/secrets/* 2>/dev/null || true' 
 Expected: secret files exist only if you mounted them; modes should be 400/440 and owned by the app user.

- Network exposure

sudo ss -ltnp | grep 8080
sudo docker network inspect app_net | grep -E '"Internal":|"Subnet"' 
 Expected: service listening on 127.0.0.1:8080, network shows "Internal": true .

- Health and logs

sudo docker ps --format 'table {{.Names}} {{.Status}} {{.Ports}}'
sudo docker logs --tail 50 web 
 Expected: healthy container, no permission errors due to read-only or missing capabilities.

---

## Failure Modes and Recovery

When hardening, the most common issues relate to permissions, capabilities, and write paths. Keep a simple rollback plan.

### Common issues

<div class="my-stack-md overflow-x-auto">
<table class="w-full min-w-[42rem] border-collapse text-left">
<thead><tr><th scope="col" class="border border-outline-variant bg-surface-container-low px-4 py-3 text-left font-label-md font-semibold text-on-surface">Symptom</th><th scope="col" class="border border-outline-variant bg-surface-container-low px-4 py-3 text-left font-label-md font-semibold text-on-surface">Likely cause</th><th scope="col" class="border border-outline-variant bg-surface-container-low px-4 py-3 text-left font-label-md font-semibold text-on-surface">Quick fix</th></tr></thead>
<tbody><tr><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">App cannot write to expected path</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant"><code class="font-mono text-[0.9em] bg-surface-container px-1 py-0.5 rounded">--read-only</code> blocks writes</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Mount a writable path: <code class="font-mono text-[0.9em] bg-surface-container px-1 py-0.5 rounded">-v /srv/app/data:/app/data:rw</code> or add <code class="font-mono text-[0.9em] bg-surface-container px-1 py-0.5 rounded">--tmpfs /var/cache/app</code></td></tr>
<tr><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Permission denied on bind mount</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant"><code class="font-mono text-[0.9em] bg-surface-container px-1 py-0.5 rounded">userns-remap</code> changed host UID/GID mapping</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant"><code class="font-mono text-[0.9em] bg-surface-container px-1 py-0.5 rounded">chown -R &lt;remapped_uid&gt;:&lt;remapped_gid&gt; /srv/app/data</code> (constructed example: 165536:165536)</td></tr>
<tr><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Cannot bind to port 80 as non-root</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Missing <code class="font-mono text-[0.9em] bg-surface-container px-1 py-0.5 rounded">NET_BIND_SERVICE</code> capability</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Add <code class="font-mono text-[0.9em] bg-surface-container px-1 py-0.5 rounded">--cap-add=NET_BIND_SERVICE</code> or use port &gt;= 1024</td></tr>
<tr><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Network calls fail from internal net</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant"><code class="font-mono text-[0.9em] bg-surface-container px-1 py-0.5 rounded">--internal</code> blocks egress</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Use a non-internal network for services needing egress</td></tr>
<tr><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Secret file unreadable</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Wrong UID/GID or mode on host</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Set <code class="font-mono text-[0.9em] bg-surface-container px-1 py-0.5 rounded">-o &lt;uid&gt; -g &lt;gid&gt; -m 0400</code> on the secret file and mount <code class="font-mono text-[0.9em] bg-surface-container px-1 py-0.5 rounded">:ro</code></td></tr>
<tr><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Process killed with OOM or pids limit</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Limits too tight</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Increase <code class="font-mono text-[0.9em] bg-surface-container px-1 py-0.5 rounded">--memory</code> or <code class="font-mono text-[0.9em] bg-surface-container px-1 py-0.5 rounded">--pids-limit</code> with measured headroom</td></tr></tbody>
</table>
</div>

### User namespace remap specifics

After userns-remap , container root maps to an unprivileged range on the host, typically starting at a high UID like 165536 (constructed example). Files mounted from the host must be owned by that UID/GID to be writable inside the container.

Constructed example fix for a writable mount:

# Determine the remapped UID/GID (constructed example assumes 165536)
grep dockremap /etc/subuid /etc/subgid || true

# Adjust ownership for a data directory
sudo chown -R 165536:165536 /srv/app/data 

### Rolling back daemon changes

- Restore previous daemon.json

 sudo cp /etc/docker/daemon.json.bak.$(date +%F -d 'yesterday' 2>/dev/null || echo backup) /etc/docker/daemon.json 

- Restart the daemon

 sudo systemctl restart docker 
 Note: restarting Docker can stop containers unless live-restore is enabled. Plan a maintenance window for production changes.

### Rolling back a container run

- Stop the hardened container and start it again with previously working flags.

sudo docker stop web
# Rerun without read-only and with broader capabilities as a temporary rollback (constructed example)
sudo docker run -d --name web --cap-drop=NET_RAW -p 127.0.0.1:8080:8080 "$IMAGE" 
 Document the differences and re-apply hardening step by step to isolate the breaking flag.

---

## Operations Checklist

Use this concise checklist for each service you harden. Mark N/A where not applicable.

### Access and daemon

- [ ] Review docker group membership; remove unnecessary users

- [ ] Confirm userns-remap enabled and working

- [ ] Set live-restore and bounded log options

### Image and runtime

- [ ] Pin image by digest

- [ ] Run as non-root user

- [ ] Drop all capabilities; add back only what is needed

- [ ] Enable no-new-privileges

- [ ] Use default seccomp profile or a stricter custom one

### Filesystem and secrets

- [ ] Enable --read-only

- [ ] Provide required writable paths via --tmpfs or dedicated mounts

- [ ] Mount secrets as read-only files with correct UID/GID and mode

### Network and exposure

- [ ] Use user-defined networks; internal where possible

- [ ] Bind published ports to loopback unless external access is required

- [ ] Keep remote Docker API disabled; if enabled, require TLS and firewall it

### Limits and health

- [ ] Set memory, CPU, and PIDs limits

- [ ] Configure a healthcheck where applicable

### Verification

- [ ] Capture docker inspect key fields for the service

- [ ] Validate in-container: non-root, read-only rootfs, limited caps

- [ ] Confirm secrets ownership and permissions

- [ ] Confirm only required ports are listening

### Recovery

- [ ] Back up daemon.json before changes

- [ ] Document previous run flags to enable quick rollback

---

## Conclusion

You now have a measured, repeatable path to harden Docker in production-like conditions without guessing: establish the environment baseline, apply layered controls for access, runtime, filesystem, secrets, networking, and limits, then verify each outcome with targeted checks. Start with a narrow pilot that you can inspect locally, record what works, and scale the same approach to additional services. Keep rollback instructions close at hand so you can move fast without getting stuck when something breaks.