Docker » Storage & Security
Storage decides what survives when a container is removed; security decides who and what can reach that data, and the host, while the container runs. The two are coupled: a database volume that outlives its container is the most valuable thing an attacker can reach, and a secret copied into an image layer is a storage decision with a security consequence. This page covers Docker’s mount types, backup and restore, and the layered controls that harden images, builds, secrets, the container runtime, and the host. Network isolation, also a security control, has its own page: Docker: Networking.
Docker Storage: Volumes, Bind Mounts, and tmpfs
Every write a container makes lands in its writable layer, a thin copy-on-write layer on top of the read-only image (see Fundamentals). That layer is deleted with the container and is slow for write-heavy workloads. Anything that must persist, be shared, or be written heavily should live in a mount instead.
flowchart LR
subgraph C["Container filesystem view"]
direction TB
root["/ (image layers + writable layer)"]
m1["/var/lib/postgresql/data"]
m2["/app/src"]
m3["/tmp"]
m4["/tools"]
end
V[("Volume<br/>/var/lib/docker/volumes/pgdata<br/>or a volume driver (NFS, cloud disk)")] --> m1
B["Host directory<br/>~/project/src"] --> m2
T["Host RAM (tmpfs)"] --> m3
I["Another image<br/>(read-only)"] --> m4
| Mount type | Backed by | Lifecycle | Typical use | Caveats |
|---|---|---|---|---|
| Volume | Directory managed by Docker, or a volume driver | Independent of containers; removed only explicitly | Databases, uploads, any persistent state | Host path is an implementation detail; back it up yourself |
| Bind mount | Any host path | Tied to the host filesystem | Source code in development, host config files | Host-dependent, inherits host ownership, container can modify host files |
| tmpfs | Host memory (may be swapped) | Discarded when the container stops | Scratch space, runtime sockets, PID files, secrets in flight | Consumes RAM; Linux only |
| Image mount | Layers of another image, read-only | Lives with the container | Bringing debug tools or shared assets into a container | Requires the containerd image store |
| Named pipe | Windows named pipe | Host-defined | Windows containers talking to the Engine API | Windows only |
--mount is the explicit syntax and is preferred in scripts: type=, source=, target= and options are named, and a misspelled bind source is an error. The short -v form is terser but silently creates a new volume or host directory when a name or path is wrong.
Volumes
Named volumes are the default choice for persistent data. Docker owns their lifecycle, they work the same on every platform (including Docker Desktop, where bind mounts cross a VM boundary and are slower), and they can be backed by drivers for NFS, CIFS, or cloud block storage.
docker volume create pgdata
docker run -d --name db \
-e POSTGRES_PASSWORD=example \
--mount type=volume,source=pgdata,target=/var/lib/postgresql/data \
postgres:17
docker volume ls
docker volume inspect pgdata # driver, mountpoint, labels
docker volume ls -f dangling=true # volumes attached to no container
docker volume rm pgdata
Behaviors worth knowing:
- Pre-population. When an empty volume is mounted over a directory that already contains files in the image, Docker copies those files into the volume first. Add
volume-nocopyto disable this. Bind mounts never pre-populate; they hide the image’s content. - Subpaths. One volume can serve several containers at different subdirectories:
--mount type=volume,src=logs,dst=/var/log/app1,volume-subpath=app1. The subdirectory must already exist in the volume. - Anonymous volumes. A
VOLUMEinstruction in an image, or-v /datawithout a name, creates a volume with a random name. These accumulate and are the usual reason a Docker host fills up;docker run --rmremoves a container’s anonymous volumes with it, anddocker volume pruneremoves unused anonymous volumes (add-ato include unused named volumes).
In Compose, declare volumes at the top level and reference them by name:
services:
db:
image: postgres:17
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata: {}
docker compose down keeps named volumes; docker compose down -v deletes them.
Bind Mounts
A bind mount exposes a host path directly. There is no copy and no indirection, which is what makes it useful for development and risky in production.
# Live-edit source in a dev container
docker run --rm -it -p 3000:3000 \
--mount type=bind,source="$(pwd)",target=/app \
-w /app node:24 npm run dev
# Read-only config file
docker run -d \
--mount type=bind,source="$(pwd)/nginx.conf",target=/etc/nginx/nginx.conf,readonly \
nginx:1.29
- Shadowing. A bind mount hides whatever the image had at the target path. Mounting a project directory over
/appalso hides the image’s/app/node_modules; a common workaround is an extra anonymous volume at/app/node_modules. For Compose development loops,docker compose watch(file sync and rebuild on change) avoids bind mounts entirely. - Ownership. Files keep their host UID/GID. If the container runs as UID 1000 and the host files belong to another UID, writes fail. Match the UID with
--useroruser:, or adjust ownership on the host. On SELinux hosts, add the:z(shared) or:Z(private) suffix with-vso Docker relabels the content. - Blast radius. Never bind-mount sensitive host paths (
/,/etc,/var/run/docker.sock) into untrusted containers. Mount read-only whenever the container only needs to read.
tmpfs Mounts
A tmpfs mount lives in memory and disappears when the container stops.
docker run -d --read-only \
--tmpfs /tmp:rw,size=64m,mode=1777 \
--tmpfs /run:rw,size=8m \
my-app:1.4.2
Always set size=: tmpfs consumes host RAM, counts against the container’s memory limit, and an unbounded mount can drive the host into memory pressure. tmpfs pages can be written to swap, so “never touches disk” holds only on hosts without swap or with encrypted swap. The natural pairing is a --read-only root filesystem with small tmpfs mounts for the few paths that must be writable.
Image Mounts
An image mount makes another image’s filesystem available read-only at a chosen path, without adding it to the container’s own image. It is useful for bringing a tool-rich image into a minimal (distroless) container for debugging, or for sharing read-only assets such as model weights or static files across containers built from different images. Image mounts require the containerd image store, the default on fresh Docker Engine 29 installs.
Sharing Data Between Containers
Several containers can mount the same volume. Grant the least access that works, and remember that Docker does not coordinate concurrent writers: file locking is the application’s job.
docker volume create shared-logs
docker run -d --name app --mount type=volume,src=shared-logs,dst=/var/log/app my-app
docker run -d --name shipper --mount type=volume,src=shared-logs,dst=/logs,readonly log-shipper
Backup and Restore
Docker does not back up volumes. The portable approach runs a short-lived helper container that mounts the volume and streams a tarball to the host:
# Backup (source mounted read-only)
docker run --rm \
--mount type=volume,src=pgdata,dst=/source,readonly \
--mount type=bind,src="$(pwd)",dst=/backup \
alpine:3.22 tar czf /backup/pgdata-$(date +%F).tar.gz -C /source .
# Restore into a fresh volume
docker volume create pgdata-restored
docker run --rm \
--mount type=volume,src=pgdata-restored,dst=/target \
--mount type=bind,src="$(pwd)",dst=/backup,readonly \
alpine:3.22 tar xzf /backup/pgdata-2026-09-22.tar.gz -C /target
For production data:
- Use the application’s consistent backup tool for databases. A
tarof a live database directory can capture a torn, mid-write state. Preferpg_dump/pg_basebackup,mysqldump, ormongodump(run withdocker exec), or stop the container during a file-level copy. - Restore into a new volume, verify it, then switch the service over; never extract over live data.
- Test restores regularly. An untested backup is a hypothesis.
- Use storage snapshots where available. Volume drivers backed by LVM, ZFS, or cloud disks can take block-level snapshots that are faster and crash-consistent, though not portable across backends.
Docker Security Best Practices
Container isolation depends on a shared kernel, so no single setting makes a container safe. The goal is defense in depth: each layer assumes the one before it may fail.
flowchart TB
subgraph Supply["Supply chain"]
I["Image: minimal base, pinned digest,<br/>scanned, signed"]
Bd["Build: BuildKit secrets, multi-stage,<br/>no credentials in layers"]
end
subgraph Run["Runtime"]
U["Non-root user, read-only rootfs"]
K["Capabilities dropped, no-new-privileges,<br/>seccomp, AppArmor/SELinux"]
R["cgroup limits: memory, CPU, PIDs"]
N["Network: user-defined networks,<br/>no unnecessary published ports"]
end
subgraph Host["Host"]
H["Patched kernel and Engine, rootless or userns-remap,<br/>docker.sock protected, audit / CIS benchmark"]
end
Supply --> Run --> Host
| Layer | Threat it addresses | Key controls |
|---|---|---|
| Image | Known-vulnerable or malicious packages | Minimal or hardened base, pinned digests, scanning, signature verification |
| Build | Credentials leaking into layers or build logs | BuildKit secret and SSH mounts, multi-stage builds, .dockerignore |
| Runtime | A compromised process escalating or escaping | Non-root user, capability drop, no-new-privileges, seccomp, LSM profiles, read-only rootfs |
| Resources | Denial of service against the host or neighbors | --memory, --cpus, --pids-limit, ulimits, log rotation |
| Network | Lateral movement, accidental exposure | Segmented user-defined networks, loopback-bound ports (Networking) |
| Host | Daemon or kernel compromise | Updates, rootless mode, socket protection, CIS Docker Benchmark |
Image Security
A container is only as trustworthy as the layers it is built from.
- Start from a minimal, maintained base. Fewer packages means fewer CVEs and fewer tools for an attacker. Options, roughly from largest to smallest:
-slimDebian variants, Alpine, Wolfi/Chainguard images, Google distroless, andscratchfor static binaries. Docker Hardened Images provide non-root, distroless-style variants of common bases with signed SBOMs, VEX statements, and SLSA Build Level 3 provenance; their core catalog is free under Apache 2.0. - Pin versions, and pin digests for production.
FROM python:3.13-slim@sha256:...makes builds reproducible; let Dependabot or Renovate propose digest bumps so pins do not go stale. - Keep build tooling out of the runtime image. Compile in a builder stage and copy only the artifact into the final stage; see Dockerfiles & CI/CD.
- Never put secrets in layers. Anyone who can pull an image can unpack every layer, including files deleted in later layers, and
docker historyshowsARGandENVvalues. - Scan, sign, and attest. Vulnerability scanning, signing, SBOMs, and provenance are covered in depth on Registries & Supply Chain.
Running Containers Securely
Run as a non-root user. Without user namespaces, UID 0 in a container is UID 0 on the host; a kernel or runtime bug that lets a process escape then yields host root. Set the user in the image:
FROM python:3.13-slim
RUN useradd --system --uid 10001 --no-create-home app
WORKDIR /app
COPY --chown=app:app . .
USER 10001
CMD ["python", "app.py"]
Use a numeric USER so that orchestrators (for example Kubernetes runAsNonRoot) can verify it without resolving /etc/passwd. Then tighten the runtime:
docker run -d --name api \
--user 10001:10001 \
--read-only --tmpfs /tmp:size=64m \
--cap-drop ALL \
--security-opt no-new-privileges=true \
--memory 512m --cpus 1 --pids-limit 200 \
-p 127.0.0.1:8080:8080 \
my-api:1.4.2
| Control | What it does |
|---|---|
--user / USER |
Runs PID 1 as an unprivileged UID. |
--read-only |
Makes the root filesystem immutable; add tmpfs mounts for paths that need writes. |
--cap-drop ALL / --cap-add |
Removes Linux capabilities; add back only what is needed (for example NET_BIND_SERVICE to bind ports below 1024). |
--security-opt no-new-privileges=true |
Prevents gaining privileges through setuid/setgid binaries or file capabilities. |
| seccomp | Filters syscalls. The default profile blocks dozens of dangerous calls (for example kexec_load, mount, reboot, and kernel-module loading); supply a tighter custom profile with --security-opt seccomp=profile.json. Never use seccomp=unconfined in production. |
| AppArmor / SELinux | Mandatory access control. Keep the docker-default AppArmor profile or the SELinux container_t type enabled. |
--memory, --cpus, --pids-limit |
Contain runaway or malicious resource use; --pids-limit stops fork bombs. |
Avoid --privileged. It disables seccomp, AppArmor/SELinux confinement, and the capability drop, and exposes all host devices; a privileged container is effectively root on the host. If a workload needs one device or one capability, grant exactly that (--device, --cap-add).
Treat docker.sock as root. Mounting /var/run/docker.sock into a container (common for CI agents, reverse proxies, and dashboards) lets that container start a privileged container and take over the host. If unavoidable, put a filtering socket proxy in front of it that allows only the API calls needed.
Host Hardening
- Rootless mode. Rootless Docker runs the daemon and containers as an ordinary user, so a full daemon compromise yields only that user’s privileges. Trade-offs include restricted networking performance, no binding to ports below 1024 without extra configuration, and limited cgroup support on older setups.
- User-namespace remapping. With
userns-remapindaemon.json, container UID 0 maps to an unprivileged range on the host while the daemon still runs as root. It is incompatible with some features, and on Engine 29 it keeps the classic image store rather than the containerd store. - Keep the kernel, runc, and Engine patched. Container escapes are usually kernel or runtime vulnerabilities (CVE-2019-5736 and CVE-2024-21626 in runc are well-known examples); updates are the only fix.
- cgroup v2. cgroup v1 is deprecated as of Engine 29; cgroup v2 provides better resource accounting and is required by newer features.
- Audit. The CIS Docker Benchmark, and the
docker-bench-securityscript that implements it, check daemon, host, and container configuration against published recommendations.
Secrets Management
The rule is mount, do not bake: deliver secrets as files at build time or run time so they never enter an image layer, the image config, or docker inspect output.
| Stage | Mechanism | Notes |
|---|---|---|
| Build | BuildKit secret mounts (RUN --mount=type=secret) |
Available only to the RUN step that mounts it; never written to a layer or the cache key. |
| Build | BuildKit SSH mounts (RUN --mount=type=ssh) |
Forwards the host SSH agent for cloning private repositories. |
| Run (Compose) | Compose secrets: |
Mounted read-only at /run/secrets/<name> from a file or environment variable. |
| Run (Swarm) | Docker secrets | Encrypted in the Raft store, delivered to tasks as tmpfs files. |
| Run (production) | External secret manager | Vault, AWS Secrets Manager, GCP Secret Manager, Azure Key Vault, fetched at startup or injected by the orchestrator. |
| Development | --env-file .env |
Convenient; keep .env out of version control and out of the build context. |
Build-time secret, using the current Dockerfile syntax:
# syntax=docker/dockerfile:1
FROM python:3.13-slim
RUN --mount=type=secret,id=pip_token,env=PIP_TOKEN \
pip install --index-url "https://__token__:${PIP_TOKEN}@pypi.example.com/simple" private-pkg
docker build --secret id=pip_token,src=./pip_token.txt -t my-app .
Runtime secrets with Compose:
services:
db:
image: postgres:17
environment:
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
secrets:
- db_password
secrets:
db_password:
file: ./secrets/db_password.txt
Prefer file-mounted secrets over environment variables even at run time: environment variables appear in docker inspect, are inherited by every child process, are readable through /proc/<pid>/environ, and frequently end up in logs and crash reports. Many official images support a *_FILE variant of their password variables for this reason.
Security Checklist
Image and build
- Minimal or hardened base image, pinned by digest
- Multi-stage build; no compilers or package managers in the runtime image unless required
- No secrets in
COPY,ARG,ENV, or layers;.dockerignoreexcludes.envand.git - Scanned in CI and on a schedule; signed, with SBOM and provenance attached
Runtime
- Numeric non-root
USER --read-onlywith sized tmpfs mounts--cap-drop ALL, adding back only required capabilities--security-opt no-new-privileges=true; default or stricter seccomp and AppArmor/SELinux- Memory, CPU, and PID limits; log rotation configured
- No
--privileged, nodocker.sockmount, no sensitive host bind mounts
Host and network
- Kernel, runc, containerd, and Engine patched; cgroup v2
- Rootless mode or
userns-remapwhere compatible - User-defined networks; ports published on
127.0.0.1unless they must be public (Networking) - CIS Docker Benchmark reviewed
Troubleshooting Common Docker Issues
Most storage and security problems surface as permission errors, a full disk, or a container that exits immediately.
Debugging Containers
Container exits immediately:
docker ps -a --filter name=api # status and exit code
docker logs api # the process's own error output
docker inspect api --format '{{.State.ExitCode}} {{.State.OOMKilled}} {{.State.Error}}'
docker run --rm -it --entrypoint sh my-api:1.4.2 # explore the image by hand
Exit code 137 with OOMKilled=true means the memory limit was hit; 126 or 127 usually means the entrypoint is not executable or does not exist in the image.
Permission and mount problems:
docker inspect api --format '{{json .Mounts}}'
docker exec api id # UID/GID the process runs as
docker exec api ls -ln /data # numeric ownership of the mounted files
Most “permission denied” errors on mounts are UID mismatches between the container user and the files. On SELinux hosts, an unlabeled bind mount produces the same symptom; add :z or :Z.
Disk usage:
docker system df -v # images, containers, volumes, build cache
Common Error Solutions
| Error | Likely cause | Fix |
|---|---|---|
Cannot connect to the Docker daemon |
Daemon not running, or wrong context | sudo systemctl start docker; check docker context ls and DOCKER_HOST |
permission denied ... docker.sock |
User lacks socket access | Use sudo, rootless Docker, or add the user to the docker group (root-equivalent; log out and back in) |
no space left on device |
Images, build cache, logs, or volumes | docker system df, then docker builder prune, docker image prune -a; rotate logs |
permission denied on a mount |
UID mismatch or SELinux label | Align --user with file ownership; :z/:Z on SELinux |
Read-only file system |
Container started with --read-only |
Add a sized --tmpfs for the path that needs writes |
exec format error |
Image built for another CPU architecture | Pull or build the right platform (--platform linux/arm64) |
Reclaiming space safely:
docker system prune # stopped containers, unused networks, dangling images, build cache
docker volume prune # unused anonymous volumes only
docker system prune -a --volumes # everything unused, including named volumes: destructive
Be deliberate with --volumes: it removes every volume not attached to a container, including databases whose container happens to be stopped.
Health Checks
A health check lets Docker, Compose, and Swarm distinguish “running” from “working”.
HEALTHCHECK --interval=30s --timeout=3s --start-period=20s --retries=3 \
CMD ["/app/healthcheck"]
docker ps # STATUS shows (healthy) / (unhealthy)
docker inspect --format '{{json .State.Health}}' api
Slim and distroless images often lack curl or wget, so a common pattern is a small health-check binary or script shipped with the application. --start-period gives slow-starting services a grace period during which failures do not count. Docker itself does not restart unhealthy standalone containers; Compose depends_on: condition: service_healthy, Swarm, or an external supervisor acts on the status. Kubernetes ignores HEALTHCHECK and uses its own probes.
See Also
- Fundamentals - Images, layers, the engine architecture, and isolation primitives
- Docker: Networking - Network drivers, service discovery, and network-layer security
- Dockerfiles & CI/CD - Multi-stage builds, BuildKit features, and pipelines
- Registries & Supply Chain - Scanning, signing, SBOMs, and provenance
- Container Runtimes - Sandboxed runtimes (gVisor, Kata) for stronger isolation
- Docker Essentials - Command cheat sheet
- Cloud and Container Security - Broader container security context