bottleneckcrm/docs/self-hosting/environment-variables.md

89 lines
6 KiB
Markdown

# Environment variables
`backend/crm/settings.py` is read once, at process import time, by every Django entry point:
`manage.py runserver`, `manage.py migrate`, a Gunicorn/Uvicorn worker, a Celery worker, a management
command. This page explains where those values come from and which ones you actually have to set
yourself; the exhaustive per-variable table lives in the Reference section.
## How configuration is loaded
`backend/crm/settings.py` calls `load_dotenv()` (from `python-dotenv`) near the top of the module,
before any setting is read, so a `.env` file next to `manage.py`, created by copying
`backend/.env.example`, per [Manual setup](../getting-started/manual-setup.md), is loaded into the
process environment automatically. `docker-compose.yml` does the equivalent for containers by
listing `.env.docker` and then the optional, gitignored `.env.docker.local` under each service's
`env_file:`, in that order, so values in `.env.docker.local` win.
Almost every setting in `crm/settings.py` follows the same pattern: `os.environ.get("NAME",
<default>)`, so an unset variable falls back to a working (usually dev-oriented) default rather than
failing. `SECRET_KEY`, `DEBUG`, `ALLOWED_HOSTS`, `DBNAME`/`DBUSER`/`DBPASSWORD`/`DBHOST`/`DBPORT`,
`EMAIL_BACKEND`, `CELERY_BROKER_URL`/`CELERY_RESULT_BACKEND`, `GOOGLE_CLIENT_ID`/`GOOGLE_CLIENT_SECRET`,
`DOMAIN_NAME` and `FRONTEND_URL` are all read this way. A
smaller set of variables has no default at all: `backend/crm/server_settings.py` (imported only when
`ENV_TYPE=prod`, see [Production deployment](production-deploy.md)) reads `AWS_BUCKET_NAME`,
`AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `AWS_SES_REGION_NAME`, `AWS_SES_REGION_ENDPOINT` and
`SENTRY_DSN` with plain `os.environ[...]`. A missing one is a `KeyError` at import time, which means
the process refuses to start at all rather than starting with something half-configured.
Because all of this runs at import time, not per-request, a changed environment variable only takes
effect the next time the process restarts: editing `.env` while `runserver` is already running does
nothing until you restart it (or, for the Docker Compose setup, until the container restarts, which
happens automatically on `docker compose up --build`).
## The minimum for a working install
The values in `.env.example` and `.env.docker` are enough to bring the stack up and use it locally;
you do not need to touch anything else to follow [Docker quick start](../getting-started/docker-quick-start.md)
or [Manual setup](../getting-started/manual-setup.md). Before anyone other than you can reach the
instance, the variables that actually need a real value are: `SECRET_KEY` (a unique secret, not the
shipped `django-insecure-...` placeholder), `DEBUG=False`, `ALLOWED_HOSTS` (your real hostname(s)),
`CSRF_TRUSTED_ORIGINS` (each entry needs a scheme, e.g. `https://crm.example.com`), `DBUSER` pointed
at a non-superuser PostgreSQL role, and `CELERY_BROKER_URL`/`CELERY_RESULT_BACKEND` pointed at a
Redis instance you control. [Production deployment](production-deploy.md#required-settings) covers
the full list and exactly what each one guards against; [PostgreSQL and RLS](postgresql-and-rls.md)
covers why the database role matters as much as any of the Django settings.
## Database
`DATABASES["default"]` in `crm/settings.py` reads `DBNAME` (default `crm_db`), `DBUSER` (default
`postgres`), `DBPASSWORD` (default `postgres`), `DBHOST` (default `localhost`) and `DBPORT` (default
`5432`). The default `DBUSER` is the PostgreSQL superuser account on most installations, and a
superuser silently bypasses every Row-Level Security policy this project relies on for tenant
isolation. See [PostgreSQL and RLS](postgresql-and-rls.md) for how to create and verify a
non-superuser role before pointing these variables at anything but a local, throwaway database.
## Email
`EMAIL_BACKEND` defaults to `django.core.mail.backends.console.EmailBackend`, so outbound mail
(welcome emails, magic-link sign-in emails, comment-mention notifications) prints to stdout instead
of being sent anywhere. `.env.example` sets `EMAIL_BACKEND` to that same console backend explicitly;
`.env.docker` doesn't set `EMAIL_BACKEND` at all, only a comment noting the console backend is
what's active, so it's relying on the identical default rather than setting it. `DEFAULT_FROM_EMAIL`
and `ADMIN_EMAIL` default to `noreply@localhost`/`admin@localhost`. Switching to a real mail backend,
and the Amazon SES-specific variables that come with it, is covered in full in
[Email and Celery](email-and-celery.md).
## Integrations
`GOOGLE_CLIENT_ID` and `GOOGLE_CLIENT_SECRET` both default to empty strings in `crm/settings.py`,
and `.env.docker` and `.env.example` ship both blank. Google sign-in stays off until they are set, on
both the backend and the SvelteKit frontend. There is no `GOOGLE_REDIRECT_URI` setting: the redirect
URI used in the token exchange is the one the frontend sends in the request body on each call, so a
setting of that name was read into Django and never referenced again. See
[Google OAuth](google-oauth.md) for what each one does and where to get the values.
`SENTRY_DSN` is read (with no default) only inside `server_settings.py`, i.e. only when
`ENV_TYPE=prod`; it's not consulted at all otherwise.
`CACHE_URL` is empty by default, which makes Django's default cache a per-process `LocMemCache`.
That is fine for `runserver` and wrong for anything with more than one worker, because DRF stores
its throttle counters there: with N Gunicorn workers the effective rate limit becomes roughly N
times the configured one and resets on every restart. The public [web form](../integrations/web-forms.md)
submit endpoint is the one thing today whose spam controls depend on it, so on any real deployment
point `CACHE_URL` at the Redis you already run for Celery (a separate database, e.g.
`redis://localhost:6379/1`). `WEBFORM_THROTTLE_IP` and `WEBFORM_THROTTLE_GLOBAL` set those limits
and both have working defaults.
## Full reference
Every variable this project reads, its default, and the file that reads it is tabulated in full in
the Reference section.