Raft Demo / Documentation

Deploying on Debian

  1. Deploying on Debian
    1. Install Requirements
    2. Install Raft
    3. Install Services
    4. Configure Raft
    5. Create Database
    6. Test Puma
    7. Install Nginx
    8. Systemd
    9. Firewall
    10. Troubleshooting
      1. Nginx

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 {} \;