Docker
Run containers with varsafe secrets: the CLI as entrypoint, compose interpolation from the host, and BuildKit secret mounts for builds.
Build one image and run it anywhere, with secrets arriving only when the container starts. Nothing sensitive goes into an image layer, a build argument or a committed file, so an image can be pushed, pulled and inspected without exposing anything. The only credential you handle is a team-scoped API token. Rotate a secret in varsafe and a restarted container has the new value, with no rebuild.
Build time and runtime
This distinction drives every pattern on this page:
- Runtime: the container receives its secrets when it starts. This is where application secrets belong.
- Build time: a secret needed while building the image, such as a private registry token or a licence key. Use a BuildKit secret mount, never
ARGorENV.
Which approach to use
Every runtime approach below ends with secrets in the application’s process environment. They differ in where the CLI runs and what the container needs to reach:
| Approach | Pick it when | CLI lives in | Container needs |
|---|---|---|---|
| Entrypoint | Default. Every start fetches current values, nothing on the host | The image | Network access to the varsafe API |
| Inject from the host | You cannot or will not add the CLI to the image | The host | Nothing |
| Export to an env file | A tool insists on a file | The host | Nothing |
varsafe as the entrypoint (recommended)
Install the CLI in the image and make varsafe run the entrypoint. Secrets are fetched at every container start and exist only in the application’s process environment:
# Runtime secret injection in a container: varsafe wraps the entrypoint,
# so secrets exist only in the process environment — never in a layer,
# build arg, or file.
FROM oven/bun:1-slim
RUN apt-get update && apt-get install -y --no-install-recommends curl ca-certificates openssh-client \
&& rm -rf /var/lib/apt/lists/* \
&& mkdir -p /app \
&& chown bun:bun /app
USER bun
RUN curl -fsSL https://varsafe.dev/install.sh | bash
ENV PATH="/home/bun/.varsafe/bin:${PATH}"
WORKDIR /app
# Copy only runtime inputs; dotenv files and VCS metadata never enter the image.
COPY --chmod=0555 package.json bun.lock ./
COPY --chmod=0555 src ./src
RUN bun install --production
# VARSAFE_API_TOKEN is provided at runtime (docker run -e VARSAFE_API_TOKEN)
ENTRYPOINT ["varsafe", "run", "-i", "your-project-id", "-e", "production", "--"]
CMD ["bun", "src/index.ts"]Run it with the token taken from your shell’s environment, so it never appears on the command line:
read -rs VARSAFE_API_TOKEN && export VARSAFE_API_TOKEN # paste at a hidden prompt: nothing in shell history
docker run -e VARSAFE_API_TOKEN my-image
-e VARSAFE_API_TOKEN with no value forwards the variable from the host. With compose, the token comes from the host environment the same way:
# Runtime injection with Docker Compose: the token comes from the host
# environment; application secrets never appear in this file.
services:
app:
build: .
environment:
VARSAFE_API_TOKEN: ${VARSAFE_API_TOKEN}
restart: unless-stopped
healthcheck:
test: ['CMD', 'curl', '-f', 'http://localhost:3000/health']
interval: 30s
timeout: 5s
retries: 3What the wrapper gives you:
- Current secrets on every start. Rotate a value in varsafe and restart the container: no rebuild, no redeploy.
- The token is not inherited.
varsafe runremovesVARSAFE_API_TOKENandVARSAFE_TOKENfrom your application’s environment, so the app does not pass them on to its own child processes or print them with the rest of its environment. Code in the container can still find the token, so scope it to what this container needs. - Clean shutdown. As PID 1,
varsafe runforwardsSIGTERMandSIGHUPto your application, sodocker stoptriggers its graceful shutdown, and the container exits with the application’s exit code. - Arguments pass through as written. Your
CMDreaches the application unchanged.
The container needs network access to the varsafe API at start-up.
Inject from the host environment
Run compose under varsafe run on the host, and let compose interpolate the values. Set VARSAFE_PROJECT_ID to your project ID; use -i in automation so a project rename does not affect it.
varsafe run -i "$VARSAFE_PROJECT_ID" -e production -- docker compose up -d
services:
app:
image: my-image
environment:
- DATABASE_URL=${DATABASE_URL}
- API_KEY=${API_KEY}
Compose resolves ${VAR} from the environment varsafe run created. Name each variable the service needs: nothing else reaches the container. To limit what is injected into the compose process, use --include, which takes comma-separated glob patterns and can be repeated:
varsafe run -i "$VARSAFE_PROJECT_ID" -e production --include 'DB_*,API_*' -- docker compose up -d
With docker run, list each variable to forward (a bare -e KEY copies it from the host environment):
varsafe run -i "$VARSAFE_PROJECT_ID" -e production -- docker run -e DATABASE_URL -e API_KEY my-image
varsafe run fetches secrets before starting Docker, so a failed fetch stops the container command from running. Host injection also supports multi-line values.
Values are read when the containers are created. Run Compose under varsafe run again after rotating a secret so it can recreate containers with the updated configuration. A plain docker restart keeps the old environment.
Export to an env file
When a tool insists on a file, use this Linux-only recipe on a host with /dev/shm mounted as tmpfs. The consumer decides the format:
(
env_file=/dev/shm/varsafe-$$.env
trap 'rm -f "$env_file"' EXIT
varsafe export -i "$VARSAFE_PROJECT_ID" -e production --plain --tmpfs -o "${env_file##*/}" &&
docker compose --env-file "$env_file" up -d
)Compose uses this file only to fill in ${VAR} references in compose.yaml, so list each variable under the service’s environment:, as in host injection.
For docker run --env-file, use -f docker: it treats quotes as literal characters, and the docker format leaves values unquoted:
(
env_file=/dev/shm/varsafe-$$.docker
trap 'rm -f "$env_file"' EXIT
varsafe export -i "$VARSAFE_PROJECT_ID" -e production -f docker --tmpfs -o "${env_file##*/}" &&
docker run --env-file "$env_file" my-image
)The docker format refuses multi-line values; use host injection for those. If export fails, && prevents Docker from starting.
--tmpfs writes to /dev/shm/ plus the file name from -o, discarding any directory part. It creates and mounts nothing, so where /dev/shm does not exist, as on macOS, the export fails and && skips the next command. The file is 0600, readable by you only. The process ID in the name keeps two jobs running as the same user on one runner from reading or deleting each other’s file. The trap deletes it when the subshell exits, even if Compose fails, and && keeps Compose from reading a stale file if the export fails. The CLI refuses to write through a symlink or over a file that belongs to someone else.
On macOS, prefer host injection with varsafe run. If a file is required, omit --tmpfs and export to a 0600 file under a private mktemp -d directory removed by an exit trap. Use -f docker when the consumer is docker run --env-file. See the temporary-directory recipe; this file can reach disk.
Build-time secrets
For a secret needed during docker build, mount it into the single RUN instruction that uses it. BuildKit exposes it to that command and writes it to no layer:
# syntax=docker/dockerfile:1
RUN --mount=type=secret,id=npm_token,env=NPM_TOKEN npm ci
Then run the build under varsafe run, which puts the value in the build client’s environment, and hand it to BuildKit from there:
varsafe run -i "$VARSAFE_PROJECT_ID" -e production --include NPM_TOKEN -- \
docker build --secret id=npm_token,env=NPM_TOKEN -t my-image .
The value never touches the disk, the build arguments or the image. Mount each secret the build needs this way, and only on the steps that need it.
In CI/CD
In a pipeline, store the API token as a masked CI variable and combine it with any pattern above. The full walkthrough, with GitLab CI and GitHub Actions configurations, is in CI/CD pipelines.
Verify
-
Check that a secret arrived, without printing it:
docker run --rm -e VARSAFE_API_TOKEN my-image sh -c 'test -n "$DATABASE_URL" && echo "DATABASE_URL is set"' -
Check that no layer of the image holds a secret value:
docker history --no-trunc my-image -
Rotate a test secret in the dashboard, restart the container, and confirm the application sees the new value.