Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

iTechGuides is reader-supported. When you buy through links on our site, we may earn an affiliate commission. As an Amazon Associate I earn from qualifying purchases. Learn more

The safest way to provision a Rails VPS with Ansible is to model the server as a small, versioned system: Nginx accepts public traffic, Puma runs the Rails application, PostgreSQL stores data, systemd supervises processes, and Ansible roles install and configure each layer. Keep first-boot provisioning separate from application releases, store secrets outside Git, and make every task idempotent so a failed run can be corrected and rerun without damaging the host.

This guide uses the conventional single-VPS architecture and an Ubuntu 22.04 (Jammy) example. Adapt package names and service details if you choose another supported Ubuntu LTS or a different database and process manager.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

What the finished server looks like

A typical single-VPS Rails deployment has this request path:

Internet → Nginx → Puma → Rails → PostgreSQL

Nginx terminates TLS, serves compiled static files when Rails is configured to do so, and proxies dynamic requests to Puma over a local socket or loopback port. Puma runs one or more Rails workers as an unprivileged application user. PostgreSQL can run on the same VPS or on a private managed service. systemd starts Puma (and any worker process), restarts failed services, and records service state in the journal.

rbenv is a practical way to install the Ruby version required by the application’s Gemfile. Capistrano-style releases can deploy known revisions into timestamped directories, while Ansible remains responsible for the host, service units, users, packages and baseline configuration.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Prerequisites and decisions to make first

Host prerequisites

  • A supported Ubuntu LTS VPS; the representative setup here assumes Ubuntu 22.04 Jammy.
  • An SSH key that can log in to the instance.
  • Initial root or sudo-capable access for bootstrap tasks.
  • A DNS name pointed at the VPS if you intend to use HTTPS.
  • A local workstation or CI runner with a pinned Ansible version and access to the repository.

Pin the inputs

Do not let a playbook silently select “latest” versions. Put the operating-system assumptions, Ruby version, Rails dependency set, PostgreSQL major version, Nginx version, Puma version and Ansible version in documentation or variables. The application’s .ruby-version, Gemfile.lock and JavaScript lockfile should be treated as release inputs. Update those pins deliberately and test the resulting release before changing production.

Choose where PostgreSQL runs

Choice Advantages Costs and responsibilities
Same VPS Lowest network latency, one private machine to manage, and no separate database bill. CPU, memory, disk and failure domain are shared with Rails. You own backups, restore testing, upgrades, replication decisions and capacity planning.
Managed database Usually provides an independent failure domain, managed backups and easier high availability. Additional recurring cost, network latency and provider-specific configuration. You still need credentials, access controls and restore verification.

For a small installation, a local database is straightforward. Move PostgreSQL to a managed service when isolation, failover or operational capacity matters more than the simplicity of one host.

Organize the Ansible repository

Use roles with narrow responsibilities. A layout like this keeps the playbook readable and allows a rebuild or a later multi-host split:

ansible/
  inventory/production/hosts.yml
  group_vars/production.yml
  host_vars/rails-vps.yml
  playbooks/site.yml
  roles/
    common/
    ruby/
    postgresql/
    rails_app/
    puma/
    nginx/
    deploy/

Ansible describes playbooks as repeatable and reusable configuration-management and multi-machine deployment artifacts. Keep environment-specific values in group or host variables, and encrypt sensitive values with Ansible Vault or inject them from an external secret system at runtime.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Example inventory

all:
  children:
    rails:
      hosts:
        rails-vps:
          ansible_host: 203.0.113.10
          ansible_user: ubuntu
          ansible_ssh_private_key_file: ~/.ssh/rails-vps

Use your real address and key path in the local file or CI secret store. Never commit a private key.

Example variables

app_name: myapp
app_user: deploy
app_group: deploy
app_root: /srv/myapp
ruby_version: "3.2.2"
app_revision: "v2026.10.1"
postgresql_database: myapp_production
postgresql_user: myapp
puma_bind: unix:///run/myapp/puma.sock
rails_env: production

