Where they live
Each site has its own system user and its own folder, ~/public_html by default. Put a file called .env there, next to package.json or artisan:
# ~/public_html/.env
DATABASE_URL="postgresql://app:[email protected]/app"
STRIPE_SECRET_KEY="sk_live_..."
NEXT_PUBLIC_SITE_URL="https://example.com"
Create it over SSH as the site’s user, or with the File Manager in the site’s settings. Keep it out of git by adding it to .gitignore: the file belongs to the server, and the repository shouldn’t know its values.
There’s no separate “production” and “preview” set, as there is on Vercel. A site has one .env, and a staging site is a separate site with its own (see staging below).
Why they survive deploys
A Git deploy doesn’t start from an empty folder. Cito copies the live site, pulls the new commit into the copy and swaps it in, so anything that isn’t in git, .env included, comes along. You set a value once and it stays until you change it.
Deploys through the Cito MCP server work the other way round: they upload your project from your machine, and they leave out any file called .env so your local values can’t overwrite the server’s.
Next.js
Next.js reads .env itself, when it builds and when the app starts, so there’s nothing to add to app.js. What changes is when each kind of value takes effect.
-
NEXT_PUBLIC_values are fixed at build time. Next.js writes them into the JavaScript it sends to the browser. Change one and nothing happens until the next build. -
Other values are read when the app starts. Change one, then restart the app:
passenger-config restart-app ~/public_html -
Keep
.env.localoff the server. Next.js reads.env.localand.env.production.localahead of.env. The MCP deploy only leaves out a file called exactly.env, so a.env.localin the folder you deploy from would be uploaded and quietly override the server’s values. Keep local files out of that folder, or out of the deploy.
Other Node apps
Node 24 can load a .env file without any packages. Do it at the top of app.js, before anything reads process.env:
process.loadEnvFile('.env');
If the app already uses dotenv, that works too. Either way, a changed value takes effect when the app restarts.
Laravel and other PHP apps
Laravel reads .env as usual. If you cache the config in production, Laravel stops reading .env until you rebuild the cache, so after changing a value run one of these as the site’s user:
php artisan config:clear # read .env directly again
php artisan config:cache # or rebuild the cache with the new value
Staging and live
When you clone a site to make a staging copy, the clone takes the live site’s files, and that includes its .env. Cito can clone the database too, with a new database user, and its auto-reconfigure option updates some apps’ config files to match. Don’t rely on it for .env: check it yourself, because a clone that still holds the live settings talks to live services.
Before you use a staging copy, open its .env and change:
- the database connection, to the cloned database Cito created;
- API keys, to test keys where the provider has them (Stripe’s
sk_test_keys, for example); - anything that sends email, texts or webhooks to real people.
Otherwise a test on staging can charge a real card or email a real customer.
Keeping them safe
Make the file readable by the site’s own user only:
chmod 600 ~/public_html/.env
If a key has ever been committed to git, deleting the file isn’t enough: it’s still in the repository’s history, and anyone with access to the repository can read it. Roll the key with the provider first, then clean up. The readiness audit checks your history for exactly this.