Deploying on Debian
Directory layout used in this tutorial:
/home/raft/raft: checkout of Raft git./home/raft/production.env: sensitive configuration, readable only by root./home/raft/container/production/compose.yml: a minimal docker compose file for running the necessary service dependencies. In a real production environment, you would have better way to host these./etc/nginx/sites-enabled/example.org: nginx configuration for raft./etc/systemd/system/raft.service: systemd service definition for web app (puma)/etc/systemd/system/raft-job.service: systemd service definition for background job processing (good_job)
Install Requirements
Install ruby build requirements:
apt update && apt dist-upgrade
apt install build-essential autoconf libssl-dev libyaml-dev zlib1g-dev libffi-dev libgmp-dev rustc
apt install rbenv ruby-build
Install docker. Sadly, the packaged version is pretty out of date.
curl -fsSL https://get.docker.com -o get-docker.sh
sh get-docker.sh
Confirm it is working:
docker run hello-world
Install Raft requirements:
apt install git libpq-dev mupdf libvips libreoffice wkhtmltopdf
NOTE: mupdf, libvips, libreoffice, and wkhtmltopdf are all used to generate file previews. These are optional. wkhtmltopdf has unfixable security vulnerabilities, but for Raft it is only used to generate Markdown previews and is not ever given unsafe HTML.
Create user raft, add www-data to group raft:
useradd -s /usr/bin/bash -m raft
su -l raft <<'EOF'
echo 'eval "$(rbenv init -)"' >> ~/.bashrc
EOF
usermod -a -G raft www-data
chmod g+rx /home/raft
Install ruby as user raft:
rbenv install 3.4.1
rbenv global 3.4.1
ruby --version # ensure it says 3.4.1
Install Raft
As user raft:
git clone https://tangled.org/rewire.host/raft ~/raft
cd ~/raft
bundle
rake raft:precompile
Install Services
Generate necessary secrets:
As user raft:
cd ~/raft
rake env > ~/production.env
There are a many ways to run the necessary services needed for Raft. The file
container/production/compose.yml has a very minimal example using Docker
Compose. Despite the name, it is not suitable for real production but can get
you started.
As root:
cd /home/raft/raft/container/production
docker compose --env-file /home/raft/production.env up
Test that services came up:
curl "http://localhost:8002/9.x/initials/svg?seed=hi"
pg_isready -h localhost -p 5432 -d raft -U user
psql "postgresql://raft:$DB_PASSWORD@localhost:5432/raft" -c "SELECT 1"
Configure Raft
edit raft/config/config.yml:
production:
site_title: "Raft"
site_domain: domain.org
site_public_url: https://${site_domain}
site_private_url: http://host.docker.internal:3000
docserver_public_url: https://${site_domain}/docserver
docserver_private_url: http://localhost:8001
dicebear_url: https://${site_domain}/avatar
The value site_public_url cannot be changed once users have started to enroll
with the site, because webauthn authenticators will only work if the origin they
are bound to matches this url exactly.
Check your configuration for errors:
rake conf
Test that Raft can connect to database:
cd raft
bin/rails runner 'p ActiveRecord::Base.connection'
Create Database
Load Schema:
set -a && source /home/raft/production.env && set +a
RAILS_ENV=production rake db:schema:load
Confirm that it worked:
bin/rails runner 'p User.count'
Test Puma
set -a && source /home/raft/production.env && set +a
RAILS_ENV=production bundle exec puma -C config/puma.rb
Confirm that it worked:
curl http://localhost:3000
Install Nginx
apt install nginx
apt install snapd
snap install certbot --classic
Edit /etc/nginx/sites-enabled/example.org:
upstream raft {
server 127.0.0.1:3000;
}
upstream dicebear {
server 127.0.0.1:8002;
}
upstream docserver {
server 127.0.0.1:8001;
}
# define $connection_upgrade as either "upgrade" or "close"
# depending on the value of $http_upgrade header
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
server_name example.org;
root /home/raft/raft/public;
# never redirect to proxied urls
proxy_redirect off;
location / {
try_files $uri @app;
}
location @app {
proxy_pass http://raft;
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 https;
}
location ^~ /assets {
access_log off;
expires 30d;
add_header Cache-Control public;
add_header Last-Modified "";
add_header ETag "";
break;
}
location /avatar/ {
# note: trailing slash necessary to strip prefix before sending to proxy
proxy_pass http://dicebear/;
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 https;
}
location /cable {
proxy_pass http://raft;
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 https;
# WebSocket support
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_buffering off;
proxy_read_timeout 60s;
proxy_connect_timeout 60s;
}
location /docserver/ {
# note: trailing slash necessary to strip prefix before sending to proxy
proxy_pass http://docserver/;
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 https;
# tell docserver to prefix all its paths
#proxy_set_header X-Forwarded-Prefix /docserver;
proxy_set_header X-Forwarded-Host $host/docserver;
# WebSocket support
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_buffering off;
proxy_read_timeout 60s;
proxy_connect_timeout 60s;
}
error_page 500 502 503 504 /500.html;
#
# TLS
#
listen 443 ssl;
listen [::]:443 ssl ipv6only=on;
ssl_certificate /etc/letsencrypt/live/example.org/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.org/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
}
#
# redirect all http to https
#
server {
listen 80;
listen [::]:80;
server_name example.org;
if ($host = example.org) {
return 301 https://$host$request_uri;
}
return 404;
}
Restart nginx:
nginx -t # test configuration
systemctl restart nginx
systemctl status nginx
Systemd
Stop puma if it is running.
Change the environment file so that it can only be read by root:
chown root:root /home/raft/production.env
chmod 400 /home/raft/production.env
Make a backup copy of production.env. If you lose it, all data will be lost.
As root, create file /etc/systemd/system/raft.service:
[Unit]
Description=Raft Server (Puma)
After=network.target
[Service]
User=raft
Group=raft
Type=notify
WorkingDirectory=/home/raft/raft
Environment=RAILS_ENV=production
Environment=PATH=/home/raft/.rbenv/shims:/usr/bin:/bin
EnvironmentFile=/home/raft/production.env
ExecStart=/home/raft/raft/bin/bundle exec puma -C config/puma.rb
WatchdogSec=10s
Restart=always
StandardOutput=journal
StandardError=journal
SyslogIdentifier=raft
[Install]
WantedBy=multi-user.target
Create file /etc/systemd/system/raft-job.service
[Unit]
Description=Raft Background Job Processor
After=network.target
[Service]
User=raft
Group=raft
Type=notify
WorkingDirectory=/home/raft/raft
Environment=RAILS_ENV=production
Environment=MALLOC_ARENA_MAX=2
Environment=PATH=/home/raft/.rbenv/shims:/usr/bin:/bin
EnvironmentFile=/home/raft/production.env
ExecStart=/home/raft/raft/bin/bundle exec good_job start
WatchdogSec=5s
RestartSec=1s
Restart=always
StandardOutput=journal
StandardError=journal
SyslogIdentifier=raft-job
[Install]
WantedBy=multi-user.target
Check for errors:
systemd-analyze verify /etc/systemd/system/raft*.service
Enable raft.service:
systemctl daemon-reload
systemctl enable raft*
systemctl start raft*
systemctl status raft*
Check systemd logs for any errors:
journalctl -xeu raft*
Firewall
iptables -F DOCKER-USER
iptables -A DOCKER-USER -i docker0 -j RETURN # Allow internal docker bridge traffic
iptables -A DOCKER-USER -i lo -j RETURN # Allow loopback
iptables -A DOCKER-USER -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
iptables -A DOCKER-USER -i eth0 -p tcp --dport 22 -j ACCEPT # Replace eth0 with your external interface
iptables -A DOCKER-USER -i eth0 -p tcp --dport 80 -j ACCEPT
iptables -A DOCKER-USER -i eth0 -p tcp --dport 443 -j ACCEPT
iptables -A DOCKER-USER -i eth0 -j DROP
Persist firewall:
apt install iptables-persistent
netfilter-persistent save
Troubleshooting
Nginx
Permissions
id www-data # member of group raft?
chmod g+rwx /home/raft
# Ensure the directory and files have correct group permissions
find /home/raft/raft/public -type d -exec chmod 750 {} \;
# Files need read (r) permission for the group
find /home/raft/raft/public -type f -exec chmod 640 {} \;