The Environment Variables a Container Host Can Detect for You, Across Seven Ecosystems
The most common reason a first deploy crashes is a missing environment variable: the secret key the settings file reads without a default, the database URL that was in a local .env, the API token the code assumes. SnapDeploy reads the repository before the first deploy and lists the variables it looks like the app needs, so you fill them in up front rather than after the crash. This is what it finds, what it cannot, and how to lay out a repository so the list is right the first time.
What the detection reads
- The Dockerfile.
ENVandARGlines name variables the image expects. Values given there are treated as defaults you may want to override. - A
.env.example(or.env.sample,.env.template). This is the best signal you can give: one line per variable, with a placeholder or an empty value. Placeholders such asyour_api_keyorxxxare recognised as "fill me in", not as real defaults. - Framework configuration. Spring Boot's
application.propertiesandapplication.ymlfor${VAR}placeholders; Django's settings foros.environ; Node and TypeScript sources forprocess.env.NAME, including the indirect forms many projects use; Go foros.Getenv; Laravel'senv()calls; Rails'ENV[]; .NET configuration. Seven ecosystems: Node.js (Express, Next.js, NestJS and plain), Python (Django, Flask, FastAPI), Java and Spring Boot, Go, PHP and Laravel, Ruby and Rails, .NET.
Each variable comes with where it was found, so a name that appears in the Dockerfile and in the code shows up once, not twice.
What it does with what it finds
- Secrets are marked. Names containing
KEY,SECRET,PASSWORD,TOKEN,CREDENTIAL,AUTH,PRIVATEorCERTare flagged as sensitive, so you know which values to treat as secrets and which are plain configuration. - Database URLs are recognised.
DATABASE_URL,POSTGRES_URL,MYSQL_URL,MONGODB_URI,REDIS_URL,RABBITMQ_URL,AMQP_URLand their common spellings are identified as connection strings. If you attach the matching add-on, the value is injected for you and the variable is satisfied without pasting anything. - Defaults are prefilled when the source had one (a Dockerfile
ENV PORT=3000, a.env.examplewith a real value), and left blank when the source was a placeholder.
What it cannot see
- Names built at runtime.
process.env["FEATURE_" + name]or a loop over a list of keys cannot be resolved by reading the code. List those in.env.example. - Values. It knows the app reads
STRIPE_SECRET_KEY; it has no idea what yours is, and it should not. - Variables read by a library rather than your code (an ORM reading
DATABASE_URLwithout you ever typing the name). The common ones are on the recognised list; unusual ones are not. - Runtimes it does not scan. Bun and Deno projects, Rust, Elixir and others are not read for variables; a
.env.examplecovers them completely.
Lay out the repository so the list is right
# .env.example (committed; .env is in .gitignore)
DATABASE_URL=
SECRET_KEY=change-me
ALLOWED_HOSTS=localhost
STRIPE_SECRET_KEY=your_stripe_key
SMTP_HOST=smtp.example.com
SMTP_PORT=587
LOG_LEVEL=info- One variable per line, real defaults where a default is safe (
SMTP_PORT,LOG_LEVEL), placeholders where it is not. - Keep
.envout of Git. If it was ever committed, rotate everything in it; deleting the file does not delete the history. - Use the conventional names.
DATABASE_URLis understood by Django (throughdj-database-url), Rails, Prisma, SQLAlchemy, Laravel (DB_URL) and almost everything else, and it is what the add-ons inject. - Do not put a variable in the Dockerfile
ENVif it is a secret;ENVvalues are baked into the image and visible to anyone who can pull it. Secrets belong in the container's environment settings.
The flow before a first deploy
- Connect the repository. The detected variables appear as a list with a source and a sensitive flag for each.
- Fill in the values. For connection strings, attach the add-on instead of pasting; for secrets, paste and forget, the value is not shown again in full.
- Add anything the detection missed (the runtime-built names, library-read names). A variable you add here is saved with the container and injected on every start.
- Deploy. If the app still crashes on a missing variable, the runtime log names it in the last lines, and adding it and restarting is one round trip rather than a new build.
Build-time versus runtime variables
Front-end frameworks bake some variables into the JavaScript bundle at build time: NEXT_PUBLIC_*, VITE_*, REACT_APP_*. Those must be present when the image is built, they cannot be changed without a rebuild, and they are visible to every browser, so they are configuration, never secrets. Everything else is read when the process starts and can be changed in the container settings followed by a restart. The detection lists both kinds; it is on you to keep secrets on the runtime side.
Injected variables you did not set
PORT: the port the container should listen on. Honour it.DATABASE_URLfrom a PostgreSQL, MySQL or MariaDB add-on;MONGODB_URIfrom MongoDB;REDIS_URLfrom Redis; the RabbitMQ add-on injects its AMQP URL. These override anything you typed under the same name.
Everything about the container's environment is on one screen, and the full reference is in the environment variables docs.
Rotating a secret later
A runtime variable is changed in the container settings and picked up on the next restart; no rebuild, no deploy from the allowance. A build-time variable (the NEXT_PUBLIC_* and VITE_* kind) needs a rebuild because its value is inside the bundle, which is one more reason never to put a secret there. Rotate in this order: create the new key at the provider, set it on the container, restart, confirm the app works, then revoke the old key at the provider. Done the other way round there is a window where nothing works.
Related: our guide to where container secrets actually leak covers the habits on the other side of this: keeping the values safe once they are set. Free tier: 4 containers, 512 MB, 0.25 vCPU, 100 container-hours a month, no card (free tier docs).