← Back to Documentation

Deployment

Roe CMS supports Kamal deployments. This means that you can use Roe with many popular Ruby on Rails hosts. This document contains basic instructions for general deployment and step-by-step instructions for DigitalOcean.com and Fly.io.

DigitalOcean

DigitalOcean has an excellent step-by-step article that explains How To Set Up an Ubuntu Server on a DigitalOcean Droplet. I would go through this article and below, I will include any Roe specific recommendations and instructions.

Create an Account

  1. Go to DigitalOcean.com and click Menu → Products → Computer → Droplets
  2. Add your name, email and password.
  3. Signup

Create a Droplet

  1. Choose a datacenter region near you or your primary audience.
  2. Choose an image (Ubuntu recommended)
  3. Choose a Droplet Plan
    • Basic
    • Regular (if available in region. try Permium AMD or Premium Intel if Regular is not available.)
    • Lowest price possible for the size of your site (most sites should work with the 35GB plan.)
  4. You can skip “Add additional storage” unless you have a very large site with tons of content.
  5. Skip “Enable backups” unless you really need them. Roe takes care of backups for you.

Authentication

Add an SSH Key

If you’re unfamiliar with how to create an SSH key, this article with DigitalOcean goes through it step-by-step for all platforms: Create SSH Keys with OpenSSH on macOS, Linux, or Windows

When you create the SSH, paste this command into your terminal:

ssh-keygen -t ed25519 -C "roe-do-deploy" -f ~/.ssh/roe_do

You can skip adding a PASSPHRASE or add one if you like.

This will create 2 SSH files on your system:

  1. /Users/<username>/.ssh/roe_do
  2. /Users/<username>/.ssh/roe_do.pub

Never share roe_do, this is your private key. roe_do.pub is the part you need to add to DigitalOcean (or any platform).

You can add this SSH key to your ssh/config so Kamal can automatically pick the right key without you having to remember every time. Copy and paste the command below into your terminal:

Mac/Linux

Make sure to replace the DROPLET_IP with your Public IP from DigitalOcean for this droplet.

mkdir -p ~/.ssh && chmod 700 ~/.ssh
DROPLET_IP="100.200.100.200"
cat >> ~/.ssh/config << EOF

Host $DROPLET_IP
  IdentityFile ~/.ssh/roe_do
  User root
EOF
chmod 600 ~/.ssh/config

Windows/PowerShell:

$DropletIP = "100.200.100.200"
New-Item -ItemType Directory -Force -Path "$HOME\.ssh" | Out-Null
@"

Host $DropletIP
  IdentityFile ~/.ssh/roe_do
  User root
"@ | Add-Content -Path "$HOME\.ssh\config"

Copy the public key to your clipboard so you can paste it into DigitalOcean:

Mac

pbcopy < ~/.ssh/roe_do.pub

Linux

xclip -selection clipboard < ~/.ssh/roe_do.pub

Windows

cat ~/.ssh/roe_do.pub | clip

Paste it into DigitalOcean’s SSH key field when prompted.

Additional

Enable Improved Metrics and monitoring

Free, gives you CPU/memory/disk graphs in the DO control panel. Highly recommended on small droplets where running out of memory is a real failure mode.

Don’t enable Startup scripts

Kamal handles all the Roe-specific setup. Startup scripts add a layer that’s hard to debug if something goes wrong on first boot.


Connect to the Droplet

Once the droplet is created, you can sign in via SSH:

ssh root@100.200.100.200

Replace the numbers with your droplet’s public IP address. You should land in a shell prompt without being asked for a password — that’s because the ~/.ssh/config entry from the previous step tells SSH which key to use automatically.

Type exit to leave SSH at any time.

Prepare the Droplet

This is only recommended if you’re using a small droplet (1GB). Adding a 2GB swap file prevents the system from being overloaded. SSH into the droplet and paste this:

sudo swapon --show
sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
free -h

The last command should show Swap: 2.0Gi in the output. The swap file persists across reboots thanks to the /etc/fstab line.