Database passwords, RAILS_MASTER_KEY, cloud tokens and SMTP credentials do not belong in this plaintext file. Reference a vaulted variable or a secret injected by your deployment system instead.

Build the site playbook

Compose roles in dependency order. A minimal site playbook can look like this:

- name: Provision Rails hosts
  hosts: rails
  become: true
  roles:
    - common
    - ruby
    - postgresql
    - rails_app
    - puma
    - nginx

If workers, monitoring or backups are separate concerns, add roles for them rather than putting unrelated shell commands in the site playbook. Ansible can also compose common, database, web and monitoring roles in a rolling-upgrade workflow and be invoked from CI/CD or AWX.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Step 1: Bootstrap a non-root deployment user

The first run may use the image’s default administrator account. Create a dedicated user for releases and services, install its authorized key, and grant only the sudo permissions that provisioning actually needs.

  1. Create the user and its primary group with ansible.builtin.user.
  2. Install the public SSH key with ansible.posix.authorized_key.
  3. Create a narrowly scoped sudoers entry with ansible.builtin.copy or template; validate it before enabling it.
  4. After confirming a second login works, disable password authentication and direct root login where your recovery process permits.

Keep the deploy user separate from root. Rails, Puma and workers should run as this unprivileged account; root should be reserved for tasks that require system changes.

Step 2: Apply common host configuration

Packages and build dependencies

Use ansible.builtin.apt with an explicit package list. Include compiler and library packages required by native gems, PostgreSQL client headers, Git, a JavaScript runtime and any image-processing libraries your application actually uses. Updating the package cache can be a separate task with a controlled cache-validity period.

Time, firewall and log policy

  • Configure a time-synchronization service and verify the timezone expected by your operations team.
  • Allow only SSH, HTTP and HTTPS on the public interface. Permit PostgreSQL only on loopback or a private network when it is not local-only.
  • Configure log rotation for Nginx, Rails and worker logs. Journal retention must leave enough history to diagnose a failed release without exhausting disk.
  • Install a disk-usage check and alert before the filesystem reaches the point where PostgreSQL or Puma cannot write.

Represent firewall rules, sysctl settings and log policies as templates or module parameters so a second run converges on the same state.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Step 3: Install Ruby and application tooling

The Ruby role should install rbenv for the deploy user, the exact Ruby version required by the application, the matching Bundler version and the JavaScript tooling needed to compile assets. Record all of these as variables.

  1. Install the build libraries and Git as the common role.
  2. Install rbenv and its Ruby-build component for the deploy user.
  3. Compile or install ruby_version, then set it as the user’s global or application-local version.
  4. Install the Bundler version declared by the lockfile or deployment policy.
  5. Install Node.js and the package manager version required by the application’s lockfile.

Run these operations with the deploy user where possible. If a command is unavoidable, guard it with creates, set an accurate changed_when, or provide a check-mode implementation. An unguarded “install Ruby” shell command is not idempotent and will make every playbook run look like a change.

Step 4: Install and secure PostgreSQL

The PostgreSQL role should install the selected major version, create the application role and database, and apply access rules that match your topology.

  1. Install the server and client packages.
  2. Enable and start the database service.
  3. Create a login role owned by the application, with a password supplied from a secret variable.
  4. Create the production database with the intended owner and encoding.
  5. Restrict pg_hba.conf to local sockets or the private application network; never expose broad public access for convenience.
  6. Reload PostgreSQL after authentication configuration changes and verify a real application connection.

Take a verified backup before migrations that drop or transform data. A backup job that has never been restored is an assumption, not a recovery plan. Schedule restore drills and document the time and steps required to recover onto a clean host.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Step 5: Create the application release layout

Give the deploy user ownership of a stable root such as /srv/myapp. A release layout separates immutable code from shared configuration and persistent data:

