This guide is the Managed-VPS-specific path through hosting a Ruby on Rails app: provision a DeployHQ Managed VPS from inside DeployHQ, set up Ruby + Puma + Nginx, wire in Sidekiq for background jobs, configure the deployment pipeline, and ship the first deploy.
If you're weighing application servers — Puma vs Passenger vs Falcon vs iodine — our Ruby application servers performance guide covers the architecture and benchmarking. For this tutorial we'll use Puma as the default, which is what Rails ships with and what most production deployments use.
What you'll build
- A DeployHQ Managed VPS running Ubuntu with Ruby installed
- Puma running the Rails app behind an Nginx reverse proxy
- Sidekiq workers for background jobs
- DeployHQ build pipeline running bundle install + asset precompilation
- Custom domain over HTTPS
- Atomic zero-downtime deploys with one-click rollback
Expected time: 30-45 minutes for a fresh project.
Prerequisites
- A Rails project in a Git repository
- A DeployHQ account with beta features enabled (Settings > Beta Features)
- A database — install Postgres or MySQL on the VPS, or use an external managed DB
- Redis if your app uses Sidekiq, Action Cable, or caching
- Local Ruby + Bundler to verify builds before pushing
If you don't have a DeployHQ account yet, start a free trial — Managed VPS is free during beta, and you can pick Starter, Standard, or Plus from inside any project.
Step 1: Provision the Managed VPS
In your DeployHQ project, click New Server:
- Enter a name for the server (your reference only)
- Select Managed VPS from the protocol picker under the Hosting section
- Choose a region and a server size. Rails apps with Sidekiq and a Postgres DB on the same box typically need at least 2 GB RAM. For a smaller workload, 1 GB is workable but tight.
- SSH keys: blank to let DeployHQ generate one, or paste your own
- Set the deployment path to
/var/www/rails-app(or your preferred root) - Click Create Server
Provisioning takes about a minute. Once active, the server appears in Settings > Hosted Resources with its IP, region, size, and status.
Step 2: Install the runtime stack
SSH in. Install Ruby (via rbenv or rvm — rbenv shown here), Nginx, Postgres, Redis, and supporting libraries:
# rbenv for Ruby version management
sudo apt update
sudo apt install -y git curl libssl-dev libreadline-dev zlib1g-dev \
build-essential libyaml-dev libxml2-dev libxslt-dev libcurl4-openssl-dev \
libffi-dev nginx postgresql postgresql-contrib redis-server \
libpq-dev nodejs npm
# Install rbenv
git clone https://github.com/rbenv/rbenv.git ~/.rbenv
echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bashrc
echo 'eval "$(rbenv init -)"' >> ~/.bashrc
exec $SHELL
# Install ruby-build plugin
git clone https://github.com/rbenv/ruby-build.git ~/.rbenv/plugins/ruby-build
# Install Ruby — match your project's .ruby-version.
# Rails 8.1 requires Ruby 3.2 or newer. 3.4.10 is a safe, widely-supported choice;
# Ruby 4.0.x is the current stable line if your gems are ready for it.
rbenv install 3.4.10
rbenv global 3.4.10
gem install bundler
Enable services on boot:
sudo systemctl enable nginx postgresql redis-server
sudo systemctl start nginx postgresql redis-server
Set up the deployment directory:
sudo mkdir -p /var/www/rails-app
sudo chown -R $USER:www-data /var/www/rails-app
Step 3: Configure Nginx as a reverse proxy
Create /etc/nginx/sites-available/rails-app:
upstream puma {
server unix:///var/www/rails-app/shared/sockets/puma.sock fail_timeout=0;
}
server {
listen 80;
server_name example.com www.example.com;
root /var/www/rails-app/current/public;
location / {
try_files $uri @puma;
}
location @puma {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $http_host;
proxy_redirect off;
proxy_pass http://puma;
}
location ~ ^/(assets|packs)/ {
gzip_static on;
expires max;
add_header Cache-Control public;
}
error_page 500 502 503 504 /500.html;
client_max_body_size 10M;
keepalive_timeout 10;
}
The current symlink in the root path is critical — DeployHQ's zero-downtime deploys use atomic symlink switching, so Nginx serves whatever current/public/ points to at any moment.
Enable the site:
sudo ln -s /etc/nginx/sites-available/rails-app /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
Step 4: Database + environment
Create the Postgres user and database:
sudo -u postgres psql -c "CREATE USER rails_app WITH PASSWORD 'STRONG-PASSWORD';"
sudo -u postgres psql -c "CREATE DATABASE rails_app_production OWNER rails_app;"
Create /var/www/rails-app/shared/config/credentials/production.key with your Rails master key, and /var/www/rails-app/shared/.env.production with environment variables (DATABASE_URL, REDIS_URL, RAILS_MASTER_KEY, etc.). These live in shared/ so they persist across deploys.
Also create shared/sockets/ for the Puma Unix socket and shared/log/ and shared/tmp/ for log and tmp persistence:
mkdir -p /var/www/rails-app/shared/{sockets,log,tmp,config/credentials}
Step 5: Configure Puma for production
Create config/puma.rb in your Rails app (commit to git):
threads_count = ENV.fetch('RAILS_MAX_THREADS', 5)
threads threads_count, threads_count
port ENV.fetch('PORT', 3000)
environment ENV.fetch('RAILS_ENV', 'production')
bind "unix:///var/www/rails-app/shared/sockets/puma.sock"
stdout_redirect "/var/www/rails-app/shared/log/puma.stdout.log",
"/var/www/rails-app/shared/log/puma.stderr.log", true
pidfile "/var/www/rails-app/shared/tmp/pids/puma.pid"
state_path "/var/www/rails-app/shared/tmp/pids/puma.state"
workers ENV.fetch('WEB_CONCURRENCY', 2)
preload_app!
on_worker_boot do
ActiveRecord::Base.establish_connection if defined?(ActiveRecord)
end
plugin :tmp_restart
Create a systemd service /etc/systemd/system/puma-rails-app.service:
[Unit]
Description=Puma for Rails app
After=network.target
[Service]
Type=simple
User=www-data
WorkingDirectory=/var/www/rails-app/current
Environment=RAILS_ENV=production
EnvironmentFile=/var/www/rails-app/shared/.env.production
ExecStart=/home/www-data/.rbenv/shims/bundle exec puma -C config/puma.rb
ExecReload=/bin/kill -USR1 $MAINPID
KillMode=mixed
Restart=on-failure
RestartSec=5
SyslogIdentifier=puma
[Install]
WantedBy=multi-user.target
Enable and start:
sudo systemctl daemon-reload
sudo systemctl enable puma-rails-app
# Don't start yet — wait for first deploy
Step 6: Configure the build pipeline in DeployHQ
In project build settings, add the build pipeline:
bundle config set --local deployment true
bundle config set --local without 'development test'
bundle install
bundle exec rake assets:precompile
Set RAILS_ENV=production and your Rails master key as an environment variable in DeployHQ's UI.
In deployment hooks, add a post-deploy hook to run on the VPS:
cd /var/www/rails-app/current
bundle exec rake db:migrate
sudo systemctl restart puma-rails-app
sudo systemctl restart sidekiq-rails-app # added in next step
In server settings, ensure shared files includes .env.production and shared folders includes log, tmp, sockets, and config/credentials. Enable atomic transfers and zero-downtime mode — see zero-downtime deployments for what the atomic transfer machinery does under the hood.
Step 7: First deploy
Push to the repository or trigger a manual deploy. DeployHQ:
- Clones the repo at HEAD
- Runs the build pipeline (bundle install + asset precompile)
- Transfers the built artifact to the VPS via SSH
- Symlinks
current/to the new release - Runs the post-deploy hook (migrations, restart Puma)
- Old releases kept for rollback
Watch the log stream. When complete, verify Puma is running with sudo systemctl status puma-rails-app, then visit your server's IP / domain to confirm Rails responds.
Common first-deploy failures:
- Bundle install fails on the VPS during deploy — but it shouldn't, since DeployHQ runs bundle install on its build infrastructure. If you see this, your build pipeline isn't picking up the bundle step.
- Assets aren't precompiled — confirm
bundle exec rake assets:precompileruns in the build pipeline, not as a post-deploy hook. Pre-compiling on the VPS is slow and uses VPS memory. - Database migrations fail — run
bundle exec rake db:migratemanually via SSH first to verify connectivity and credentials.
Step 8: Sidekiq for background jobs
Create /etc/systemd/system/sidekiq-rails-app.service:
[Unit]
Description=Sidekiq for Rails app
After=network.target
[Service]
Type=simple
User=www-data
WorkingDirectory=/var/www/rails-app/current
Environment=RAILS_ENV=production
EnvironmentFile=/var/www/rails-app/shared/.env.production
ExecStart=/home/www-data/.rbenv/shims/bundle exec sidekiq -C config/sidekiq.yml
Restart=on-failure
RestartSec=5
SyslogIdentifier=sidekiq
[Install]
WantedBy=multi-user.target
Enable and start:
sudo systemctl daemon-reload
sudo systemctl enable sidekiq-rails-app
sudo systemctl start sidekiq-rails-app
For scheduled jobs, use sidekiq-cron or whenever to register the schedule, and the post-deploy hook restarts Sidekiq so new schedules pick up.
Step 9: Custom domain + HTTPS
Point your domain at the server's IP via A record. Then either:
Cloudflare: add domain, set DNS to Proxied, HTTPS terminated at the edge.
Let's Encrypt on the VPS:
sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d example.com -d www.example.com
Certbot updates Nginx, configures SSL, sets up auto-renewal.
Common gotchas
Puma doesn't pick up the new code after deploy. Make sure the post-deploy hook calls systemctl restart puma-rails-app (not just kill -HUP). The systemd restart picks up the new code from the freshly-symlinked current/.
config/credentials.yml.enc decryption fails. Rails encrypted credentials need RAILS_MASTER_KEY in the environment. Either set it in DeployHQ's env vars (passed through to the build) or store it in shared/config/credentials/production.key and reference from .env.production.
Asset fingerprints break the manifest. When deploying, the new release's public/assets/.manifest-*.json should match the precompiled assets. If you see asset not found
errors, the manifest didn't make it into the deploy — confirm assets:precompile ran successfully in the build pipeline.
Sidekiq's web UI not accessible. If you mount Sidekiq::Web in routes.rb, it's accessible at /sidekiq but unprotected by default. Add HTTP Basic Auth or authenticated user check in routes before exposing.
Database connection pool exhausted. Puma workers + Sidekiq concurrency must fit in RAILS_MAX_THREADS * Puma workers + Sidekiq concurrency * number of Sidekiq processes ≤ Postgres max_connections. On a small VPS with default Postgres settings, this caps quickly.
What you've shipped
You now have:
- A Rails app running on a DeployHQ Managed VPS
- Puma + Nginx + Postgres + Redis on a real Ubuntu box you can SSH into
- Sidekiq for background jobs under systemd
- Atomic zero-downtime deploys with one-click rollback
- HTTPS via Cloudflare or Let's Encrypt
For broader DeployHQ context, the hosting hub catalogs all five hosting types. The Managed VPS pillar guide covers the provisioning surface and the comparison against DigitalOcean App Platform, Linode, and Render. For the wider category reference, best software deployment tools in 2026, and for the cross-cluster bridge across DeployHQ's five hosting types, your universal deployment and hosting platform.
What's next
If you also have a static frontend (a documentation site, marketing pages, a Vue/React SPA), pair this Managed VPS Rails backend with DeployHQ Static Hosting — the frontend on Cloudflare's edge, the Rails API on the VPS, one project, one billing relationship.
For the canonical Rails deployment reference covering Capistrano and other patterns, the Rails deployment guide is the next read.
Deploying a different stack on the same Managed VPS pattern? The same pipeline covers how to host a Laravel app on a Managed VPS and how to host a Node.js service on a Managed VPS.
Start a free trial if you don't have an account yet. For the full Managed VPS product reference, see the Managed VPS support library.
Questions or feedback on Rails + Managed VPS? Email support@deployhq.com or follow @deployhq on X for product updates.