Supply settings from a secret store
Deliver TulipFarm settings as environment variables from Doppler, Infisical, 1Password, Vault, a cloud secret manager, Kubernetes, or a file.
TulipFarm does not include a client for any secret store. The application reads environment variables. Your delivery layer fetches values from the store and sets those variables. To change stores, change only the delivery layer.
secret store ──▶ delivery layer ──────────────────────▶ environment ──▶ TulipFarm Doppler doppler run -- docker compose up POSTGRES_PASSWORD=… Infisical infisical run -- docker compose up ANTHROPIC_API_KEY=… 1Password op run --env-file … -- docker compose up SOPS + KMS sops exec-env … 'docker compose up' Kubernetes External Secrets Operator → Secret → envFrom a file self-host/.env (chmod 600)Docker Compose reads interpolation variables
from the shell environment first, then from self-host/.env. So any command that
runs docker compose with variables already set works without changes. Use the
names in self-host/.env.example.
Each method below supplies the operator settings.
Generated settings stay in the generated volume. To keep them in your store too,
see Generated settings in a store.
A local file
Section titled “A local file”cp .env.example .envchmod 600 .envPut single quotes around values that contain $. Never commit .env. To keep the
file encrypted at rest, use SOPS with your KMS.
Doppler
Section titled “Doppler”Store the names from .env.example in a Doppler config. Then run Compose through the
Doppler CLI:
doppler setup --project tulipfarm --config prddoppler run -- docker compose up -d --buildOn a server without interactive login, set DOPPLER_TOKEN to a
service token that can read only this config.
To import an existing file: doppler secrets upload .env.
Infisical
Section titled “Infisical”Store the names in an Infisical environment. Then use the Infisical CLI:
infisical run --env=prod --expand=false -- docker compose up -d --build--expand=false stops Infisical from expanding ${…} references inside values.
To import an existing file: infisical secrets set --file .env --env=prod. On a
server, authenticate with a machine identity.
1Password
Section titled “1Password”Replace each value in a copy of .env.example with a
secret reference.
This file holds no secrets, so you can commit it:
# tulipfarm.op.envPOSTGRES_PASSWORD=op://TulipFarm/postgres/passwordANTHROPIC_API_KEY=op://TulipFarm/anthropic/credentialop run --env-file tulipfarm.op.env -- docker compose up -d --buildHashiCorp Vault and OpenBao
Section titled “HashiCorp Vault and OpenBao”Vault Agent renders a template to an env file. Point Compose at it:
template { destination = "/etc/tulipfarm/operator.env" perms = "0600" contents = <<-EOT {{ with secret "kv/data/tulipfarm" }}{{ range $k, $v := .Data.data }}{{ $k }}={{ $v }} {{ end }}{{ end }} EOT}docker compose --env-file /etc/tulipfarm/operator.env up -d --buildCloud secret managers
Section titled “Cloud secret managers”For AWS Secrets Manager, Google Secret Manager, or Azure Key Vault secrets, use the
platform’s runtime integration. Examples: ECS task secrets, Cloud Run
--set-secrets, or Container Apps secretref. Each one sets the same environment
variable names. On a single Compose host, either use SOPS with that cloud’s KMS,
or export values before you run Compose:
export ANTHROPIC_API_KEY="$(aws secretsmanager get-secret-value \ --secret-id tulipfarm/anthropic --query SecretString --output text)"Kubernetes
Section titled “Kubernetes”TulipFarm does not ship a Helm chart yet. Run the same images, and give each Deployment its variables from a Kubernetes Secret. The External Secrets Operator syncs that Secret from any supported store, such as AWS, GCP, Azure, Vault, Doppler, Infisical, or 1Password:
apiVersion: external-secrets.io/v1kind: ExternalSecretmetadata: { name: tulipfarm-api }spec: refreshInterval: 1h secretStoreRef: { kind: ClusterSecretStore, name: your-store } target: { name: tulipfarm-api } dataFrom: - extract: { key: tulipfarm/api }---# In the api Deployment's container spec:envFrom: - secretRef: { name: tulipfarm-api }Run the setup image (apps/database/Dockerfile) once as a Job against your database
and object storage. Then load each generated file into the store, as described in the
next section.
Generated settings in a store
Section titled “Generated settings in a store”pnpm self-host:setup --out DIR writes one env file per service. To manage them like
other secrets, upload each file, then delete the local copy:
doppler secrets upload --project tulipfarm --config prd_api generated/api.envinfisical secrets set --file generated/worker.env --env=prod --path=/workerKeep state.json as an encrypted backup. Setup needs it to rerun without changing
passwords or keys. See KMS: encrypt backups.
Rules for every store
Section titled “Rules for every store”- Give each service only its own variables. The worker does not need
BETTER_AUTH_SECRET. - Use read-only, machine-scoped credentials on servers.
- Restart the services after you change a value. TulipFarm reads settings at startup.
- Never pass secrets as command-line arguments. Process lists and shell history can expose them.
- To keep secrets out of the environment, mount each one as a file and set
<NAME>_FILEto its path. See Secret files.