Secrets and configs
Hand secrets to containers as files that never appear in docker inspect, mount configuration files, and pass secrets to builds.
Dockflow reads secrets from your CI variables, .env.dockflow or DOCKFLOW_SECRETS (see configuration). A container gets a secret in one of two ways:
- Interpolated, as
${DB_PASSWORD}inenvironment:. Most images expect this. The value becomes part of the container's configuration: anyone with Docker access on the server sees it withdocker inspect. - As a file, with Compose
secrets:. The value is written to a file on a tmpfs of the server, and mounted read-only in the container. It appears neither indocker inspect, nor in the container's environment, nor in the release records. Use this for anything sensitive.
A secret as a file
services:
db:
image: postgres:17
environment:
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
secrets: [db_password]
secrets:
db_password:
environment: DB_PASSWORD # the secret DB_PASSWORD (PRODUCTION_DB_PASSWORD in CI)The official images of Postgres, MySQL, MariaDB and many others read a *_FILE variable for each setting that holds a password.
A secret's source is one of:
environment: <KEY>: a secret or variable Dockflow knows;file: <path>: a file of your project, read on your machine.
The long syntax of a service's secrets: sets where the file goes and who can read it:
# compose.yml (fragment)
services:
app:
image: ghcr.io/acme/app:1.4
user: "1000"
secrets:
- source: api_token
target: /etc/app/token # default /run/secrets/<source>; a relative target goes under /run/secrets
uid: "1000"
gid: "1000"
mode: 0400 # default 0444Configs
Compose configs: mount configuration files the same way, from file:, content: or environment:. They are not secret: Dockflow keeps them on the server's disk, with the app's state.
# compose.yml (fragment)
services:
proxy:
image: nginx:1.27
configs:
- source: site
target: /etc/nginx/conf.d/default.conf
configs:
site:
content: |
server {
listen 80;
location / { proxy_pass http://app:3000; }
}content: is interpolated like the rest of the Compose file, so ${DOMAIN} works in it. For a real template, write the file and list it under templates:.
How a new value reaches the containers
Each secret and config file is named after the hash of its content, owner and mode. So a new value is a new file:
- The plan shows
secret.create(orconfig.create) for the new file, andservice.updatefor the services that mount it. - The services move to the new file with their usual rollout, without downtime for routed services.
- After the deploy, files that no kept release uses are deleted. A rollback to a kept release finds its files on the server.
An unchanged value changes nothing: the next deploy's plan is empty.
After a reboot
The tmpfs is emptied when the server reboots. Dockflow keeps a copy of each secret file in its state directory, /var/lib/dockflow, readable by root only. The values interpolated into environment: are already kept there too.
After a reboot, the watchdog writes the files of the current releases again from these copies. It then starts the containers Docker could not start without them. A rollback finds its files the same way.
Without the watchdog (server init --no-watchdog), the apps that use Compose secrets wait for the next deploy.
Dockflow writes secrets under /run/dockflow, which must be on a tmpfs. On a systemd distribution, /run is one. Elsewhere, a deploy that needs a secret file fails with secret.not_tmpfs and writes nothing: mount a tmpfs on /run/dockflow first.
Secrets in builds
A build gets a secret with build.secrets, as in Compose. The Dockerfile mounts it in the one RUN instruction that needs it, and it does not stay in the image:
# compose.yml (fragment)
services:
web:
build:
context: .
secrets: [npm_token]
secrets:
npm_token:
environment: NPM_TOKEN# Dockerfile (fragment)
RUN --mount=type=secret,id=npm_token \
NPM_TOKEN=$(cat /run/secrets/npm_token) npm ciDockflow hands the value to buildx (or Podman) through the build process's environment, never on a command line.
BuildKit does not take a secret's value into account for its cache. If a step must run again when the secret changes, use no_cache or change the step.
Secrets in a SOPS file
Instead of CI variables, an environment's secrets can live in git, in a SOPS file encrypted with age: secrets/production.sops.yaml for production.
Dockflow decrypts the file in memory. It reads it after CI variables and .env.dockflow, which win over it. The file is a regular sops file: either tool reads and writes it.
-
Create an age key, and keep the private part where
sopslooks for it:age-keygen -o ~/.config/sops/age/keys.txtDockflow tries the keys of
SOPS_AGE_KEY, and those of the fileSOPS_AGE_KEY_FILEnames. When that variable is not set, it reads$XDG_CONFIG_HOME/sops/age/keys.txt, which defaults to~/.config/sops/age/keys.txt(~/Library/Application Support/sops/age/keys.txton macOS). -
Name the public key,
age1…, as the recipient of your SOPS files:.sops.yaml creation_rules: - path_regex: secrets/.*\.sops\.yaml$ age: age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p -
Set secrets. The value is read from stdin, or asked for without echo:
dockflow secrets set production DB_PASSWORD dockflow secrets list production -
In CI, give the private key as the repository's
SOPS_AGE_KEYsecret, instead of one secret per value:# .github/workflows/deploy.yml (fragment) - run: dockflow deploy env: PRODUCTION_CONNECTION: ${{ secrets.PRODUCTION_CONNECTION }} SOPS_AGE_KEY: ${{ secrets.SOPS_AGE_KEY }}
dockflow secrets edit productionopens every value in your editor.- A file is a flat map of
KEY: value. Keys ending with_unencryptedstay in clear, as withsops. - To give each person and the CI their own key, add several recipients.
Revoking a leaked secret
After a new value is deployed, the files of the old one stay on the server, so a rollback still works. When the old value leaked, remove them at once:
dockflow secrets revoke production db_passwordTheir copies are removed too, so the releases that used the old value can no longer be rolled back to.
Masking
The values of secrets and of secret files are masked in everything Dockflow shows or stores: events, build and hook output, errors. Interactive commands (exec, run, ssh) show the command's own output, unmasked.