Files
rezervovator/deploy/README.md
T

160 lines
10 KiB
Markdown
Raw Normal View History

# Running Rezervovator for many customers
The application serves one company per instance. To host many customers, every customer gets their own container from the same image, with their own database, configuration and data volume. This directory contains what is needed to run that on one Docker host:
- `rezervator.sh` — creates, imports, upgrades, backs up, restores and removes customers
- `docker-compose.yml` — the optional shared services: a reverse proxy (Traefik) and a PostgreSQL server
- `config.template.toml` — template of a customer's configuration
- `.env.example` — settings of the stack
The reverse proxy and PostgreSQL can each be either a container of this stack or the one already installed on the host:
| `.env` | Value | Meaning |
|---|---|---|
| `REZERVATOR_PROXY` | `traefik` (default) | Traefik container, routes by domain, issues Let's Encrypt certificates itself |
| | `nginx` | nginx installed on the host; the script generates a server block per customer |
| `REZERVATOR_POSTGRES` | `container` (default) | PostgreSQL container of this stack |
| | `host` | PostgreSQL installed on the host |
The two settings are independent. [Setup with nginx and PostgreSQL on the host](#setup-with-nginx-and-postgresql-on-the-host) below describes the combination for a server that already runs both.
## Requirements
- Docker with the Compose plugin, `bash` and `openssl` on the host
- The application image on the host: `docker build -t rezervator:latest .` in the repository root, or pulled from a registry. The image must match the CPU architecture of the host.
- A DNS record for every customer domain pointing to the host
## First setup
```sh
./rezervator.sh init # creates .env and stops
$EDITOR .env # proxy, PostgreSQL, image
$EDITOR config.template.toml # the [mailing] section: your SMTP server
./rezervator.sh init # starts the shared services, if any, and checks the setup
```
With `REZERVATOR_PROXY=traefik`, `REZERVATOR_TLS=true` gets a Let's Encrypt certificate for every customer domain and redirects HTTP to HTTPS; it needs `ACME_EMAIL` and ports 80 and 443 reachable from the internet. Use `false` only for local testing or behind another proxy that terminates TLS.
The session cookie is marked `Secure`. Whatever the setup, customers must reach the application over HTTPS, otherwise logging in works only on `localhost` addresses.
## Setup with nginx and PostgreSQL on the host
```ini
# .env
REZERVATOR_PROXY=nginx
REZERVATOR_POSTGRES=host
PG_ADMIN="sudo -u postgres"
```
Nothing from `docker-compose.yml` runs in this setup; `init` only creates the Docker network and checks that it can manage PostgreSQL and that containers can reach it.
Run the script as a user who may use Docker and `sudo -u postgres`.
### PostgreSQL
The script manages databases with `sudo -u postgres psql` and `pg_dump`. The customer containers connect over the Docker network to `host.docker.internal:5432`, which is the host's address on the Docker bridge, so PostgreSQL has to accept connections from there. A default installation listens on `localhost` only.
1. `postgresql.conf`: listen on the Docker bridge too. `listen_addresses = '*'` is the simple way; the next two steps keep it closed to everybody else. Check `max_connections` (see [Sizing](#sizing)).
2. `pg_hba.conf`: allow the Docker networks with password authentication. Every customer has a database and a role of the same name, so `sameuser` limits each role to its own database:
```text
host sameuser all 172.16.0.0/12 scram-sha-256
```
3. Firewall: allow port 5432 from `172.16.0.0/12` and keep it closed from outside.
4. Restart PostgreSQL and run `./rezervator.sh init`. It fails with a message when the containers cannot reach the database.
`172.16.0.0/12` covers Docker's default address pools; adjust it if your Docker uses other ranges. These steps were not tried on a Linux server yet — the script was tested against a PostgreSQL outside the stack reached through the host address, on Docker Desktop.
### nginx
Every customer container listens on `127.0.0.1` on its own port (`BASE_PORT` and up), not reachable from outside. `add` writes `customers/<name>/nginx.conf`, a plain HTTP server block that proxies the domain to that port; the commands to install it and to add a certificate with certbot are at the top of the file. Adapt it to how TLS is done on your server.
nginx's default limit for uploads is 1 MB; the generated block raises it to 20 MB for banner images.
### Moving an existing installation into a container
`import` creates a customer from an installation that runs directly on the host: it copies its database into a new one and the uploaded files from its data directory into a new volume. The accounts and passwords come along. The source is only read.
```sh
./rezervator.sh import main rezervace.example.com rezervator /srv/rezervator/target/site/data
```
The arguments are the new customer name, its domain, the name of the existing database and the directory with the uploaded banner (in old versions `target/site/data` next to the binary; leave it out if there is no banner). Config files in that directory are not copied.
Suggested order:
1. Copy the `[mailing]` section of the old `config.toml` into `config.template.toml`.
2. Try it: run `import`, check the new instance on its local port (`./rezervator.sh list` shows it), then `./rezervator.sh remove main --purge`.
3. For real: stop the old installation, run `import` again, and point the existing nginx server block of the domain to the new port (`proxy_pass http://127.0.0.1:<port>;`) — keeping its TLS settings is easier than installing the generated file. Reload nginx.
4. Keep the old database until the new instance has proven itself, then drop it.
Bookings made in the old installation between the import and the switch are not in the new one, hence stopping it first.
The new instance gets a new session key and a new database password, so everybody has to log in again. If the old installation kept its `config.toml` in `target/site/data`, that file was publicly downloadable: change the SMTP password too. `import` warns when the account `admin` still has the password `admin`.
## Adding a customer
```sh
./rezervator.sh add alpha rezervace.alpha.example
```
This creates the database and its role, writes `customers/alpha/config.toml` with a unique session key and database password, creates the data volume, starts the container and waits until it is healthy. At the end it prints the generated password of the `admin` account — it is shown only once.
An optional third argument sets the sender address of e-mails (default `noreply@<domain>`).
`./rezervator.sh list` shows all customers, their ports and the state of their containers.
## Upgrading
Build or pull the new image under the name set in `REZERVATOR_IMAGE`, then:
```sh
./rezervator.sh upgrade alpha # try one customer first
./rezervator.sh upgrade --all
```
Customers are upgraded one by one. For each one the script makes a backup, replaces the container (database migrations run when the new version starts) and waits for it to become healthy. Customers that already run the image are skipped, so an interrupted upgrade can simply be run again.
If a customer does not become healthy, the script puts back the database and data from the backup it just made, starts the previous image again and stops. The remaining customers are not touched. A customer is unavailable for a few seconds while its container is replaced.
## Backup and restore
```sh
./rezervator.sh backup --all
./rezervator.sh restore alpha 20260101T020000Z
```
A backup is two files in `backups/<name>/`: `<timestamp>-db.sql.gz` (database dump) and `<timestamp>-data.tar.gz` (uploaded banner and generated styles). `restore` replaces both the database and the data volume, and keeps the image the customer was running.
The script does not schedule backups, does not delete old ones and does not copy them off the host. Run `backup --all` from cron, and ship `backups/`, `customers/` and `.env` somewhere else — `customers/` and `.env` hold the passwords and session keys and cannot be regenerated.
## Removing a customer
```sh
./rezervator.sh remove alpha # stops the container, keeps everything else
./rezervator.sh remove alpha --purge # also deletes the database, data volume and config
```
After a plain `remove`, `upgrade alpha` starts the customer again. `--purge` asks for the customer name as confirmation and keeps existing backups. With nginx, remove the customer's server block yourself.
## Health and monitoring
Every instance answers `GET /health` with 200 when it can reach its database and with 503 otherwise. The image uses it as its Docker health check; `list` shows the result, and Traefik sends traffic only to healthy containers. Point an external monitor to `https://<domain>/health`.
## Sizing
- One idle instance used about 7 MiB of memory in a test, about 12 MiB after some use. PostgreSQL needs far more than that.
- Every instance may open up to `CUSTOMER_MAX_CONNECTIONS` database connections (5 by default; written into the customer's config when the customer is created) and closes idle ones. `max_connections` of PostgreSQL (100 in a default installation, `PG_MAX_CONNECTIONS` for the container) must stay above the number of customers times that limit, plus what else uses the server. Beyond that, put PgBouncer in front of PostgreSQL.
## Security notes
- `customers/` and `.env` contain secrets and are readable only by the user who ran the script. Keep them out of version control (they are in `.gitignore`).
- Customers share one PostgreSQL server but each has its own database and role, without access to the others.
- With Traefik: the proxy has read access to the Docker socket to discover containers, which is equivalent to root on the host. Do not expose its API or dashboard.
- With PostgreSQL on the host: the script runs `psql` as the PostgreSQL superuser, so whoever may run it controls all databases on that server, not only the customers'.
## Running a second stack
Change `REZERVATOR_PREFIX` (and the ports) in `.env` of another copy of this directory. All container, network and volume names are derived from the prefix.