Configure

cd deploy
cp ../.env.example .env
chmod 600 .env

.env.example documents every setting; Docker Compose passes the whole file to the hosts. Each __ in a name is a : in configuration (Email__Smtp__Host is Email:Smtp:Host).

What a first deployment must set

SettingWhat to put
APP_DOMAIN, IDENTITY_DOMAIN, API_DOMAINThe three host names
ACME_EMAILYour address, for Let's Encrypt's expiry notices
POSTGRES_PASSWORD, RABBITMQ_PASSWORDLong random values
OidcClientOptions__ClientSecretA long random value: the gateway's secret at the identity host
ApiIntrospection__SecretA long random value: the API's secret to check personal access tokens
Email__*Your SMTP provider — the hosts refuse to start in Production without it (see E-mail)
Captcha__SiteKey, Captcha__SecretKeyCloudflare Turnstile; without them the anonymous forms are unprotected
FileStorage__*Nothing on one server (files stay in the files volume); an S3-compatible bucket for more than one
DataProtection__Certificate__Password, DataProtection__RequireEncryption=trueSee below — before the first start
Email__Footer__*The privacy and terms links in every e-mail's footer
Billing__Catalog__*Your prices — before the first start

A long random value: openssl rand -base64 48 | tr -d '/+=' | cut -c1-48. Keep $ out of .env: Docker Compose reads it as a variable.

The Data Protection certificate

The three hosts keep their ASP.NET Core Data Protection keys in PostgreSQL. Those keys encrypt the session cookie, the e-mail confirmation and password-reset tokens, the identity host's signing keys, the webhooks' signing secrets and the organizations' own AI keys. The certificate encrypts the keys themselves, so a leaked database dump is not enough to forge a session or read a secret.

openssl req -x509 -newkey rsa:3072 -nodes -days 3650 \
  -subj "/CN=blouppy-data-protection" -keyout dp.key -out dp.crt
openssl pkcs12 -export -inkey dp.key -in dp.crt \
  -out secrets/data-protection.pfx -passout pass:<DataProtection__Certificate__Password>
shred -u dp.key dp.crt
sudo chown 1654:1654 secrets/data-protection.pfx   # the containers' non-root user
sudo chmod 400 secrets/data-protection.pfx

Then set DataProtection__RequireEncryption=true, so a lost setting stops the hosts instead of falling back to clear text.

Keep a copy of the .pfx and its password outside the server, with your database backups: without it, a restored database cannot be read. Rotating it never removes the old one — the new certificate goes in DataProtection__Certificate__*, the old one in DataProtection__PreviousCertificates__0__*, and every previous certificate is kept for good. deploy/README.md gives the full procedure.

The identity host's signing keys

Nothing to do by default: the identity host generates its token-signing and encryption keys on the first start and keeps them in the database, protected by Data Protection. To bring your own certificates, set ServerKeys__SigningCertificate__* and ServerKeys__EncryptionCertificate__*; .env.example explains the rotation.

Prices

The plan catalog is written from Billing__Catalog__* on every start: it is configuration, not sample data, and the defaults are placeholders. Set yours, create the matching prices in Stripe, and give their ids in Stripe__Prices__* — each must charge the same amount in the same currency per seat, or the checkout is refused. Without a Stripe key, billing is off: plans are shown and nothing can be bought. See Billing.

Next: start and operate.