diff --git a/Services/Website/.env.local b/Services/Website/.env.local index 92d31f5..750ae9d 100644 --- a/Services/Website/.env.local +++ b/Services/Website/.env.local @@ -49,10 +49,15 @@ DATABASE_NAME=loud # Provide the real password via the environment or a secret — never commit it. DATABASE_PASSWORD=loud +# Maximum pooled connections per event loop, one loop per core — an 8-core host +# can open 8 times this, and every replica that many again. +DATABASE_POOL_MAX_PER_EVENT_LOOP=4 + # Port of the database to connect to. DATABASE_PORT=5432 -# TLS posture when connecting: off | prefer | require (use `require` in production). +# TLS posture when connecting: off | prefer | require. Plaintext is the usual +# choice over a private container network; use `require` across one you share. DATABASE_TLS=off # Username of the database to connect as. diff --git a/Services/Website/README.md b/Services/Website/README.md index 6346a19..8c99460 100644 --- a/Services/Website/README.md +++ b/Services/Website/README.md @@ -63,7 +63,7 @@ Configuration is read through [swift-configuration](https://github.com/apple/swi 5. Built-in defaults The two files play different roles: -- **`.env`** (git-ignored) holds your deployment values, and is the file the Makefile and Compose read for their `${VAR}` placeholders — it typically selects the PostgreSQL backend. +- **`.env`** (git-ignored) holds your deployment values — including the database password — and is the file the Makefile and Compose read for their `${VAR}` placeholders; it typically selects the PostgreSQL backend. Keep it out of version control and off shared machines: Compose passes its values to the container as environment variables, so anything in it is readable through `docker inspect` and by every process in the container. - **`.env.local`** (tracked) holds the local development overrides: in-memory database, `debug` logging. Sitting *above* `.env`, it keeps a direct launch (`swift run` or a debugger) on the local values even when `.env` points at a deployment. Compose never reads it, and the production image does not ship it. The Makefile `include`s `.env` and exports every value, so a target launched through `make` runs with the deployment configuration rather than the `.env.local` one: `make site-run` uses the backend `.env` selects, a bare `swift run Website` the in-memory one. And because a makefile assignment outranks an inherited environment variable, overriding a value for a single invocation takes a command-line variable *after* the target (`make site-mount DATABASE_DRIVER=postgres`) — an environment prefix is silently discarded. @@ -112,6 +112,8 @@ A dotted config key maps to an environment variable by upper-casing, splitting c | `database.tls` | `DATABASE_TLS` | `prefer` | TLS posture when connecting: `off`, `prefer`, or `require`. Ignored for `inMemory`. | | `database.pool.maxPerEventLoop` | `DATABASE_POOL_MAX_PER_EVENT_LOOP` | `4` | Maximum pooled connections per event loop. Ignored for `inMemory`. | +> **Connection budget:** the pool holds `database.pool.maxPerEventLoop` connections *per event loop*, and the event loop group runs one loop per core. An 8-core instance can therefore open 32, and each replica that many again — three replicas exhaust PostgreSQL's default `max_connections` of 100. Size this against the server's limit, not against the number alone. + See [Persistence](#persistence-1) below for the workflow. ### Paths @@ -169,7 +171,7 @@ make site-run # run locally with hot reload (hb watch) make site-mount # docker compose up --build --detach make site-unmount # docker compose down + remove the local image ``` -Unlike a direct `swift run`, these targets inherit the exported `.env` values (see [Configuration](#configuration)), so they run against whichever backend `.env` selects. `make help` lists every available target. +Unlike a direct `swift run`, these targets inherit the exported `.env` values (see [Configuration](#configuration)), so they run against whichever backend `.env` selects. The one exception is `DATABASE_HOST` under `make site-mount`: the local Compose override pins it to the `postgres` service name, since the `.env` value addresses the database from the host rather than from inside the container. `make help` lists every available target. ## Persistence The service persists data through Fluent and selects its backend at runtime with `database.driver`. @@ -287,8 +289,9 @@ The Makefile and Compose files read these from `.env` (or the environment). Prov | `HTTP_SERVER_NAME` | Runtime server name (default `LoudWebsite`). | | `SECURITY_STRICT_TRANSPORT_SECURITY` | HSTS header value (default `max-age=31536000; includeSubDomains`). | | `DATABASE_DRIVER` | `inMemory` or `postgres`. The production Compose file defaults it to `postgres`; the local override defaults back to the in-memory backend. | -| `DATABASE_HOST`, `DATABASE_PASSWORD` | **Mandatory** — the Compose files carry no default for either, since none can be correct: `localhost` inside the container is the container itself, and a blank password authenticates as nobody. Compose refuses to start without them rather than booting a website that serves 503s. Provide the password via a secret. | +| `DATABASE_HOST`, `DATABASE_PASSWORD` | **Mandatory** — the production Compose file carries no default for either, since none can be correct: `localhost` inside the container is the container itself, and a blank password authenticates as nobody. It refuses to start without them rather than booting a website that serves 503s. Both come from `.env`, which is git-ignored — never commit the password. Compose interpolates each file before merging, so both must be set for a local `docker compose up` too, even though the override pins the host. | | `DATABASE_PORT`, `DATABASE_NAME`, `DATABASE_USERNAME` | The rest of the PostgreSQL connection (when `DATABASE_DRIVER=postgres`); these do default (`5432`, `loud`, `loud`). | +| `DATABASE_POOL_MAX_PER_EVENT_LOOP` | Pooled connections per event loop (default `4`) — see the [connection budget](#persistence) before scaling out. | | `DATABASE_TLS` | TLS posture: `off`, `prefer`, or `require`. The production Compose file defaults it to `require`, which refuses a server offering no TLS; the local override defaults it to `off` for the plaintext development container. `prefer` continues in plaintext when the upgrade is stripped, handing over the password — so it is not a safe production posture. | Run the migrations against the production database once before (or during) rollout: `docker compose -f docker-compose.yml run --rm website --database-migrate`. diff --git a/Services/Website/docker-compose.yml b/Services/Website/docker-compose.yml index 98765aa..a1e8981 100644 --- a/Services/Website/docker-compose.yml +++ b/Services/Website/docker-compose.yml @@ -27,6 +27,7 @@ services: DATABASE_USERNAME: ${DATABASE_USERNAME:-loud} DATABASE_PASSWORD: ${DATABASE_PASSWORD:?DATABASE_PASSWORD is required} DATABASE_TLS: ${DATABASE_TLS:-require} + DATABASE_POOL_MAX_PER_EVENT_LOOP: ${DATABASE_POOL_MAX_PER_EVENT_LOOP:-4} healthcheck: test: ["CMD", "curl", "--fail", "--silent", "--show-error", "http://127.0.0.1:8080/health"] interval: 30s