Heroku calls environment variables config vars. Same thing, different name, and knowing the name matters because it is what the CLI commands and the dashboard use.
Check environment variables
From the CLI, inside your app's directory or with -a to name the app:
heroku config
heroku config -a my-app
That lists every config var and its value. To get one:
heroku config:get DATABASE_URL
To get them in .env format, which is useful for copying into local development:
heroku config -s
The -s flag (shell format) prints KEY=value lines instead of the aligned table.
In the dashboard: open the app, Settings, then Reveal Config Vars.
Set environment variables
heroku config:set STRIPE_SECRET_KEY=sk_live_abc123
Several at once:
heroku config:set NODE_ENV=production LOG_LEVEL=info
Every config:set restarts the app's dynos so the new value takes effect. If you are setting many, set them in one command to get one restart instead of ten.
Remove one:
heroku config:unset LOG_LEVEL
In the dashboard: Settings, Reveal Config Vars, then the pencil icon to edit or the field at the bottom to add. Dashboard edits also restart dynos.
Use them in your app
Nothing Heroku specific. Config vars are ordinary environment variables inside the dyno.
const stripeKey = process.env.STRIPE_SECRET_KEY;
import os
stripe_key = os.environ["STRIPE_SECRET_KEY"]
The one Heroku convention to know: PORT is set by Heroku and your web process must listen on it, not on a hardcoded port. Apps that ignore PORT fail to bind and get killed after the boot timeout.
Variables Heroku sets for you
Add-ons set their own config vars when provisioned. Heroku Postgres sets DATABASE_URL, Redis add-ons set REDIS_URL, and so on. Do not set these yourself; if the add-on rotates credentials, it updates the var and your manual value would be wrong.
DYNO, PORT and the app's HEROKU_* metadata vars are also injected at runtime.
Local development
Do not use heroku config:set for local values. Put them in a .env file and load it. The Heroku CLI's heroku local command reads .env automatically when it runs your Procfile:
heroku local web
If you are running the app directly rather than through heroku local, load the file yourself with dotenv or your framework's equivalent. If the format is new to you, what is a .env file covers it.
Add .env to .gitignore before the first push.
Staging and production
Each Heroku app has its own config vars. A pipeline with my-app-staging and my-app-production means two separate sets, and nothing keeps them in sync. Promoting a build through the pipeline moves the slug, not the config.
That is the correct design, since staging and production should have different database URLs and keys, but it means a new variable has to be added to both apps by hand, and it is easy to forget one.
Copy a value between apps:
heroku config:get API_KEY -a my-app-staging | xargs -I {} heroku config:set API_KEY={} -a my-app-production
Or diff them:
diff <(heroku config -s -a my-app-staging | sort) <(heroku config -s -a my-app-production | sort)
Review apps inherit config vars from the pipeline's review app settings, not from staging. Set defaults there under the pipeline's Settings tab, or they boot without your variables.
Config vars in CI
If your CI deploys to Heroku, it needs HEROKU_API_KEY as a secret in the CI system, and nothing else from Heroku. Config vars live on the Heroku side and the app reads them at runtime; CI never needs to see them.
The exception is build time variables for frontend bundlers. Heroku exposes config vars during the build, so VITE_API_URL set as a config var is available when npm run build runs on the dyno.
Limits and what Heroku does not do
- Config var values are limited to 32 KB each and the total across an app is capped. Certificates and large JSON blobs sometimes hit this.
- Values are stored encrypted at rest but shown in plaintext to anyone with access to the app. There is no per variable permission model.
- There is no history. Change a value and the old one is gone.
- There is no rotation, no approval flow, and no way to share a variable across apps except copying it.
For one app with two environments, none of that matters. For a team with several apps, a few environments each, and developers who need the same variables locally, the copying gets error prone.
That is the point at which a secrets manager makes sense: one source of truth, each environment and each developer pulls from it. Krypt, which we build, is one option with a flat team price; this comparison covers Doppler, Infisical and the rest. All of them push into Heroku config vars or wrap your process so the app reads from the manager directly.
FAQ
How do I see all environment variables on Heroku? heroku config in the CLI, or Settings, Reveal Config Vars in the dashboard.
Does setting a config var restart the app? Yes. Every heroku config:set or dashboard edit restarts the dynos. Batch multiple changes into one command.
Why does my app crash with a port error on Heroku? It is not listening on process.env.PORT. Heroku assigns the port; hardcoded ports fail to bind.
How do I copy config vars from staging to production? heroku config -s -a staging-app prints them in KEY=value form; pipe individual values into heroku config:set -a production-app. There is no built in copy all command.
Can I use a .env file on Heroku? Not for deployed apps; config vars are the mechanism. .env is for local development with heroku local.
Are Heroku config vars secure? Encrypted at rest and only visible to collaborators on the app. But anyone with app access sees every value in plaintext, and there is no audit trail of changes.