Rails apps run on an application server, usually Puma, with Nginx in front for HTTPS and static files. This guide shows how to deploy Ruby on Rails on a VPS manually, which is also what tools like Kamal or Capistrano automate.
Before you start
- An Ubuntu LTS VPS with 2 GB RAM or more and a deploy user with sudo.
- A domain pointing to the VPS.
- Your app in Git, with
config/master.keyavailable to you (it is not committed).
Install build tools and Ruby
sudo apt update && sudo apt install -y git curl build-essential libssl-dev libreadline-dev zlib1g-dev libyaml-dev libpq-dev nginx postgresqlInstall rbenv and the Ruby version from your app's .ruby-version, as the deploy user:
git clone https://github.com/rbenv/rbenv.git ~/.rbenv && git clone https://github.com/rbenv/ruby-build.git ~/.rbenv/plugins/ruby-buildecho 'eval "$(~/.rbenv/bin/rbenv init - bash)"' >> ~/.bashrc && source ~/.bashrcrbenv install 3.3.6 && rbenv global 3.3.6 && gem install bundlerUse the version your app requires instead of the example. Install Node.js too if your app uses a JavaScript bundler.
Create the database
sudo -u postgres createuser -P deploysudo -u postgres createdb -O deploy myapp_productionGet the code and configure it
git clone git@github.com:you/myapp.git ~/myapp && cd ~/myappbundle config set --local deployment true && bundle config set --local without 'development test' && bundle installCopy your master key to the server (never commit it) and restrict it:
chmod 600 config/master.keySet the database URL and environment in an env file, e.g. ~/myapp/.env.production, used by systemd below:
RAILS_ENV=production
DATABASE_URL=postgres://deploy:strong-password@127.0.0.1/myapp_production
RAILS_SERVE_STATIC_FILES=false
RAILS_LOG_TO_STDOUT=truePrepare the database and assets:
set -a; . ./.env.production; set +a; bin/rails db:migrate && bin/rails assets:precompileRun Puma with systemd
Create /etc/systemd/system/myapp.service:
[Unit]
Description=myapp Puma
After=network.target
[Service]
Type=simple
User=deploy
WorkingDirectory=/home/deploy/myapp
EnvironmentFile=/home/deploy/myapp/.env.production
ExecStart=/home/deploy/.rbenv/shims/bundle exec puma -C config/puma.rb -b tcp://127.0.0.1:3000
Restart=always
[Install]
WantedBy=multi-user.targetsudo systemctl daemon-reload && sudo systemctl enable --now myappTune workers and threads in config/puma.rb (WEB_CONCURRENCY, RAILS_MAX_THREADS) to your CPU and RAM.
Configure Nginx
server {
server_name example.com;
root /home/deploy/myapp/public;
location ^~ /assets/ {
expires max;
add_header Cache-Control public;
}
try_files $uri @app;
location @app {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}Nginx needs execute permission on the path to public (chmod o+x /home/deploy). Reload Nginx and add SSL with sudo certbot --nginx -d example.com. In Rails, keep config.force_ssl = true.
Deploy updates
cd ~/myapp && git pull && bundle install && set -a && . ./.env.production && set +a && bin/rails db:migrate && bin/rails assets:precompile && sudo systemctl restart myappTroubleshooting
- ActiveSupport::MessageEncryptor::InvalidMessage: wrong or missing
master.keyfor the credentials file. - 502 from Nginx: Puma is not running.
journalctl -u myapp -n 50shows why. - Assets 404: assets were not precompiled or Nginx cannot read
public. - Blocked host error: add the domain to
config.hostsin production.rb.
Rails deployment FAQ
Should I use Kamal instead?
Kamal (bundled with Rails 8) deploys Docker containers over SSH and is a great option once you are comfortable with containers.
Can Rails run on cPanel hosting?
Some cPanel servers offer Ruby app support via Passenger, but versions and gems are limited. A VPS is the dependable choice.
How many Puma workers should I use?
Roughly one worker per CPU core, with 3–5 threads each, limited by available RAM.
