8.4 KiB
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
- Fedora:
- 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 |
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:
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):
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:
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:
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:
[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
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.
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:
#!/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:
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):
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):
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:
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:
# 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
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
HttpOnlyandSameSite=Strict, and gain theSecureattribute automatically on HTTPS requests (detected viaX-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 stableadmin.session_keyso sessions survive restarts. - Keep
config.tomlandusers.tomlreadable only by thevolumenuser (chmod 600). Both may contain password hashes. volumen hash-passwordreads the password without echo; never pass passwords on the command line.