E-NO
Docker security 11 Min Read

Docker security hardening with practical examples: practical implementation guide

calendar_today Published: 2026-07-28
update Last Updated: 2026-07-28
analytics SEO Efficiency: 100%
Technical guide illustration for Docker security hardening with practical examples: practical implementation guide.

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.

  1. 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
  1. 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.
  1. 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": "/etc/docker/certs.d/server-cert.pem",
  "tlskey": "/etc/docker/certs.d/server-key.pem",
  "tlscacert": "/etc/docker/certs.d/ca.pem",
  "tlsverify": true
}

7. Image hygiene and pinning

  • Pin images by digest: repo: tag@sha256:<digest> 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.

AreaControlWhere to setExampleExpected outcome
AccessRestrict docker groupHostManage membership with sudoFewer users with root-equivalent access
Daemonuserns-remap/etc/docker/daemon.json"userns-remap": "default"Container root mapped to unprivileged host UID
RuntimeNon-root userDockerfile/run flags--user 10001:10001App runs without root inside container
RuntimeCapabilitiesRun flags--cap-drop=ALL --cap-add=NET_BIND_SERVICELeast-privilege capabilities
RuntimeNo new privilegesRun flags--security-opt no-new-privilegesBlocks privilege escalation
RuntimeSeccomp profileRun flags--security-opt seccomp=defaultSyscall filtering active
FilesystemRead-only rootRun flags--read-only; --tmpfs /tmpPrevents unexpected writes to rootfs
SecretsFile mounts or SwarmRun/service flags-v secret:...:ro; --secret ...Secrets not in env or image layers
NetworkMinimize exposureRun flags/networks-p 127.0.0.1:8080:8080; --internalReduced external attack surface
LimitsMemory/CPU/PIDsRun flags--memory 256m --cpus 1 --pids-limit 200Contained resource usage

---

Verification and Diagnostics

Verify each control with targeted checks and record results.

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

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

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

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

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

  1. Health and logs
sudo docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.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

SymptomLikely causeQuick fix
App cannot write to expected path--read-only blocks writesMount a writable path: -v /srv/app/data:/app/data: rw or add --tmpfs /var/cache/app
Permission denied on bind mountuserns-remap changed host UID/GID mappingchown -R <remapped_uid>:<remapped_gid> /srv/app/data (constructed example: 165536:165536)
Cannot bind to port 80 as non-rootMissing NET_BIND_SERVICE capabilityAdd --cap-add=NET_BIND_SERVICE or use port >= 1024
Network calls fail from internal net--internal blocks egressUse a non-internal network for services needing egress
Secret file unreadableWrong UID/GID or mode on hostSet -o <uid> -g <gid> -m 0400 on the secret file and mount :ro
Process killed with OOM or pids limitLimits too tightIncrease --memory or --pids-limit with measured headroom

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

  1. Restore previous daemon.json
sudo cp /etc/docker/daemon.json.bak.$(date +%F -d 'yesterday' 2>/dev/null || echo backup) /etc/docker/daemon.json
  1. 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.

Article Quality Score

Reader usefulness 100%
  • check_circle Reader-ready guide
  • check_circle Practical examples included
  • check_circle Clean SEO article URL