# Deployment > Status: production deployment on Linux with systemd + nginx. volumen runs as a single Ruby process (Puma) 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. ## Requirements - **Ruby ≥ 3.3** with a C toolchain (Puma compiles a native extension). - Fedora: `sudo dnf install ruby ruby-devel @development-tools openssl-devel` - Debian/Ubuntu: `sudo apt install ruby-full build-essential libssl-dev` - **nginx** and a TLS certificate (e.g. via certbot). - A dedicated system user, e.g. `volumen`. ## Layout | Path | Purpose | |------|---------| | `/opt/volumen` | source checkout (or installed gem) | | `/etc/volumen/config.toml` | configuration (auto-generated on first run) | | `/var/lib/volumen/posts` | Markdown posts | | `/var/lib/volumen/users.toml` | admin users (created from Settings) | | `/var/lib/volumen/posts/media` | uploaded images | ```bash sudo useradd --system --home /var/lib/volumen --shell /usr/sbin/nologin volumen sudo mkdir -p /etc/volumen /var/lib/volumen/posts sudo chown -R volumen:volumen /var/lib/volumen ``` ## Install ### Quick install (Linux + systemd) From a cloned repository, run the installer as root. It installs the Ruby toolchain from the distro repositories on **Fedora** and **openEuler** (prints guidance on other distros), creates the `volumen` user and directories, runs `bundle install`, generates `config.toml`, and installs and enables the systemd unit: ```bash sudo git clone https://codeberg.org/petrbalvin/volumen.git /opt/volumen cd /opt/volumen sudo ./install.sh ``` The service is enabled but not started — finish by setting the admin password (printed steps), then `sudo systemctl start volumen`. The manual steps below describe what the script does. ### Manual install From a source checkout (reproducible, no publishing needed): ```bash sudo git clone https://codeberg.org/petrbalvin/volumen.git /opt/volumen cd /opt/volumen sudo bundle install --deployment ``` Alternatively build and install the gem (`just build` produces `pkg/volumen.gem`, then `gem install pkg/volumen.gem` puts `volumen` on the PATH). ## First run and the admin password The first `serve` writes a commented `config.toml` if it is missing: ```bash sudo -u volumen bundle exec exe/volumen serve \ --config /etc/volumen/config.toml \ --content /var/lib/volumen/posts # → "volumen: wrote a default config to /etc/volumen/config.toml; review it and restart." ``` Generate a password hash and a stable session secret, then edit the config: ```bash bundle exec exe/volumen hash-password # → scrypt$... openssl rand -hex 64 # → session_key sudo -u volumen $EDITOR /etc/volumen/config.toml ``` Set `host = "127.0.0.1"`, your `site.base_url`, the `admin.password_hash`, and a `admin.session_key` (so sessions survive restarts). You can now sign in as user `admin`; from the **Settings** page you can add more users (roles `admin` / `author`) and change passwords. From then on `users.toml` is the source of truth. ## systemd `/etc/systemd/system/volumen.service`: ```ini [Unit] Description=volumen blog engine After=network.target [Service] User=volumen Group=volumen WorkingDirectory=/opt/volumen ExecStart=/usr/bin/bundle exec exe/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 ``` ```bash sudo systemctl daemon-reload sudo systemctl enable --now volumen sudo systemctl status volumen journalctl -u volumen -f ``` ## FreeBSD (rc.d) volumen runs on FreeBSD too — it is plain CRuby plus gems; only Puma (and its `nio4r` dependency) build a native extension, which compiles with the base clang toolchain. ```sh pkg install ruby # then install volumen as above (git clone + bundle install) ``` Conventional FreeBSD paths: config in `/usr/local/etc/volumen/`, data in `/var/db/volumen/`. Service script `/usr/local/etc/rc.d/volumen`: ```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_dir:="/usr/local/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} \ /bin/sh -c 'cd ${volumen_dir} && exec bundle exec exe/volumen serve --config ${volumen_config} --content ${volumen_content}'" run_rc_command "$1" ``` Enable and start: ```sh 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 cd /opt/volumen sudo git pull sudo bundle install --deployment sudo systemctl restart volumen ``` ## Security notes - Bind to `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`, which the nginx snippet above sets) — 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. Set a stable `admin.session_key` so sessions survive restarts. - Keep `config.toml` and `users.toml` readable only by the `volumen` user (`chmod 600`). Both may contain password hashes. - `volumen hash-password` reads the password without echo; never pass passwords on the command line.