Safeguard
Container Security

Keeping Docker secrets secure without Kubernetes

Docker ships with tmpfs-backed Swarm secrets, BuildKit secret mounts, and Compose file secrets — here's how to use them without Kubernetes.

Priya Mehta
DevSecOps Engineer
8 min read

A single leaked AWS access key baked into a Docker image layer cost one fintech startup roughly $72,000 in unauthorized compute charges before anyone noticed — the key had been sitting in a public Docker Hub repository for 11 days, left there by an ENV instruction in a Dockerfile written back in 2022. Kubernetes dominates most supply chain security conversations, but a huge share of production workloads still run on plain Docker Engine, Docker Compose, or Docker Swarm, with no etcd, no Secrets API, and no admission controllers to fall back on. That doesn't mean secrets have to live in plaintext environment variables or get committed straight into a docker-compose.yml. Docker itself ships with tmpfs-backed Swarm secrets, BuildKit's --mount=type=secret build flag, and Compose's file-based secrets block. Paired with an external vault, these primitives cover most of what Kubernetes Secrets do, minus the orchestration overhead. Here's how docker secrets management actually works when Kubernetes isn't in the picture.

Why shouldn't you pass secrets through Docker environment variables?

Environment variables are visible to anyone who can run docker inspect, read /proc/<pid>/environ on the host, or grep application logs, so they should never be the delivery mechanism for credentials. Run docker inspect <container_id> on almost any container started with -e DB_PASSWORD=... and the value shows up in plaintext inside the Config.Env array — no special privileges required beyond Docker socket access. Child processes spawned inside the container inherit the full environment by default, so a compromised dependency or a debug shell (docker exec -it <id> env) exposes everything at once. GitGuardian's 2024 State of Secrets Sprawl report found 12.8 million secrets exposed on public GitHub in 2023 alone, a 28% jump year over year, and a meaningful share of those were database URLs and API keys lifted straight out of committed .env files that were meant to feed docker run --env-file. Once a secret is in an env var, it also tends to get copied into crash dumps, CI logs, and monitoring tool payloads, which is why it spreads so much faster than a secret confined to a single mounted file.

What does Docker Swarm's native secrets feature actually protect against?

Docker Swarm secrets, introduced in Docker 1.13 in November 2016, encrypt secret data at rest inside the Raft consensus log and only ever expose it to a running task as an in-memory tmpfs file under /run/secrets/<name> — never as an environment variable and never written into an image layer. Creating one is a single command: docker secret create db_password ./password.txt, after which any service in that swarm can reference it via the secrets: block in a stack file, and Docker mounts it read-only at container start. Because the mount is tmpfs, the secret never touches the container's writable layer or the host disk, and it disappears the moment the task stops. The catch is scope: Swarm secrets only work for services deployed with docker service create or docker stack deploy in swarm mode — a plain docker run container gets none of this, and each secret is capped at 500KB, which is fine for credentials and certificates but not for large key bundles.

How do you keep secrets out of Docker image layers during the build itself?

BuildKit's --mount=type=secret flag, available since Docker 18.09 shipped in September 2018, lets a single RUN instruction access a secret file that exists only for the duration of that instruction and is never committed to a layer. A Dockerfile that starts with # syntax=docker/dockerfile:1 can run something like RUN --mount=type=secret,id=npmrc,target=/root/.npmrc npm install, and the secret is passed at build time with docker build --secret id=npmrc,src=$HOME/.npmrc. Compare that to the still-common pattern of ARG NPM_TOKEN followed by ENV NPM_TOKEN=$NPM_TOKEN — running docker history --no-trunc <image> against that image reveals the token in the build cache even after a later layer overwrites the variable, because every intermediate layer is retained. Teams that migrated CI pipelines to BuildKit secret mounts after 2019 report the change eliminates an entire category of "secret found in public image on Docker Hub" incidents, since there's simply no layer for a scanner — or an attacker who pulls the image — to inspect.

