How do you handle secrets in Docker without baking them into images?
Quick Answer
Never embed secrets in Dockerfiles or images. Inject them at runtime using environment variables, Docker Swarm secrets, mounted secret volumes, or external secret managers like Vault or AWS Secrets Manager.
Detailed Answer
Imagine baking a house key into a brick wall during construction. Anyone who gets access to the wall, even years later, can dig out the key. Baking secrets into Docker images is just as dangerous: images are stored in registries, cached on build servers, and pulled to every node that needs them. Even if you delete the secret in a later Dockerfile layer, it stays in the image's layer history and can be extracted with docker history or by inspecting the layer tar archive. The right approach is to hand the key to the resident only at the moment they walk through the door. Inject secrets at runtime, not build time.
Docker secrets management is about keeping sensitive data like database passwords, API keys, TLS certificates, and tokens out of image layers and source code. The simplest approach is runtime environment variables passed via docker run -e or the environment key in Docker Compose. While this works, environment variables are visible in docker inspect output and process listings inside the container. Docker Swarm has a built-in secrets feature: you create a secret with docker secret create, reference it in your service definition, and Docker mounts it as a file at /run/secrets/secret_name inside the container with restricted permissions. The secret is encrypted at rest in the Swarm Raft log and only sent to nodes running tasks that need it. For non-Swarm setups, you can inject secrets via volume mounts from the host or tmpfs mounts that live only in memory.
Internally, Docker Swarm secrets use the Raft consensus protocol to replicate encrypted secret data across manager nodes. When a service task gets scheduled on a worker node, the manager sends the decrypted secret over a mutually authenticated TLS connection to the worker. The secret is mounted as a tmpfs filesystem inside the container, meaning it never touches disk on the worker node. For third-party tools, HashiCorp Vault uses a sidecar or init container pattern: an init container authenticates to Vault using a machine identity (like an IAM role or Kubernetes service account), fetches the secrets, and writes them to a shared tmpfs volume. The application container reads from that volume and never needs to know about Vault directly. AWS Secrets Manager and GCP Secret Manager follow similar patterns, often using cloud-native identity federation instead of static credentials.
In production, a layered approach is standard. First, use a .dockerignore file to stop secret files like .env, credentials.json, and private keys from being included in the build context. Second, use multi-stage builds so even if a secret is needed during the build (for example, to access a private package registry), it stays in the builder stage and is never copied to the final image. Third, use BuildKit secret mounts with --mount=type=secret, which makes the secret available to a single RUN instruction without storing it in any layer. Fourth, at runtime, connect to an external secret manager that supports automatic rotation. When a database password rotates, the manager updates the mounted value and the application reloads it without a container restart. This rotation ability is something environment variables cannot provide because they are fixed at container start.
The most dangerous gotcha is using ARG for secrets in a Dockerfile. Build arguments show up in image metadata and can be extracted with docker history --no-trunc. Similarly, running RUN echo $SECRET > /app/.env followed by RUN rm /app/.env does not actually remove the secret. It is still in the layer created by the echo command. Always use BuildKit secret mounts for build-time secrets. Another trap is logging: applications that print their configuration at startup may accidentally write secrets to stdout, which Docker captures and stores. Make sure your logging framework redacts sensitive fields. Finally, docker inspect on a running container reveals environment variables in plain text. Anyone with Docker socket access can read every secret passed via -e flags, which is why file-based secret injection with restricted permissions is preferred for high-security setups.