An environment variable is a delivery mechanism, not encryption. If an application sends its value to the browser, writes it into a log or builds it into a public JavaScript file, the original .env permissions no longer protect that copy.
My study notes' distinction between encoded and protected information applies directly here. Base64, a hidden settings page and a name containing SECRET do not make a value confidential. This guide turns that idea into a deployment checklist for Node and Next.js.
Classify each setting before storing it
| Setting | Intended audience | Typical location |
|---|---|---|
| Public site name or analytics ID | Browser users | Explicit public build configuration |
| Database password | API runtime | Restricted server configuration or secret manager |
| Migration credentials | Controlled migration job | Separate privileged secret |
| Encryption key | Authorised server component | Key-management system or restricted runtime secret |
| Upload directory | Server runtime | Private configuration, outside public web roots |
Not all configuration is secret, but every secret needs an owner, purpose, rotation process and recovery plan. Keep a manifest of required variable names and formats without real values.
A value available during a build can be copied into generated output. A value needed at runtime may be unavailable to static generation. Decide which phase consumes it instead of providing every production secret to every build.
Browser variables are public by design
Next.js exposes specially prefixed variables to browser code. The environment-variable guide explains NEXT_PUBLIC_ inlining and build-time behaviour. Never put a database password, signing key or private API token under that prefix.
NEXT_PUBLIC_SITE_NAME=Demo Studio
DATABASE_HOST=127.0.0.1
DATABASE_NAME=demo_app
DATABASE_PASSWORD=SET_PRIVATELY_ON_THE_SERVERThis is a classification example, not a production-ready file. A browser bundle can be downloaded by anyone. Minification and an unadvertised URL are not access controls. Conversely, a variable without NEXT_PUBLIC_ can still leak if your own route returns it or passes it to a client component.
For same-origin browser requests, a relative /api path often avoids baking a server address into frontend code. The server still validates authentication and authorisation; hiding the API address is not a security boundary.
Keep replaceable releases separate from persistent state
srv/demo/
├── releases/
│ ├── release-a/
│ └── release-b/
├── current -> releases/release-b/
└── shared/
├── runtime.env # private, outside the web root
└── uploads/ # private application storageNeither shared directory belongs in public, a downloadable ZIP or a source-code product. Release transfers should use an explicit allowlist or carefully reviewed exclusions. The rsync verification sheet covers dry runs and destination checks.
Use a deployment account to own release code and a restricted runtime account to execute it. A common model is a root-owned configuration file readable by the runtime group, with no world access. Directory traversal permissions matter too: see Linux permissions for deployed apps.
stat -c '%A %U:%G %n' /srv/demo/shared/runtime.env
namei -l /srv/demo/shared/runtime.envDo not run cat on a production secret file just to prove it exists. Do not paste environment dumps, process arguments or complete systemd configurations into public support messages.
Load configuration explicitly
A systemd service does not run your interactive shell startup files. Its environment-file syntax is not a general shell script either. A minimal example is:
[Service]
User=demo
Group=demo
WorkingDirectory=/srv/demo/current
EnvironmentFile=/srv/demo/shared/runtime.env
ExecStart=/usr/bin/node server.mjsThis excerpt assumes those accounts, paths and the application already exist; it is not a complete service unit. Use the actual absolute Node path on your server. Read the systemd execution-environment reference and the service environment troubleshooting sheet.
Ordinary environment variables are not ideal for every high-value secret. Privileged users, the runtime itself, crash tooling or debugging access may reveal them. Where supported, consider systemd credentials or a dedicated secret manager, and make the application read secrets through that mechanism. File permissions do not protect against a fully compromised runtime that legitimately needs the value.
Fail early without logging the secret
Validate required settings at startup. Report the missing or malformed name, never its value. Check valid port ranges, allowed modes and required URLs. Do not substitute a production default password because a variable is missing.
export function requiredSetting(env, name) {
const value = env[name];
if (typeof value !== 'string' || value.length === 0) {
throw new Error('Missing required setting: ' + name);
}
return value;
}This helper only checks presence; it does not validate entropy, strength, scope or correctness. It deliberately does not trim passwords, because changing bytes can change the credential. Extend validation for each setting's actual contract. Tests should use synthetic values and confirm errors omit them.
The download contains the helper, two local test groups and a README. Run it without connecting to any server:
node --test config.test.mjsAvoid logging complete request headers, cookie strings or database connection options. Use a redacted diagnostic such as databaseConfigured: true, plus a separate bounded readiness check that does not return connection details.
Verify the boundary before release
In a disposable staging build, use a distinctive synthetic marker as a fake secret. Search the browser-served output and network responses for it. Do not use a real production secret as a test marker. Check source maps, downloadable archives, error pages and logs too.
A marker search is useful but not proof that every leak is impossible: values can be transformed or encoded. Review code paths and permissions alongside it. Confirm a missing runtime secret prevents startup, that public responses remain generic, and that old releases cannot read secrets they no longer require.
When rotating encryption keys, first plan how old data remains readable. Follow key rotation without losing records. When a secret has leaked, removing it from the latest file is not enough: revoke or rotate it and account for copies in history, backups and logs.
The snippets here are configuration patterns and a small pure function, not a claim that a particular production secret store was audited. Return to the VPS learning path to place these checks in a complete release process.
