This guide is the Managed-VPS-specific path through hosting a Ruby on Rails app: provision a [DeployHQ Managed VPS](https://www.deployhq.com/hosting/managed-vps) from inside [DeployHQ](https://www.deployhq.com), 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](https://www.deployhq.com/blog/ruby-application-servers-in-2025-a-complete-performance-and-architecture-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](https://www.deployhq.com) Managed VPS running Ubuntu with Ruby installed
- Puma running the Rails app behind an Nginx reverse proxy
- Sidekiq workers for background jobs
- [DeployHQ](https://www.deployhq.com) 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](https://www.deployhq.com) 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](https://www.deployhq.com) account yet, [start a free trial](https://www.deployhq.com/signup) — 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](https://www.deployhq.com) project, click **New Server** :

1. Enter a name for the server (your reference only)
2. Select **Managed VPS** from the protocol picker under the **Hosting** section
3. 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.
4. SSH keys: blank to let [DeployHQ](https://www.deployhq.com) generate one, or paste your own
5. Set the **deployment path** to `/var/www/rails-app` (or your preferred root)
6. 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](https://www.deployhq.com/features/build-pipelines):

```
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](https://www.deployhq.com/features/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:

1. Clones the repo at HEAD
2. Runs the build pipeline (bundle install + asset precompile)
3. Transfers the built artifact to the VPS via SSH
4. Symlinks `current/` to the new release
5. Runs the post-deploy hook (migrations, restart Puma)
6. 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](https://www.deployhq.com) 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:precompile` runs 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:migrate` manually 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](https://www.deployhq.com) 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](https://www.deployhq.com) context, the [hosting hub](https://www.deployhq.com/hosting) catalogs all five hosting types. The [Managed VPS pillar guide](https://www.deployhq.com/blog/managed-vps-hosting-on-deployhq) 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](https://www.deployhq.com/blog/best-software-deployment-tools), and for the cross-cluster bridge across DeployHQ's five hosting types, [your universal deployment and hosting platform](https://www.deployhq.com/blog/deployhq-your-universal-deployment-platform-for-all-hosting-types).

## 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](https://www.deployhq.com/hosting/static) — 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](https://www.deployhq.com/guides/ruby-on-rails) 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](https://www.deployhq.com/blog/host-laravel-on-deployhq-managed-vps) and how to [host a Node.js service on a Managed VPS](https://www.deployhq.com/blog/host-nodejs-on-deployhq-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](https://www.deployhq.com/support/servers/managed-vps-hosting).

* * *

Questions or feedback on Rails + Managed VPS? Email [support@deployhq.com](mailto:support@deployhq.com) or follow [@deployhq](https://x.com/deployhq) on X for product updates.

