Rails Deploy Guide
This follows the same DigitalOcean / Ubuntu / Passenger + nginx server schema already used across the other projects on this host, plus how a GitHub Actions pipeline automates it on top.
Overview
The server stack here is: Ubuntu droplet → nginx with the Passenger module built in → Rails app. There is no separate Puma/systemd service to manage — Passenger runs the app inside nginx, so restarting nginx is what brings new code live.
Two things need to exist for automated deploys to work:
- The host — provisioned once, following the exact sequence below: deploy user → firewall → SSH hardening → RVM/Ruby → PostgreSQL → Node/Yarn → Passenger → nginx → SSL.
- The workflow file (
.github/workflows/deploy.yml) — already in the repo. On every push to the deploy branch it SSHes into the host and repeats the same commands you'd otherwise run by hand:git pull,bundle install,db:migrate,assets:precompile, then restarts nginx.
Glossary — three keys, don't mix them up
| Key | Lives where | Used for | Public half goes to |
|---|---|---|---|
| Your admin key | Your own laptop (~/.ssh/id_rsa) |
Logging into the deploy user by hand for provisioning/maintenance |
Server's ~/.ssh/authorized_keys (deploy user) |
| Actions → Server key | GitHub secret SSH_PRIVATE_KEY |
Lets the GitHub Actions runner log into the server as the deploy user, unattended | Server's ~/.ssh/authorized_keys (deploy user, same file — a second line) |
Server → GitHub key (deploy_id_rsa) |
Server, in ~/.ssh/deploy_id_rsa |
Lets the server itself pull from GitHub — for private repos or private git-sourced gems in the Gemfile | GitHub repo → Settings → Deploy keys |
If the repo is cloned over plain HTTPS (as in step 2.1 below) and nothing in the Gemfile needs SSH, the third key is optional. It's included because it's what the workflow script assumes, and it costs nothing to set up once.
1. One-time host setup DigitalOcean · Ubuntu
Skip this whole section if the droplet already hosts another app deployed the same way — reuse the same deploy user, RVM, PostgreSQL, and Passenger install for every new project.
1.1 Create the deploy user
adduser deploy
usermod -aG sudo deploy
1.2 Enable the firewall
ufw allow OpenSSH
ufw allow 80
ufw allow 443
ufw enable
1.3 Add your SSH key to the deploy user
On your local machine (skip if you already have a key pair):
ssh-keygen
cat ~/.ssh/id_rsa.pub
On the server, switch into the new user and register that public key:
su deploy
mkdir -p ~/.ssh
echo "public_key_string" >> ~/.ssh/authorized_keys
chmod -R go= ~/.ssh
chown -R deploy:deploy ~/.ssh
Replace public_key_string with the actual output of cat ~/.ssh/id_rsa.pub from your machine — don't paste it literally.
1.4 Harden SSH — disable password login
sudo nano /etc/ssh/sshd_config
Change:
PasswordAuthentication yes
# to
PasswordAuthentication no
sudo systemctl restart ssh
Confirm step 1.3 actually works — open a second terminal and successfully key-login as deploy — before restarting ssh with password auth disabled. Otherwise you can lock yourself out with no way back in except the provider's console.
1.5 Passwordless sudo for the deploy user
sudo visudo
Add at the bottom of the file:
deploy ALL=(ALL) NOPASSWD: ALL
This is the broad form used across these projects — it's what lets the CI script's sudo service nginx restart run unattended without hanging on a password prompt. If you'd rather not grant blanket passwordless sudo, scope it to just /usr/sbin/service nginx restart instead (see Security notes).
1.6 Install RVM and Ruby
Log out twice (you land in root, then again to get back to your own machine) and back in as deploy before running these, so RVM installs under the right user:
ssh deploy@<host>
# only if gpg2 isn't already installed
sudo apt-get install gnupg2 -y
gpg2 --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3 7D2BAF1CF37B13E2069D6956105BD0E739499BDB
\curl -sSL https://get.rvm.io | bash -s stable
source ~/.rvm/scripts/rvm
rvm install <ruby version> --default
If the key fingerprint above is rejected, grab the current one from rvm.io — RVM occasionally rotates signing keys.
1.7 Install PostgreSQL
sudo apt-get install postgresql postgresql-contrib libpq-dev
# lets nginx/Passenger traverse into the deploy user's home directory
sudo chmod og+rX /home /home/deploy/
sudo -u postgres createuser -s deploy -P
The password you set here for the deploy Postgres role is the one that goes into each project's .env as POSTGRES_PASSWORD later.
1.8 Install Yarn and Node.js
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -
echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
sudo apt update && sudo apt install --no-install-recommends yarn
wget -qO- https://raw.githubusercontent.com/creationix/nvm/v0.33.8/install.sh | bash
source ~/.profile
nvm install <version> --default
1.9 Install Passenger
sudo apt install libcurl4-openssl-dev
sudo apt-get install -y dirmngr gnupg
sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 561F9B9CAC40B2F7
sudo apt-get install -y apt-transport-https ca-certificates
# this exact apt source works on Ubuntu 22 (jammy); for other versions check
# https://www.phusionpassenger.com/
sudo sh -c 'echo deb https://oss-binaries.phusionpassenger.com/apt/passenger jammy main > /etc/apt/sources.list.d/passenger.list'
sudo apt-get update
sudo apt-get install -y libnginx-mod-http-passenger
rvmsudo passenger-install-nginx-module
1.10 Install nginx and add SSL via certbot
sudo apt install nginx
Per-domain nginx configuration is covered in step 2.4 once a project actually exists to point at. Once a site block is live on port 80, issue a certificate:
sudo snap install --classic certbot
sudo certbot --nginx
Certbot rewrites the site's server block to add the 443 listener and SSL directives, and adds an HTTP→HTTPS redirect block. Set up renewal:
sudo crontab -e
30 2 * * 1 certbot renew
1.11 Optional: image processing
sudo apt-get -y install imagemagick
Needed if the app uses Active Storage variants for image resizing.
2. Per-project setup do this for every new app
2.1 Clone the repo
cd ~
sudo mkdir www
cd www
sudo git clone https://<repository>.git
sudo chown deploy -R <repository>
cd <repository>
2.2 Install gems
gem install bundler
bundle install
2.3 Rails credentials (only if the master key needs resetting)
If you don't already have the project's real config/master.key, generate a fresh encrypted credentials file rather than trying to guess the old one:
EDITOR="nano" bin/rails credentials:edit
rm config/credentials.yml.enc
EDITOR="nano" bin/rails credentials:edit
Skip this entirely if you already have the correct master.key for this app — this only applies to bootstrapping a project that doesn't have one on this server yet.
2.4 Configure .env and the database
chmod 700 config db
chmod 600 config/database.yml
If the repo has a .env-template file (ls .env-template shouldn't error):
sudo cp .env-template .env
Otherwise create it directly with at least:
sudo nano .env
POSTGRES_USER='deploy'
POSTGRES_PASSWORD='<password from step 1.7>'
POSTGRES_DB='<projectname>'
Then build assets and set up the database:
rails assets:precompile db:{create,migrate,seed} RAILS_ENV=production
2.5 nginx server block for this project
sudo nano /etc/nginx/sites-available/<domain>
server {
listen 80;
server_name yourserver.com;
root /home/deploy/www/<repository>/public;
rails_env production;
passenger_enabled on;
passenger_ruby /home/deploy/.rvm/rubies/ruby-<ruby-version>/bin/ruby;
}
Repeat for www.<domain> too if that's in use, then enable the site:
sudo ln -s /etc/nginx/sites-available/<domain> /etc/nginx/sites-enabled/
sudo service nginx start
Now go back to step 1.10 to issue the SSL certificate for this specific domain.
3. CI/CD with GitHub Actions
Everything in section 2 above — pulling code, installing gems, migrating, precompiling assets, restarting the app — is exactly what the workflow automates on every push. Nothing new runs on the server; it's the same commands, just triggered by a push instead of by you typing them in over SSH.
3.1 Add the deploy key for git access (optional but assumed by the script)
ssh-keygen -t ed25519 -f ~/.ssh/deploy_id_rsa -N ""
chmod 600 ~/.ssh/deploy_id_rsa
cat ~/.ssh/deploy_id_rsa.pub
Paste that public key into the repo: Settings → Deploy keys → Add deploy key. Leave "Allow write access" unchecked unless the server needs to push.
3.2 Generate the Actions → Server key
This is a separate key pair from your own admin key in step 1.3 — generate it on the server as the deploy user:
ssh-keygen -t ed25519 -f ~/.ssh/actions_deploy_key -N ""
cat ~/.ssh/actions_deploy_key.pub >> ~/.ssh/authorized_keys
cat ~/.ssh/actions_deploy_key
Copy the full private key output — including the BEGIN/END lines — you'll paste it into a GitHub secret next. Never commit it anywhere.
3.3 Add the repository secrets
Settings → Secrets and variables → Actions → New repository secret:
| Secret name | Value |
|---|---|
HOST | Server IP address or domain |
USERNAME | deploy |
SSH_PRIVATE_KEY | Full private key from step 3.2 |
3.4 The workflow file, annotated
on:
push:
branches:
- production # ← only this branch triggers a deploy
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: appleboy/ssh-action@master
with:
host: ${{ secrets.HOST }} # ← step 3.3
username: ${{ secrets.USERNAME }} # ← step 3.3, "deploy"
key: ${{ secrets.SSH_PRIVATE_KEY }} # ← step 3.2 private half
port: 22
script: |
set -e # ← abort on first failing command
cd /home/deploy/www/<repository> # ← step 2.1 path
source ~/.rvm/scripts/rvm
rvm use <ruby version> # ← step 1.6, must match .ruby-version
chmod 600 ~/.ssh/deploy_id_rsa || true
ssh-keyscan -t ed25519 github.com >> ~/.ssh/known_hosts 2>/dev/null || true
git checkout production
git pull --ff-only
git status
bundle config set without 'development test'
bundle config set path 'vendor/bundle'
GIT_SSH_COMMAND='ssh -i ~/.ssh/deploy_id_rsa -o IdentitiesOnly=yes' \
bundle install # ← step 3.1 key used here
bundle exec rake db:migrate RAILS_ENV=production
bundle exec rake assets:precompile RAILS_ENV=production
sudo service nginx restart # ← step 1.5 sudo rule; Passenger
# picks up the new code on restart
4. Running & verifying a deploy
- Push a small, safe commit to the deploy branch (e.g. a comment change).
- Open the repo's Actions tab and watch the run —
appleboy/ssh-actionstreams the remote script's output live. - If it goes green, hit the site and confirm the change is live.
- If it fails, the log tells you exactly which shell command failed — that's the whole point of the
set -eline at the top of the script.
5. Troubleshooting
6. Security notes
- Never commit
.env,master.key,deploy_id_rsa, oractions_deploy_keyto any repo. - Test key-based SSH login before disabling password authentication — see the warning in step 1.4.
deploy ALL=(ALL) NOPASSWD: ALL(step 1.5) is convenient but broad — a scoped alternative isdeploy ALL=(ALL) NOPASSWD: /usr/sbin/service nginx restartif that's the only unattended command actually needed.- Keep the git deploy key (step 3.1) read-only on GitHub unless something genuinely needs to push from the server.
- Rotate the
SSH_PRIVATE_KEYsecret if it's ever pasted somewhere outside GitHub's secret field (chat, ticket, etc.) — treat that as a leak. - One deploy user, one RVM/Postgres/Passenger install, shared across every project on the box — no need to repeat host setup per project, only per server.