This article has more info about this fix: Boosting Your DigitalOcean Droplet’s Memory: No Upgrade Needed When You’re Short on RAM

Set Up a Docker Registry

Kamal builds the Roe image locally and pushes it to a Docker registry. The droplet pulls from there to deploy. Docker Hub is the easiest option for getting started.

Create a Docker Hub Account

  1. Go to hub.docker.com and sign up.
  2. Verify your email.

Generate a Personal Access Token

Don’t use your account password — generate a dedicated token instead:

  1. Docker Hub → Account Settings → Personal Access Tokens → Generate New Token
  2. Description: roe-kamal-deploy
  3. Expiration: None (or pick the longest option offered)
  4. Permissions: Read, Write, Delete (Kamal needs all three)
  5. Copy the token immediately — Docker Hub only shows it once

You’ll paste this into .kamal/secrets shortly.

Configure Roe for Kamal

Roe needs three values to deploy. You set them once in the Roe admin UI; Roe writes the necessary config and secrets files for you.

Open the Deployment Settings

In your local Roe admin: Settings → Deployment.

Fill in the three required fields:

  1. Droplet IP address — the public IP from your DigitalOcean droplet.
  2. Docker Hub username — the username from your Docker Hub account.
  3. Docker Hub access token — the personal access token you generated earlier. (This one is treated as a secret — it’s stored encrypted and never displayed after save.)

Click Save. Roe generates two files behind the scenes:

  • config/deploy.yml — Kamal’s main configuration, populated with your droplet IP, Docker Hub username, volume paths, and the Roe-specific defaults (SQLite single-container deploys, asset paths, aliases).
  • .kamal/secrets — holds your Docker Hub access token and reads the Rails master key from config/master.key (which Rails created automatically when Roe was installed).

Both files are written to the right place and given the right permissions. You don’t need to open or edit either one directly.

Verify the Rails Master Key

Roe uses config/master.key to decrypt encrypted credentials at runtime. This file was created when Rails was installed and lives at the standard location. Confirm it’s there:

cat config/master.key

You should see a single 32-character hexadecimal string. Add the master.key value to your password manager — losing it means losing access to any encrypted credentials.

Optional: Build on the Droplet Instead of Your Laptop

By default, Roe builds your Docker image on the machine running Kamal (your laptop). If you’re on an Apple Silicon Mac (M1/M2/M3/M4) and your droplet is amd64, the build uses Docker’s emulation layer. This works, but can be slow and occasionally produces inconsistent output.

Under Settings → Deployment → Advanced, you can toggle “Build on the droplet”. This SSHes into your droplet to build the image there directly, eliminating any cross-architecture issues. Tradeoff: builds use the droplet’s RAM (which is why we set up the swap file earlier). On a 1GB droplet, expect builds to take 5–10 minutes.

If you hit build issues, try this option before debugging anything else.

Deploy

First Deploy

The first time you deploy, use kamal setup. This:

  1. Installs Docker on the droplet (if not present)
  2. Logs into Docker Hub from the droplet
  3. Builds the Roe image
  4. Pushes it to Docker Hub
  5. Pulls it on the droplet and starts the container

cd /path/to/your/roe
kamal setup

Expect 5–10 minutes on first run. Subsequent deploys (kamal deploy) are faster because Docker caches layers.

Verify the Deploy

Once kamal setup finishes, hit your droplet’s IP in a browser:

http://<your-droplet-ip>

You’ll get a 500 error initially — that’s expected, because the /site directory on the droplet is empty (no pages/home.md yet). The error confirms Roe is running; it just doesn’t have any content yet.

Verify the container is healthy:

kamal app details
kamal app logs --lines 30

Should show one running container and Rails boot messages.

Create an Admin User on the Droplet

Roe’s database is per-environment (it lives in /site/db/production/ on the droplet, separate from your local one). So you need to create your first admin user there:

kamal console

Then in the Rails console:

User.create!(
  email_address: "you@example.com",
  password: "your-strong-password",
  password_confirmation: "your-strong-password"
)
exit

Use a real strong password and save it to your password manager. This is the only admin user on production until you add more.

