Why this pattern works
If you run self-hosted AI or automation services, the easiest setup is often the least safe: API keys in .env files, service tokens in client configs, and long-lived credentials copied into Docker stacks. That works until you need to rotate secrets, share access safely, or audit what is actually live. A better pattern is to keep only one bootstrap credential on disk and resolve the rest from 1Password at process start.
This approach works especially well for stacks like LiteLLM, LangGraph workers, automation runners, and Home Assistant bridges because those services already expect environment variables at startup. Instead of hardcoding values into local files, you keep secret references in your runtime templates and let 1Password inject the real values only when the service launches.
The design in one sentence
Store one service-account token locally, store all other secrets in 1Password, and launch services through wrappers that run op run --env-file=....
What stays on disk
Keep only the service-account token in a local file with tight permissions, for example:
OP_SERVICE_ACCOUNT_TOKEN=ops_...
Set file permissions so the token is not world-readable:
chmod 600 ~/runtime/service-account.env
That file becomes the root of trust for the host. Everything else should be resolved from 1Password.
What goes into 1Password
Create one vault for automation or infrastructure secrets and store your runtime credentials there. Typical items include:
- provider API keys for OpenAI, Anthropic, Gemini, MiniMax, or xAI
- database passwords
- internal API keys for worker services
- Home Assistant tokens or bridge tokens
- application encryption keys and basic-auth passwords
Use predictable item names so your wrappers and secret-reference files stay readable.
Use secret references instead of raw values
Instead of putting secrets directly in your stack files, create a small reference file that points to 1Password items:
OPENAI_API_KEY=op://HomeLab/litellm-openai/password
MINIMAX_API_KEY=op://HomeLab/litellm-minimax/password
LITELLM_MASTER_KEY=op://HomeLab/litellm-master-key/password
Those references are safe to keep on disk because they are not the secret values themselves.
Launch services through wrappers
Use a wrapper script to load the service-account token, establish a writable runtime directory, and execute the real startup command under op run.
#!/usr/bin/env bash
set -euo pipefail
set -a
source ~/runtime/service-account.env
set +a
export XDG_RUNTIME_DIR=/tmp/op-runtime
mkdir -p "$XDG_RUNTIME_DIR"
op run --env-file=/srv/litellm/secrets.1password.env -- \
docker compose --env-file stack.env up -d
This lets Docker Compose, worker processes, or MCP servers receive the resolved environment variables without ever storing them in plaintext project files.
Keep your normal env files non-secret
Your regular stack file should now contain only fixed configuration: ports, hostnames, model aliases, feature flags, and URLs. Secret values should not live there anymore. This makes the stack easier to version, safer to share internally, and simpler to audit later.
Validate the cutover
Before declaring success, validate in three layers:
- compose resolution: confirm the service sees injected values and not empty variables
- health checks: confirm each container still starts and answers normally
- runtime behavior: confirm the service can actually use the secret, not just read it
For an AI routing stack, that usually means checking the API gateway health endpoint, the worker health endpoint, and one live request through the routed model path.
One important hardening step
If you used a read-write service account for migration, do not leave that token on the host permanently. After the migration is done, create a read-only runtime service account limited to the vault that the host needs, replace the local token file, and remove the broader token.
Why this is better than plaintext env files
- secrets are no longer duplicated across stacks, clients, and helper scripts
- rotating a secret becomes a vault update instead of a repo-wide cleanup
- runtime configs can be committed without leaking credentials
- service startup stays simple because the apps still receive normal environment variables
Best fit
This pattern is a strong default for self-hosted AI gateways, internal workers, automation services, and Home Assistant-adjacent tooling. It keeps the operational model simple while raising the floor on secret hygiene in a meaningful way.