Files
volumen/docs/deployment.md
T

328 lines
10 KiB
Markdown
Raw Normal View History

# Deployment
> Status: production deployment on Linux with systemd + nginx.
2026-07-26 18:15:29 +02:00
volumen runs as a single Python process (Uvicorn via `fastapi[standard]`) bound
to localhost, behind nginx which terminates TLS and reverse-proxies the API and
admin. The public website (e.g. a Vue SPA) is served separately and consumes
the JSON API.
The CLI binary is installed system-wide via `uv tool install` (or `pipx
install`); the *operational* bootstrap — config, data dirs, admin password,
optional systemd unit — is performed by the `volumen init` Python subcommand
that replaces the old `install.sh` bash script.
## Requirements
2026-07-26 18:15:29 +02:00
- **Python ≥ 3.14** (the bundled wheel ships its own interpreter when you use
`uv tool install` / `pipx install`; the system Python is only required if
you're building from source).
- Fedora: `sudo dnf install python3.14`
- Debian/Ubuntu: `sudo apt install python3.14 python3-venv python3-pip`
- **[`uv`](https://docs.astral.sh/uv/)** (recommended) — install with
`curl -LsSf https://astral.sh/uv/install.sh | sh`. Alternatively,
[`pipx`](https://pipx.pypa.io/) ships in most distro repos.
- **nginx** and a TLS certificate (e.g. via certbot) for the public site.
- A dedicated system user, `volumen`, created automatically by
`volumen init` when run as root.
2026-07-26 18:15:29 +02:00
## Install the CLI
```bash
# Pick one:
uv tool install volumen
# or
pipx install volumen
# Both put a self-contained `volumen` binary on PATH. Verify:
volumen version
```
For platforms where the wheel is not available (or for development), build it
from a source checkout:
```bash
git clone https://sourcedock.dev/petrbalvin/volumen.git
cd volumen
uv tool install .
```
## Bootstrap the deployment
### System install (root, systemd)
```bash
sudo volumen init
sudo systemctl status volumen
```
`volumen init` walks through:
1. Whether `config_path` already exists — if so, leaves it untouched unless
`--force` is passed (and even then it makes a timestamped `.bak.<ts>.toml`
first).
2. Creates `/etc/volumen/`, `/var/lib/volumen/posts` (and a `media/`
subdirectory), `/var/lib/volumen/users.toml`.
3. Creates the system user `volumen` if missing (`useradd --system …`).
4. Prompts for an admin password (`getpass`), hashes it via scrypt, writes the
`password_hash` into the rendered config.
5. Generates a fresh 64-byte session key (`secrets.token_hex(64)`) and writes
it into `[admin].session_key`.
6. Forces production-style settings: `host = "::1"`, `env = "production"`,
`trust_proxy = true`, `cookie_secure = true`.
7. With `--systemd` (default under root), writes
`/etc/systemd/system/volumen.service` and runs
`systemctl daemon-reload && systemctl enable --now volumen`.
Pass `--no-enable` to install the unit without starting it; pass `--force` to
back up and overwrite an existing config. The full flag list lives in
`volumen init --help`.
### Per-user install (no root)
```bash
volumen init --local
```
Per-user paths: `~/.config/volumen/config.toml`,
`~/.local/share/volumen/posts`, `~/.local/share/volumen/users.toml`. No
system user is created and no systemd unit is installed. `--systemd` is
rejected in this mode.
## Configuration locations
| Path | Purpose |
|------|---------|
2026-07-26 18:15:29 +02:00
| `/etc/volumen/config.toml` | configuration (rendered by `volumen init` on first run) |
| `/var/lib/volumen/posts` | Markdown posts |
| `/var/lib/volumen/posts/media` | uploaded images |
2026-07-26 18:15:29 +02:00
| `/var/lib/volumen/users.toml` | admin users (created from Settings) |
| `/etc/systemd/system/volumen.service` | systemd unit (only when `--systemd`) |
| `~/.config/volumen/config.toml` | per-user config (`--local`) |
| `~/.local/share/volumen/posts` | per-user data dir (`--local`) |
| `~/.local/share/volumen/users.toml` | per-user users file (`--local`) |
2026-07-26 18:15:29 +02:00
The config and users files are owned by `volumen:volumen` and `chmod 600` after
a system install — they may carry password hashes, so never make them
world-readable.
## systemd
2026-07-26 18:15:29 +02:00
`/etc/systemd/system/volumen.service` (rendered by `volumen init --systemd`):
```ini
[Unit]
Description=volumen blog engine
After=network.target
[Service]
2026-07-26 18:15:29 +02:00
Type=simple
User=volumen
Group=volumen
2026-07-26 18:15:29 +02:00
WorkingDirectory=/etc/volumen
Environment=PYTHONUNBUFFERED=1
ExecStart=/usr/local/bin/volumen serve --config /etc/volumen/config.toml --content /var/lib/volumen/posts
Restart=on-failure
RestartSec=2
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/var/lib/volumen
PrivateTmp=true
[Install]
WantedBy=multi-user.target
```
2026-07-26 18:15:29 +02:00
Hardening notes — `NoNewPrivileges`, `ProtectSystem=strict` (the entire
filesystem is read-only except for `ReadWritePaths`), `ProtectHome`
(`/home`, `/root`, `/run/user` are inaccessible), `PrivateTmp` (private
`/tmp`), and a two-second restart delay on failure.
Inspect health with:
```bash
sudo systemctl status volumen
journalctl -u volumen -f
2026-07-26 18:15:29 +02:00
volumen status # config / data dir / unit / service state
volumen status --json # machine-readable
```
## FreeBSD (rc.d)
2026-07-26 18:15:29 +02:00
volumen runs on FreeBSD too — it is plain CPython with no compiled extensions.
```sh
2026-07-26 18:15:29 +02:00
pkg install python3
# install uv (https://docs.astral.sh/uv/getting-started/installation/)
uv tool install volumen
sudo volumen init --local # no systemd on FreeBSD
```
Conventional FreeBSD paths: config in `/usr/local/etc/volumen/`, data in
2026-07-26 18:15:29 +02:00
`/var/db/volumen/`. Drop a tiny `rc.d` script in `/usr/local/etc/rc.d/volumen`
that wraps the `volumen` console script:
```sh
#!/bin/sh
#
# PROVIDE: volumen
# REQUIRE: NETWORKING
# KEYWORD: shutdown
. /etc/rc.subr
name="volumen"
rcvar="volumen_enable"
load_rc_config $name
: ${volumen_enable:="NO"}
: ${volumen_user:="volumen"}
: ${volumen_config:="/usr/local/etc/volumen/config.toml"}
: ${volumen_content:="/var/db/volumen/posts"}
pidfile="/var/run/${name}.pid"
command="/usr/sbin/daemon"
command_args="-f -r -P ${pidfile} -u ${volumen_user} \
2026-07-26 18:15:29 +02:00
/bin/sh -c 'exec volumen serve --config ${volumen_config} --content ${volumen_content}'"
run_rc_command "$1"
```
Enable and start:
```sh
2026-07-26 18:15:29 +02:00
chmod +x /usr/local/etc/rc.d/volumen
sysrc volumen_enable=YES
service volumen start
```
## nginx
volumen only owns `/api/volumen`, `/admin`, and `/media`. Serve your public
site from the same hostname and proxy those prefixes to the backend, so the
admin runs same-origin (session cookies and CSRF work without extra
configuration):
```nginx
server {
listen 443 ssl http2;
server_name blog.example.com;
ssl_certificate /etc/letsencrypt/live/blog.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/blog.example.com/privkey.pem;
# Public site (e.g. the built Vue SPA).
root /var/www/blog;
location / {
try_files $uri $uri/ /index.html;
}
# volumen API, admin and media.
location ~ ^/(api/volumen|admin|media)(/|$) {
proxy_pass http://127.0.0.1:9090;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
server {
listen 80;
server_name blog.example.com;
return 301 https://$host$request_uri;
}
```
### Hardening the admin
The admin lives at `/admin` on your public domain and is protected by login,
sessions, CSRF, and roles — but the engine has **no rate limit** on
`/admin/login`. Add brute-force throttling (and, optionally, an IP allowlist)
at nginx.
**1. A rate-limit zone** in the `http { }` context (e.g.
`/etc/nginx/conf.d/volumen.conf`):
```nginx
limit_req_zone $binary_remote_addr zone=volumen_login:10m rate=5r/m;
```
**2. A reusable proxy snippet** at `/etc/nginx/snippets/volumen-proxy.conf`:
```nginx
proxy_pass http://127.0.0.1:9090;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
```
**3. Split the single proxy location** in the `server { }` block into three —
public API/media, the throttled login, and the rest of the admin:
```nginx
# Public API and uploaded media — open to everyone.
location ~ ^/(api/volumen|media)(/|$) {
include snippets/volumen-proxy.conf;
}
# Login: throttle brute-force attempts. Only this endpoint is rate-limited,
# so the editor's live preview (/admin/preview) is unaffected.
location = /admin/login {
# Optional IP allowlist (uncomment to restrict):
# allow 203.0.113.0/24;
# deny all;
limit_req zone=volumen_login burst=10 nodelay;
include snippets/volumen-proxy.conf;
}
# The rest of the admin.
location ^~ /admin {
# Optional IP allowlist (must be repeated — locations do not inherit it):
# allow 203.0.113.0/24;
# deny all;
include snippets/volumen-proxy.conf;
}
```
nginx matches `= /admin/login` (exact) first, then `^~ /admin` (which stops
regex matching), then the `~` regex for API/media — so each path lands in the
right block. Apply with `sudo nginx -t && sudo systemctl reload nginx`.
## Updating
```bash
2026-07-26 18:15:29 +02:00
uv tool upgrade volumen
sudo systemctl restart volumen
2026-07-26 18:15:29 +02:00
volumen status # confirm: config_exists, password_hash_set, session_key_ok, …
```
2026-07-26 18:15:29 +02:00
The CLI binary is replaced in place by `uv tool upgrade`; the systemd unit's
`ExecStart` already points at the on-PATH `volumen` console script, so a
restart is all that's needed. The persistent state (`config.toml`,
`users.toml`, posts) is untouched.
If you used the legacy `install.sh` workflow (no longer documented, the script
was removed), you can migrate forward by deleting `/etc/systemd/system/volumen.service`
+ `/opt/volumen` and then running `uv tool install volumen && sudo volumen init`.
## Security notes
2026-07-26 18:15:29 +02:00
- Bind to `::1` (or `127.0.0.1`); only nginx is public. TLS is terminated by
nginx.
- Session cookies are `HttpOnly` and `SameSite=Strict`, and gain the `Secure`
attribute automatically on HTTPS requests (detected via `X-Forwarded-Proto`,
2026-07-26 18:15:29 +02:00
which the nginx snippet above sets, and enabled by
`[server].cookie_secure = true`, set automatically by `volumen init
--systemd`) — so the cookie never travels over plain HTTP in production,
while local HTTP testing still works. Combined with per-form CSRF tokens
this protects the admin. A stable `[admin].session_key` (also written by
`volumen init`) lets sessions survive restarts.
- Keep `config.toml` and `users.toml` readable only by the `volumen` user
2026-07-26 18:15:29 +02:00
(`chmod 600`). Both may contain password hashes. The installer enforces
this automatically.
- Use `volumen hash-password` to rotate; it reads the password without echo.
Never pass passwords on the command line.