
1Password for Docker Compose: Protect Secret Zero
Use 1Password with Docker Compose without hiding the hard part: protect the service-account token, prefer file secrets, and test revocation and recovery.
Putting 1Password in front of Docker Compose removes plaintext application secrets from your repository. It does not remove secret zero. An unattended host still needs a credential that lets 1Password CLI fetch those secrets, usually OP_SERVICE_ACCOUNT_TOKEN.
The short answer
For a manual deployment, use your interactive 1Password session and avoid a service-account token on the server. For an unattended deployment, use a read-only service account limited to one deployment vault, protect its token as a systemd encrypted credential, and let a small systemd unit expose it only to the deployment process.
Use Compose secrets when the image can read a file such as /run/secrets/db_password. If an image accepts only an environment variable, 1Password can keep the value out of Git and off a plaintext .env file, but Docker will still store that environment value in the container configuration. Those are different security properties.
Choose the deployment mode before writing YAML
There are three sensible starting points.
- Interactive: a person runs the deployment from an authenticated 1Password CLI session. This is simplest for a small server that changes rarely, but it cannot recover unattended after a reboot unless the containers already have everything they need.
- Service account: the host authenticates with a scoped bearer token. This fits scheduled or unattended deployment, but the token becomes a long-lived credential that must be protected, rotated, and revocable.
- 1Password Connect: you operate a Connect server and give clients Connect tokens. It can fit an existing Kubernetes or internal API design, but it adds another service, availability requirement, upgrade path, and token boundary.
1Password documents that each service account uses a token supplied as an environment variable, and that vault and Environment access is selected for that account. It also says the token is shown only once and should not be stored in plaintext. Read the current service-account setup documentation before creating one.
For one Docker host, a narrowly scoped service account is usually the middle path. Connect becomes more attractive when several workloads already need a network API, local caching, or an integration that specifically expects Connect. It is not automatically safer just because it is more infrastructure.
Build a vault boundary, not a convenient super-token
Create a dedicated vault for the stack. Give the service account read access only. Do not reuse a personal vault, a broad homelab vault, or the same service account for unrelated machines.
- Name the account after the host and purpose, such as media-01-compose-read.
- Grant access only to the deployment vault.
- Use separate service accounts for machines with different consequences.
- Set an expiry when your operating model can renew it reliably.
- Record the owner, vault, host, encrypted-token path, creation date, and revocation procedure.
A copied service-account token can act without biometric approval. Vault scope is therefore the first blast-radius control. A read-only token for one small vault is still sensitive, but it is a different incident from a token that can read every team secret.
Keep secret references in Git
The .env file committed beside the Compose file should contain 1Password references, not resolved values.
DB_PASSWORD=op://media-production/postgres/password
API_TOKEN=op://media-production/web-api/tokenThe op run reference says the command scans environment variables and files for secret references, resolves them, and gives the values to its subprocess for that process lifetime. Output masking is enabled by default.
op run --env-file=./secrets.env -- docker compose up -dDo not add --no-masking to an unattended deployment. Masking is useful log hygiene, though it is not an authorization boundary. A child process can still read the values it receives.
Prefer a file inside the container
Docker's own guidance says not to pass sensitive values as ordinary container environment variables when the application can use Compose secrets. Compose can create a secret from a host environment value and mount it only into services explicitly granted access. See the current Compose secrets reference.
services:
database:
image: postgres:18
environment:
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
secrets:
- db_password
secrets:
db_password:
environment: DB_PASSWORDWhen this runs under op run, DB_PASSWORD exists in the Compose process environment. Compose provides the value to the container as the read-only /run/secrets/db_password file, and the application reads that file through POSTGRES_PASSWORD_FILE.
This does not make the Docker host harmless. A privileged host administrator or someone controlling the Docker daemon remains inside the trust boundary. It does keep the resolved database password out of the container's ordinary environment and grants the secret only to the named service.
Some images do not support a _FILE option or another file path. Then you may need this fallback:
services:
web:
image: example/web:1.2.3
environment:
API_TOKEN: ${API_TOKEN}Be honest about the trade-off. Docker documents that environment variables are stored as plaintext in container configuration and can be inspected through the remote API. 1Password has protected the value before deployment, but it cannot change how the target application and Docker consume it.
Protect secret zero with systemd credentials
Do not put OP_SERVICE_ACCOUNT_TOKEN in the Compose file, the reference file, shell history, a world-readable EnvironmentFile, or a command-line argument. On a current Linux host managed by systemd, an encrypted service credential is a useful option.
The systemd credential implementation can encrypt and authenticate a credential with a TPM2-derived key, a root-only host key, or both. The default uses both when a TPM2 device and persistent system storage are available. Check the systemd version and recovery behavior on the actual host before adopting it.
Create the encrypted credential from a protected interactive session. The placeholder command below deliberately does not include a real token:
sudo systemd-creds encrypt \
--name=op-service-account \
--with-key=auto \
- /etc/credstore.encrypted/op-service-accountA deployment unit can load the decrypted value into its private credentials directory:
[Unit]
Description=Deploy the media Compose stack
After=docker.service network-online.target
Requires=docker.service
[Service]
Type=oneshot
User=compose-deploy
Group=compose-deploy
WorkingDirectory=/srv/media
LoadCredentialEncrypted=op-service-account:/etc/credstore.encrypted/op-service-account
ExecStart=/usr/local/libexec/deploy-media-stackThe wrapper reads the credential, exports it for 1Password CLI, and replaces itself with the deployment process:
#!/bin/sh
set -eu
OP_SERVICE_ACCOUNT_TOKEN="$(cat "$CREDENTIALS_DIRECTORY/op-service-account")"
export OP_SERVICE_ACCOUNT_TOKEN
exec op run \
--env-file=/srv/media/secrets.env \
-- docker compose --project-directory /srv/media up -dThis narrows exposure. The encrypted blob at rest is not the bearer token, and systemd supplies the decrypted credential to the unit rather than every login shell. The token still exists in memory and in the deployment process environment while op runs. Root, the deployment user, a compromised deployment binary, or a sufficiently privileged debugger can still take it.
Do not give compose-deploy general sudo access. Access to the Docker daemon itself is highly privileged. Docker's security documentation treats the daemon and its socket as a major trust boundary, so adding a user to the docker group is not a small permission.
Test recovery before trusting the automation
- Deploy a disposable stack with a low-impact secret.
- Confirm no resolved value appears in Git, secrets.env, the unit file, shell history, or journal output.
- Inspect the created container. If the application uses an environment variable, confirm you understand that the value is present in its configuration.
- Reboot and confirm the encrypted credential can still be decrypted and the unit can run.
- Revoke the service account and confirm the next deployment fails closed.
- Create a replacement account, update the encrypted credential, deploy again, then remove the old account.
- Document a recovery route for a failed TPM, replaced motherboard, restored disk, or systemd upgrade.
A TPM-bound credential can make a stolen disk less useful, but it also couples recovery to the original machine and policy. Keep the 1Password-side record and a tested account-rotation procedure. Do not keep a plaintext backup of the service-account token beside the encrypted blob.
Watch the request budget
1Password applies hourly limits per service-account token and daily limits across the account. The current rate-limit table lists 1,000 reads per hour per token and 1,000 reads or writes per day per account for individual and Families accounts. Teams has a 5,000-request daily account limit, while Business is higher.
Resolve secrets during deployment, not on every application request. A restart loop that calls op for every container or health check can turn a small stack into an account-wide outage. Count the references and redeploy frequency, then test the failure you get when 1Password is unreachable or a limit is exhausted.
When Connect is the better choice
Choose Connect when you already have the operational capacity to run it and at least one of these is true:
- Several workloads need the Connect API or an official Connect integration.
- Network placement and local availability matter enough to own another service.
- The application needs behavior the service-account CLI path does not provide.
- You have monitoring, backups, upgrades, token rotation, and a recovery owner for Connect.
Do not add Connect solely to hide the service-account token on the same single host. Connect has its own credentials and availability boundary. Decide which machine may fetch which vault, where each token lives, and what happens when the 1Password service or your Connect server is unavailable.
A practical acceptance checklist
- Only references, never resolved secrets, are committed.
- The service account is read-only and limited to one deployment vault.
- The bearer token is encrypted at rest and absent from shell profiles and Compose files.
- The deployment process receives the token only while it runs.
- Images use /run/secrets files when they support them.
- Environment-only images are documented as leaving values in container configuration.
- Docker socket access is limited to the deployment boundary.
- Reboot, revocation, rotation, rate-limit, and offline failures have been tested.
The useful outcome is not a server with no secrets. That is impossible for an unattended workload that must authenticate somewhere. The goal is one narrow secret-zero credential, protected at rest, short-lived in process scope, easy to revoke, and unable to read unrelated vaults.
If that model fits your server, check the current 1Password options. This tracked link currently earns no commission.
Primary sources checked
1Password: Get started with Service Accounts
1Password: Service-account rate limits
Docker: Set environment variables in Compose
Docker: Compose secrets reference