Give the server its settings
Everything about a self-hosted site that is not edited in the admin panel is set with environment variables: the site's address, the admin login, the database, where files are kept, how email is sent. The names are the same on every platform. This page is for whoever installs the server, and explains how to get those variables to it.
The server does not read .env by itself
Neither the server nor its helper scripts open a .env file. They read process.env and nothing else. If you write your settings in a file, something has to load that file into the environment before Node starts. Any of these does it:
| Where you run it | How the settings get in |
|---|---|
| Plain Node, by hand | node --env-file=.env server/launcher.js |
| systemd | EnvironmentFile=/opt/choirmaster/.env in the unit |
| Docker Compose | env_file: .env on the service (both Compose files in the source do this) |
docker run | --env-file .env |
| A shell | export NAME=value before npm start |
| A platform service (Fly.io, Railway, Render and the like) | The platform's own settings or secrets screen |
| Cloudflare Workers | [vars] in wrangler.toml and wrangler secret put |
The download has no .env file and no example of one. The settings every site needs has a template to copy.
Plain Node
npm start runs node server/launcher.js. The launcher starts the real server as a child process and hands it its whole environment, so loading the file into the launcher is enough:
cd /opt/choirmaster
node --env-file=.env server/launcher.js
--env-file is built into Node (the server needs Node 22.13 or newer in any case). A value may be wrapped in double quotes, which Node removes:
EMAIL_FROM="Harmony Community Choir <noreply@example.org>"
systemd
[Service]
WorkingDirectory=/opt/choirmaster
EnvironmentFile=/opt/choirmaster/.env
ExecStart=/usr/bin/node server/launcher.js
The full unit is in Run it as a systemd service.
Docker
docker run -p 8080:3001 --env-file .env -v choir-data:/app/data choir-manager
docker run --env-filedocker run --env-file does not remove quotes: EMAIL_FROM="Harmony <noreply@example.org>" arrives with the quotation marks as part of the value. Write the line without them there. Compose's env_file, Node's --env-file and systemd's EnvironmentFile all remove them.
In Compose, the environment: block of the service wins over env_file. Both Compose files in the source set NODE_ENV, PORT, the database and the storage that way, so those lines in your .env are ignored there. See Run it in Docker.
Cloudflare Workers
A Worker has no environment file. Values that are not secret go in wrangler.toml:
[vars]
NODE_ENV = "production"
SITE_URL = "https://choir.example.org"
EMAIL_PROVIDER = "resend"
EMAIL_FROM = "Harmony Community Choir <noreply@example.org>"
Secrets are stored encrypted with Wrangler and reach the Worker the same way as a variable:
npx wrangler secret put ADMIN_USERNAME
npx wrangler secret put ADMIN_PASSWORD
npx wrangler secret put RESEND_API_KEY
[vars] take effect at the next npx wrangler deploy. The database, the two file buckets and the session store are not variables on Cloudflare but bindings; see wrangler.toml explained.
The helper scripts need the same settings
The scripts in scripts/ read the environment exactly as the server does, so a script run without your settings works on the defaults: a new, empty SQLite file in ./data, not your database. Give them the same file:
node --env-file=.env scripts/check-config.js
node --env-file=.env scripts/add-admin.js conductor@example.org "Alex Example"
node --env-file=.env scripts/migrate.js
npm run config:check, npm run admin:add and npm run db:migrate are the same scripts without the file, which is right when the variables are already in the environment. That is the case inside a container:
docker compose exec app npm run config:check
docker compose exec app npm run admin:add -- conductor@example.org "Alex Example"
On Cloudflare the scripts are not used at all. They open a database from Node, and a Worker's database is reached with wrangler d1 execute instead.
How values are read
| Kind of setting | How the value is read |
|---|---|
On or off (DATABASE_SSL, DB_AUTO_MIGRATE, SMTP_SECURE, S3_FORCE_PATH_STYLE) | 1, true, yes and on mean on, in any mix of capitals. Anything else means off. Empty or unset gives the default. |
Whole number (PORT, the session lengths, the upload limits, EMAIL_BATCH_SIZE) | Read as a whole number: 1.5 is read as 1. Something that is not a number gives the default, with no warning. |
Address (SITE_URL, PUBLIC_FILES_URL, S3_ENDPOINT, UPDATE_URL) | Slashes at the end are removed. |
List (ALLOWED_ORIGINS) | Split at commas; spaces around each item are removed. |
A choice from a list (ADMIN_AUTH, UPDATE_MODE, UPDATE_CHANNEL, SECRETS_PROVIDER) | Capitals and spaces around the value do not matter. |
A choice from a list (DB_PROVIDER, STORAGE_PROVIDER, SESSION_STORE, EMAIL_PROVIDER, CAPTCHA_PROVIDER) | Must be written exactly, in lower case. Postgres is not postgres. |
A variable set to an empty value counts as not set for the on-or-off settings, and for the purposes of NAME_FILE and a secrets manager. For the other settings an empty value is not the same as leaving the line out: DB_PROVIDER= is read as an empty name and stops the server, and CAPTCHA_PROVIDER= is not read as none. Delete the line, or put a # in front of it, rather than leaving it empty. (The email settings and ADMIN_AUTH do fall back to their defaults when empty.)
PORT and DATA_DIR must be real environment variables
The launcher reads PORT and DATA_DIR itself, before the server starts and before any secret is loaded. It uses PORT to check that a newly installed version answers, and DATA_DIR to find downloaded versions. A value that arrives only through PORT_FILE, DATA_DIR_FILE or a secrets manager reaches the server but not the launcher, and the two then disagree. Set these two in the environment itself.
Check what the server sees
node --env-file=.env scripts/check-config.js
It prints the configuration the server would run with, with secrets hidden, and lists anything wrong. Check your configuration explains the output.
Every variable, with its default, is listed in Environment variables.