/srv/myapp/
  releases/20261015120000/
  current -> releases/20261015120000
  shared/
    config/master.key
    config/database.yml
    log/
    tmp/pids/
    tmp/sockets/
    storage/

Use ansible.builtin.file to create directories with explicit owners and modes. Check out a requested Git revision with ansible.builtin.git; do not deploy an unpinned branch when you need reproducibility.

Install gems with deployment settings, load the production environment, and compile assets in the release directory. Shared secrets and persistent uploads should be symlinked into the release, never copied into every revision. Ensure the master key and database configuration are readable only by the deploy user and the services that need them.

Step 6: Run migrations as an explicit release action

Migrations can be irreversible and should not be hidden inside a generic “restart application” task. Make the migration command a named deployment step with a backup and rollback decision before it runs.

  1. Confirm the release revision and database backup status.
  2. Install the release’s dependencies and compile assets.
  3. Run RAILS_ENV=production bin/rails db:migrate as the deploy user.
  4. Check migration output and application health before switching the current symlink.
  5. If the migration is destructive, use an expand-and-contract pattern or a tested restore procedure rather than assuming the previous code can immediately run against the changed schema.

Step 7: Run Puma under systemd

Render a dedicated unit with Ansible’s ansible.builtin.systemd_service module and a template. The fully qualified module name avoids collection-name conflicts; systemd is a backward-compatible alias. If you must support hosts using different init systems, ansible.builtin.service is the portability layer that proxies to the detected service manager.

[Unit]
Description=MyApp Puma
After=network.target postgresql.service

[Service]
Type=simple
User=deploy
Group=deploy
WorkingDirectory=/srv/myapp/current
Environment=RAILS_ENV=production
EnvironmentFile=/srv/myapp/shared/config/myapp.env
ExecStart=/home/deploy/.rbenv/bin/rbenv exec bundle exec puma -C config/puma.rb
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target

Use the paths and command form appropriate to your rbenv installation. Store the environment file outside the release directory, restrict its mode, and avoid putting secrets directly in the unit template.

Enable and start the unit only after its directories, socket path and release exist. A handler should run daemon reload and restart Puma when the unit or Puma configuration changes. Ordinary playbook runs should not restart a healthy service unnecessarily.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Step 8: Configure Nginx as the reverse proxy

Render a server block that listens on the required names, serves static assets according to your Rails configuration, and proxies dynamic requests to Puma’s Unix socket or loopback port. Set the forwarding headers Rails needs, configure request and proxy timeouts for your workload, and reject unknown hostnames where appropriate.

server {
  listen 80;
  server_name example.com;
  root /srv/myapp/current/public;

  location /assets/ {
    try_files $uri =404;
    access_log off;
  }

  location / {
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_pass http://unix:/run/myapp/puma.sock;
  }
}

Validate the rendered file with nginx -t before reloading. Attach a handler so a syntax-validated configuration change reloads Nginx, while an unchanged file leaves the running process alone.

Terminate HTTPS at Nginx

Use an automated certificate-renewal process, redirect HTTP to HTTPS after the certificate is active, and test renewal before depending on it. Rails must be told that the proxy is trusted and that production requests are HTTPS; otherwise secure cookies, redirects and URL generation can be wrong.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Handlers and idempotency rules

Handlers are the connection between configuration and service lifecycle. A changed Puma unit should notify a daemon-reload and restart handler; a changed Nginx template should notify a configuration-test-and-reload handler. Do not restart services at the end of every play regardless of whether anything changed.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
  • Prefer package, user, file, template, Git and service modules to shell.
  • Use creates, removes, changed_when and failed_when when a command has no suitable module.
  • Run ansible-playbook --check --diff before a production change and review rendered secrets carefully.
  • Make tasks safe to run after a partial failure; a rerun should finish the missing work rather than duplicate users, databases or releases.

Provisioning versus application releases

Provisioning establishes the machine. A release changes application code. Keeping them separate makes a replacement VPS predictable and makes application rollback smaller.