Is Docker Compose's file-based secrets support good enough for production?

Compose's secrets: top-level key is good enough for local development and small single-host deployments, provided the secret file itself lives outside version control, but it still writes the secret to disk inside the container's mount namespace at /run/secrets/<name>, so it needs to be paired with a runtime vault for anything handling real production credentials. A typical setup looks like:

services:
  api:
    image: registry.example.com/api:2026.07
    secrets:
      - db_password
secrets:
  db_password:
    file: ./secrets/db_password.txt

That keeps the credential out of the image and out of docker-compose.yml itself, but the plaintext file on the host still needs its own protection — file permissions of 600, a .gitignore entry, and ideally encryption at rest via something like git-crypt or SOPS if the file is stored anywhere near a repository. Compose v2.20+ (released mid-2023) also supports referencing secrets from external providers, which is the better path once a team outgrows local files: the secret is fetched at deploy time rather than sitting on disk indefinitely.

Do you need a dedicated secrets manager if you're not running Kubernetes?

Yes — for any workload handling production credentials, a dedicated secrets manager such as HashiCorp Vault, AWS Secrets Manager, Doppler, or 1Password Connect should sit in front of Docker's native primitives, injecting secrets at container start rather than baking them into images or Compose files. The common pattern is a Vault Agent sidecar container that authenticates to Vault using AppRole or the AWS IAM auth method, fetches a short-lived credential — Vault's dynamic database secrets engine issues credentials with a default 1-hour TTL by default, configurable per role — and writes it to a shared tmpfs volume that the application container reads on boot. This turns a static, years-old database password into a rotating credential that's worthless to an attacker within the hour even if it leaks. Doppler and AWS Secrets Manager take a simpler approach for smaller teams: a CLI wrapper (doppler run -- node server.js) injects secrets as process environment variables only inside the process's own memory space, never writing them to a file or the Compose config, which closes off the docker inspect exposure path discussed earlier without requiring a full Vault deployment.

What are the most common Docker secrets mistakes still showing up in 2026 audits?

The top three, consistently, are secrets baked into image layers via ARG/ENV pairs, .env files committed straight into git history, and plaintext database credentials sitting in a docker-compose.yml that gets checked into the same repository as the application code. All three share a root cause: the secret was treated as configuration rather than as a credential with its own lifecycle. A fourth pattern that's grown since 2024 is secrets embedded in multi-stage build cache exports — teams adopted BuildKit's remote cache feature (--cache-to type=registry) for faster CI builds without realizing that early, unoptimized Dockerfiles still leaked build-arg secrets into the exported cache image, which then sat in a private registry with broader read access than the source repo itself. Rotating a leaked secret after the fact doesn't undo the exposure window — GitGuardian's research puts the median time between a secret leaking on a public repository and its rotation at over 300 days for API keys — so the real fix is structural: stop the secret from ever landing in a layer, log line, or committed file in the first place.

How Safeguard Helps

Safeguard scans Dockerfiles, Compose manifests, and built images for exactly these patterns — hardcoded credentials in ENV/ARG instructions, secrets committed to .env files tracked by git, and plaintext values in stack files — and generates or ingests SBOMs so every credential-adjacent dependency and base image is accounted for across your container fleet. Reachability analysis then tells you whether a flagged secret or vulnerable package sits in code paths your application actually executes, so teams aren't stuck triaging thousands of theoretical findings from a scanner that can't tell a dev-only test fixture from a production credential. Griffin AI correlates those findings with the CI job, commit, and service owner responsible, and can open an auto-fix PR that swaps the hardcoded value for a BuildKit secret mount or a vault reference and strips the exposed credential from the Dockerfile — turning a multi-day incident response task into a reviewable pull request. For teams running plain Docker without Kubernetes, that combination closes the gap between "we know secrets management matters" and "we've actually verified nothing is leaking."

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.