Fly.io secrets and environment variables
Fly.io stores secrets in an encrypted vault and injects them as environment variables at runtime, never into builds. How that works, and where crunu run fits.
Part of the guide
Fly.ioAt a glance
- Env files, highest priority first
- No .env files: values come from the process environment or the platform.
- Public prefix
- None
No names are copied into browser code by a prefix.
- Values applied
- At runtime
Values are read from the environment when the app runs.
- Built-in secret store
- Fly secrets: an encrypted vault, injected into Machines as environment variables
- Crunu command
crunu run --env api/production -- /app/serverRecipeCrunu has no dedicated integration here: this is the general setup.
Common pitfalls
- fly secrets set restarts every Machine of the app and resets its ephemeral file system. Use --stage to defer the restart.
- App secrets and [env] values are not available during the Docker build. Build secrets are passed separately with --build-secret.
- A secret overrides an [env] entry with the same name in fly.toml.
- Anyone with deploy access can ship code that reads secrets and prints them to the logs.
- fly secrets list shows names and digests only, so Fly can't tell you what a secret's value is.
On this page
On Fly.io, an app's environment variables come from two places: secrets set with fly secrets set, stored encrypted and injected into every Machine of the app at runtime, and plain values in the [env] section of fly.toml. When both define a name, the secret wins. Neither reaches the Docker build. Setting a secret updates and restarts the app's Machines, so a running process always gets new values through a restart. These rules are from the Fly.io secrets documentation and the fly.toml reference, checked October 30, 2026.
Secrets, [env] and build arguments
| Mechanism | Set with | Available at runtime | Available to the build | Readable later |
|---|---|---|---|---|
| Secrets | fly secrets set | yes, as environment variables | no | no, names and digests only |
[env] in fly.toml | the config file | yes | no | yes, it's in your repository |
[build.args] and --build-arg | the config file or the CLI | no | yes | depends on the image |
| Build secrets | fly deploy --build-secret | no | only in the RUN step that mounts them | no |
[env] is for non-sensitive settings. Names can't start with FLY_, values must be strings, and the reference sends anything sensitive to the secrets command instead. Build arguments are the mirror image: they exist during the build and not in the running container, and the reference notes that application environment variables and secrets are likewise not available to builds.
What setting a secret does
fly secrets set NAME=value writes the value to the app's vault and then updates each Machine of the app. That update is a restart, and the documentation points out that it resets the Machine's ephemeral file system. Two options help when that's inconvenient:
--stagewrites the secret to the vault without touching Machines. Machines started or updated afterwards get it.fly secrets deploylater redeploys the current release with the staged secrets, so several changes land in one restart.
fly secrets unset removes values, fly secrets list shows names with a digest and a date, and the CLI can read secrets from stdin, which keeps values out of your shell history.
Who can read a Fly secret
Fly describes the path in some detail. Secrets are stored in an encrypted vault. The API servers can only encrypt, not decrypt. The Fly agent on the host that runs your Machine uses a token to decrypt the app's secrets and inject them into the Machine, and secret values are never logged. There is deliberately no command that reads a secret's plaintext back.
The documentation also states the limit honestly: people with deploy access can deploy code that reads secret values and prints them to logs. In other words, a secret is protected from casual reading, not from anyone who can change what runs. That's normal for platform secret stores, and it's the right mental model when you decide who gets deploy rights.
Secrets during the build
Because app secrets exist only at runtime, a build step that needs one, such as installing a private package, has to get it another way. The build secrets page shows the pattern: mount the secret in the Dockerfile with RUN --mount=type=secret,id=NAME, read it from /run/secrets/NAME inside that step, and pass the value with fly deploy --build-secret NAME=value. Don't use a build argument for this: build arguments can be recorded in the image, and whatever a build step writes into a layer stays there for anyone who pulls it.
Where crunu run fits on Fly.io
Crunu has no built-in sync into Fly secrets. The recommended setup keeps Fly secrets as the store your Machines read and makes Crunu the place where values are edited, shared and audited. Running crunu inside the image is a fallback for cases where that doesn't work.
Recommended: push values from Crunu into Fly secrets. Let a trusted pipeline or an admin's terminal update the Fly vault when something changes. Run the Fly CLI under crunu run and feed it the values through stdin rather than as arguments. Each update restarts the Machines, or you stage the changes and deploy once. The app then reads ordinary environment variables, and nothing about the image changes.
Fallback: run crunu inside the image. Store a single Fly secret, CRUNU_TOKEN, holding a Crunu service token that can read only the production environment. Make crunu run the container's command, so it decrypts the environment inside the Machine and starts your server with the values.
- Create a service token with access to
api/productiononly, without a network rule, and store it as the Fly secretCRUNU_TOKEN, passing the value through stdin rather than on the command line. PutCRUNU_SERVER, your Crunu server address, in[env]. - In the Dockerfile, install
crunuwith the install script's--user-installmode in a build stage, and copy the binary and CA certificates into the final image. - Set the command to
crunu run --env api/production --restart on-change -- /app/server. - Deploy with
fly deploy. Later value changes in Crunu restart only the server process inside the Machine, not the Machine.
Crunu documents service tokens for CI and short-lived environments, and recommends the agent on a machine you control for long-running servers. Before you use a token this way, accept three limits:
- Fly can decrypt this environment. Fly stores the token, its host agent decrypts it for the Machine, and the token decrypts the environment it was granted. For this environment, the promise that the server can't read your values no longer holds: the platform holding the token can.
- An expired lease stops the app from starting. The token's lease is a window of inactivity from 1 hour to 30 days, and every sign-in extends it. If no Machine starts the app for longer than the lease, the token is refused at the next start,
crunu runexits before your server starts, and the app stays down until an owner or admin resumes the token. Choose a lease longer than the longest time the app may stay stopped. - One token per environment, with the smallest grant and no network rule. Give each app and environment its own token that reads only that environment. Don't bind it to networks: the addresses a platform like Fly connects from can change between deploys, and a network rule would then lock the app out.
What you get in return: the individual values are no longer in Fly's vault, the token's page in Crunu shows which versions of that environment it received and when, and revoking the token cuts the app off at its next sign-in. A process that is already running keeps the values it has until it restarts. crunu run never passes CRUNU_TOKEN to your server process.
Which of the two to choose
Start with pushing into Fly secrets. The app, the image and the deploy stay as they are, Fly keeps doing what it's built for, and no Crunu credential is stored on Fly. It suits most teams that want one shared, audited place to edit values and a record of who changed what. Its cost is a Machine restart per update, and the values live in two vaults. In both setups Fly can decrypt the production values on the host that runs them; Crunu's blind server covers where values are edited and shared, not where Fly runs them.
Use the in-image fallback only when restarts per update are a real problem, for example with many secrets or frequent rotation. It keeps values in one place and lets a change reach the app without fly secrets set. Its cost is a second binary in the image, CA certificates in the final stage, a token that Fly can decrypt, and a lease you have to size for the longest pause.
Related reading
- Railway and Render for platforms that pass variables into builds.
- Docker Compose and Go for container specifics.
- Service tokens and crunu run.
- How to share .env files with your team.
Frequently asked questions
Something else? Write to support@crunu.com
Are Fly.io secrets available when my Docker image builds?
No. Fly's documentation says secrets are only available at runtime. For a build step that needs a secret, pass it with fly deploy --build-secret and mount it in the Dockerfile with RUN --mount=type=secret.
Who can decrypt Fly.io secrets?
Fly's API servers can only encrypt them. The Fly agent on the host that runs your Machine decrypts them and injects them. Your own code, and anyone who can deploy code, can read them as environment variables.
Does Crunu sync values into Fly secrets?
No, there's no built-in sync. Push values from a trusted pipeline with the Fly CLI, which can read secrets from stdin. As a fallback, run crunu inside the image so only a service token is stored in Fly, accepting that Fly can then decrypt that environment.
Can the crunu agent service run on a Fly Machine?
The agent service is a systemd unit for Linux hosts, and a Fly Machine runs your image's own process. The fallback is crunu run with a service token in CRUNU_TOKEN. Fly then holds a token that can decrypt that environment, and the token is refused if the app doesn't sign in for longer than its lease.
Sources
Every source this page relies on, with the date we last checked it.
- Fly.io Docs: Secrets · checked October 30, 2026
- Fly.io Docs: Build secrets · checked October 30, 2026
- Fly.io Docs: App configuration (fly.toml) · checked October 30, 2026