Provisioning workflow Release workflow
OS packages, users, SSH policy, firewall, Ruby, PostgreSQL, directories, service units, Nginx and log policy. Fetch a known revision, install gems and JavaScript dependencies, compile assets, migrate, switch the release, restart or reload Puma, and run health checks.
Run infrequently and review as infrastructure change. Run for each tested application revision through CI/CD or an approved operator.
Designed to rebuild a host from an empty image. Designed to deploy, verify and roll back a release without rebuilding the host.

Capistrano-style releases and container workflows

Approach Strengths Trade-offs
Capistrano-style releases Natural fit for a single VPS, timestamped revisions, shared directories and simple symlink rollback. Host Ruby and system libraries remain part of the maintenance surface.
Container/Kamal-style workflow Packages application dependencies into an image, improving image reproducibility and making the release artifact explicit. Requires an image registry, container runtime and more host orchestration; persistent storage and database migration sequencing still need care.

Either approach can use Ansible for base-host provisioning. Choose the release mechanism your team can monitor and roll back reliably.

Verification after a provisioning run

  1. Run ansible-playbook --check --diff and confirm the second normal run reports no unexpected changes.
  2. Check service state with systemctl status puma, systemctl status nginx and systemctl status postgresql.
  3. Inspect recent logs with journalctl -u puma -n 100 --no-pager and the corresponding Nginx and PostgreSQL logs.
  4. Call a local Rails health endpoint through Nginx and verify the expected status code and response body.
  5. Test database connectivity using the same user and environment that Puma uses.
  6. Request the public HTTPS URL, confirm the certificate chain, redirect behavior, asset loading and a session-backed page.
  7. Exercise a controlled Puma restart and confirm systemd brings it back.
  8. Check disk space, backup freshness, certificate-renewal status and monitoring alerts.

Troubleshooting common failures

Puma will not start

Check the unit’s WorkingDirectory, rbenv path, Bundler version, environment file permissions and socket directory. Run the Puma command manually as the deploy user from the release directory, then read the full journal rather than only the final systemd status line.

Nginx returns 502 Bad Gateway

Confirm Puma is active and that the socket path in Nginx exactly matches Puma’s bind path. Verify the Nginx worker can traverse the parent directories and access the socket. A stale socket after a crash may require a controlled service restart.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Assets are missing

Confirm the release compiled assets in production mode, that Nginx’s root points at current/public, and that the Rails asset host and static-file settings match the proxy design. Inspect the browser’s requested asset path and Nginx error log.

Database authentication fails

Check the effective environment file, PostgreSQL role ownership, database name, socket versus TCP settings, and the relevant pg_hba.conf rule. Reload PostgreSQL after changing access rules and test with the same credentials outside Rails.

Ansible reports changes every run

Find commands that always return success while changing state, templates that contain unstable timestamps, and package tasks that request unpinned latest versions. Replace shell with a module or add a precise idempotency guard.

Security and reliability checklist

  • Use SSH keys and disable password login when you have verified console or recovery access.
  • Allow only required public ports; keep database traffic private.
  • Keep RAILS_MASTER_KEY, database credentials and provider tokens outside source control, with restrictive ownership and permissions.
  • Run Rails, Puma and workers as an unprivileged user.
  • Terminate TLS at Nginx, automate renewal and test renewal.
  • Rotate logs and monitor disk, Puma, workers, PostgreSQL, backups and certificate expiry.
  • Take and verify backups before destructive migrations; perform restore drills.
  • Record the OS, Ruby, Rails, PostgreSQL, Nginx, Puma and Ansible versions for each release.
  • Keep a tested rollback path for both code and database changes.

Final operating model

A dependable Rails VPS is not produced by a single long shell script. It is the result of explicit inputs, role boundaries, least-privilege users, handlers tied to real changes, and two separate workflows: Ansible provisions the host, while a release process deploys and verifies a known application revision. With that model, rebuilding the VPS and repeating a deployment become routine operations rather than emergency improvisation.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.