Sync Your Site to Live

Now that the droplet is running Roe, you need to push your local content (/site folder) to it. Roe has a Site Sync feature for this.

Configure the Sync Token

Both your local Roe and the live Roe need to share a token to authenticate API calls between them.

  1. On your local admin: visit /admin/site_sync and copy the token from the settings panel.
  2. On the droplet: open a Rails console and set the same token:
       kamal console
       
       SyncConfig.current.update!(token: "<paste-token-from-local>")
       exit
       

Set the Peer URL on Local

Back on your local admin’s /admin/site_sync page, set Peer URL to your droplet’s address:

http://<your-droplet-ip>

Save. Click Refresh exchange. You should see “Last contact with Live site: less than a minute ago” — that confirms the API connection works.

Push Your Site

From the local admin’s /admin/site_sync page, click Push to live, type LIVE to confirm. This rsyncs your /site folder to the droplet. A few minutes later, your posts, pages, and theme are on production.

Verify

Reload your droplet’s IP in a browser. The home page should render now. Sign in at http://<your-droplet-ip>/session/new with the admin credentials you created earlier.

Domain and SSL (when ready)

For your first deploy you can run on the droplet’s IP over plain HTTP. When you have a domain ready:

  1. Point an A record at the droplet’s IP via your DNS provider.
  2. Edit config/deploy.yml and add a proxy: block:

       proxy:
         ssl: true
         host: yourdomain.com
       
  3. Run kamal proxy reboot to enable Let’s Encrypt SSL.

You’ll have HTTPS within a few minutes (Let’s Encrypt provisions automatically).

Going Forward: Routine Deploys

After the first deploy, deploys are simpler. The recommended sequence:

kamal app stop
ssh root@<your-droplet-ip> 'cd /var/lib/roe/site/db/production && rm -f *-wal *-shm'
kamal deploy

The first command stops the running container (releases SQLite locks). The second cleans up any stale SQLite write-ahead-log files. The third deploys.

Why the manual stop instead of just kamal deploy? Kamal’s default zero-downtime deploy strategy runs the new container alongside the old one for a moment. With SQLite, two processes can’t safely share the database file — the new container fails to initialize SolidQueue and the deploy aborts. Stopping first avoids this.


Troubleshooting

Tailwind CSS errors after deploy (“asset ‘tailwind.css’ was not found”)

If you’ve made local changes to assets, ensure they’re committed to git before deploying — Kamal versions images by git SHA. If the SHA hasn’t changed, Kamal may reuse a cached build that misses your new assets.

If the issue persists after committing, the Docker layer cache may have stale state. Force a fresh build:

ssh root@<your-droplet-ip> 'docker builder prune -a -f && docker buildx prune -a -f'
kamal deploy

Container fails health check / “target failed to become healthy within configured timeout (30s)”

Usually a SQLite lock issue. Stale write-ahead-log files left over from a crashed previous container can prevent the new one from booting. Recovery:

kamal app stop
ssh root@<your-droplet-ip> 'cd /var/lib/roe/site/db/production && rm -f *-wal *-shm'
kamal deploy

Droplet becomes unresponsive during deploy

The 1GB plan can run out of memory during the build + container restart cycle. If SSH stops responding:

  1. Power Cycle the droplet from the DigitalOcean dashboard (Power → Power Cycle).
  2. Wait ~30 seconds.
  3. SSH back in.
  4. Make sure the swap file from earlier is active: free -h should show 2GB of swap.

If this happens repeatedly, consider upgrading to the 2GB plan ($12/mo) — the extra RAM gives you headroom during deploys. The resize is reversible and doesn’t lose data.

“No such file: config/master.key” errors

The master key wasn’t created or wasn’t shipped to the container. Check that config/master.key exists locally and contains the right value, and that .kamal/secrets has the line RAILS_MASTER_KEY=$(cat config/master.key).

Need to roll back to a previous version

Kamal tags every image in your registry. To roll back:

kamal app boot --version=<previous-tag>

Find the previous tag in your Docker Hub repository’s tags list, or via kamal app version.