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 reliable production pattern is: start with a supported Ubuntu LTS VPS, create a non-administrative deploy account, pin the Ruby version with rbenv, install PostgreSQL, create a dedicated database role and database, configure Rails through environment variables, and run the app behind a supervised process and reverse proxy. You can deploy with Kamal and Docker, or use the traditional Puma, systemd, and Nginx stack. In both cases, point DNS at the server, enable HTTPS, and verify migrations, logs, database access, and restart behavior before launch.

Choose the deployment model first

Your choice determines whether Ruby, Puma, and PostgreSQL are managed as host services or whether Rails is packaged into containers. Kamal is the lower-maintenance deployment path when you are comfortable with Docker and a container registry. A traditional VPS deployment gives you direct control over every process and file, but you own more system configuration and release automation.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Concern Kamal and Docker Traditional Puma, systemd, and Nginx
Operational model Rails runs in Docker containers; the host primarily runs Docker and supporting services. Ruby, Rails, Puma, PostgreSQL, and Nginx run as separately configured host services.
Deployment Rails’ official guide documents bin/kamal setup and bin/kamal deploy. Commonly uses Capistrano or a comparable release script, followed by a supervised Puma restart.
Downtime and rollback Kamal is designed to deploy containers with zero downtime; previous images provide a practical rollback point. Downtime and rollback depend on your Capistrano, systemd, and release-directory procedures.
Resource overhead Docker adds a container runtime and image storage. The Rails guide calls for at least 1 GB of RAM for its Ubuntu LTS server. There is no container runtime, but you must account for Ruby, Puma, PostgreSQL, Nginx, and build dependencies directly on the VPS.
Configuration ownership You own the image, registry credentials, Kamal configuration, and host prerequisites. You own package versions, service units, permissions, proxy configuration, deploy scripts, and release recovery.

Use Kamal when repeatable container releases and automated proxy/TLS integration matter more than host-level simplicity. Use the traditional stack when you need direct access to the filesystem and services, or when an existing Capistrano and systemd process is already established.

Prepare and harden the Ubuntu VPS

The traditional procedure documented by Scaleway uses Ubuntu 22.04. More generally, choose a currently supported Ubuntu LTS image and record the exact release so package and Ruby behavior can be reproduced. Kamal’s Rails guide specifies an Ubuntu LTS server with at least 1 GB RAM.

  1. Provision an SSH-key login. Add your public key when creating the VPS. Test a new session before changing SSH settings so you do not lock yourself out.
  2. Update the operating system. From an account with sudo access, run
    sudo apt update
    sudo apt -y upgrade

    Reboot if the kernel or other core services were upgraded, then reconnect.

  3. Create a deploy account. Keep application work out of the root account. For example:
    sudo adduser deploy
    sudo usermod -aG sudo deploy

    Install the deploy user’s SSH key and verify that it can log in. Use sudo only for tasks that need it.

  4. Restrict inbound traffic. Permit SSH and the public web ports only. If you use UFW, review the rules before enabling it:
    sudo ufw allow OpenSSH
    sudo ufw allow 80/tcp
    sudo ufw allow 443/tcp
    sudo ufw enable
    sudo ufw status verbose

    Do not expose PostgreSQL’s default port publicly when Rails and PostgreSQL are on the same VPS; Rails can connect over localhost.

  5. Record names and paths. Decide the production hostname, application directory, Unix user, Ruby version, database name, and deployment method before installing application dependencies.

Install and pin Ruby with rbenv

Do not rely on whichever Ruby version happens to be in the Ubuntu repository. Rails applications declare a required Ruby version, commonly in the .ruby-version file and the Gemfile. Install rbenv for the deploy user, ensure the ruby-build component is available, and select the version required by the application.

  1. Install the compiler and Ruby build dependencies required by your Ubuntu release, then install rbenv and ruby-build using the distribution method or the rbenv project procedure appropriate to that release.
  2. Initialize rbenv in the deploy user’s shell and confirm it is active:
    rbenv --version
    ruby -v
  3. Install the application’s exact Ruby version and make it the project-local version:
    rbenv install <required-ruby-version>
    cd /srv/<your-app>
    rbenv local <required-ruby-version>
    ruby -v

    The angle-bracket values are placeholders for your application’s documented version; do not copy them literally.

  4. Install Bundler at the version expected by the lockfile, then use deployment mode so gems are installed reproducibly:
    gem install bundler
    bundle config set --local deployment true
    bundle install

Compile Ruby on the VPS only if its CPU and memory budget can handle the build. A small server may need temporary swap or a build performed elsewhere, but the deployed runtime must still match the pinned version.

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

Install PostgreSQL and create least-privilege credentials

Install PostgreSQL, its contributed extensions, and the client development headers that the pg gem may need:

sudo apt update
sudo apt install -y postgresql postgresql-contrib libpq-dev
sudo systemctl enable --now postgresql
sudo systemctl status postgresql

Create a login role and a database dedicated to this Rails application. Use a strong password generated outside your shell history or enter it interactively rather than embedding a real secret in a command copied into a terminal.

sudo -u postgres psql
CREATE ROLE app_production LOGIN PASSWORD 'replace-with-a-secret';
CREATE DATABASE app_production OWNER app_production;
q

Replace the names with values for your application. The role does not need PostgreSQL superuser privileges. If the application needs additional databases, extensions, or a separate migration role, document those privileges explicitly instead of granting broad access.

Test the credentials before Rails is involved. From the VPS, connect over localhost:

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.
psql "host=127.0.0.1 dbname=app_production user=app_production"

Enter the password when prompted and run conninfo, then quit with q. A successful login proves that PostgreSQL is running, the role can authenticate, and the database exists. It does not yet prove that Rails’ environment variables are correct.

Configure Rails to use PostgreSQL

Rails’ PostgreSQL adapter uses adapter: postgresql and Unicode encoding. Keep deployment-specific values outside Git. A production section in config/database.yml can read them from the environment:

production:
  adapter: postgresql
  encoding: unicode
  database: <%= ENV.fetch("POSTGRES_DB") %>
  username: <%= ENV.fetch("POSTGRES_USER") %>
  password: <%= ENV.fetch("POSTGRES_PASSWORD") %>
  host: <%= ENV.fetch("POSTGRES_HOST", "127.0.0.1") %>
  port: <%= ENV.fetch("POSTGRES_PORT", "5432") %>
  pool: <%= ENV.fetch("RAILS_MAX_THREADS", "5") %>
  advisory_locks: true

The names in this example are conventions, not mandatory Rails variables. Set them in the service environment or deployment secret store. If you use DATABASE_URL, Rails also accepts that connection string. When both database.yml and DATABASE_URL are present, Rails merges configuration, so document which source supplies each value and avoid silently overriding host, database, or pool settings.

Rails enables advisory locks by default. If you place an external pooler such as PgBouncer between Rails and PostgreSQL, evaluate whether the pooler’s connection behavior is compatible and consider advisory_locks: false as Rails documents.

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

Rails documents up to 1,000 prepared statements per database connection by default. Prepared statements can improve repeated-query performance but consume database memory. On a memory-constrained VPS, lower the statement limit or disable prepared statements after measuring the effect; apply the setting consistently to every production process.

Set production environment and secrets

Set RAILS_ENV=production for every release, migration, asset build, and application process. Rails uses this environment selector to load production configuration rather than development or test settings.

At minimum, provide:

  • RAILS_ENV=production
  • The Rails master key or the production credential mechanism used by the application
  • The PostgreSQL database name, role, password, host, and port, or a complete DATABASE_URL
  • Any application-specific encryption, mail, storage, or third-party service secrets

Store secrets in Kamal’s deployment secrets, systemd’s protected environment mechanism, or another secret store. Do not commit a production password, master key, or rendered secret file to Git. Restrict file permissions so only the deploy user and the service account that needs a secret can read it.

Build and deploy the Rails application

Kamal deployment

Kamal uses Docker containers to run Rails and is designed to deploy with zero downtime. The Rails guide requires an Ubuntu LTS server, at least 1 GB of RAM, a Docker Hub account and token, and DNS pointing at the server before the proxy requests a Let’s Encrypt certificate.

  1. Install Docker on the VPS and authenticate the deployment workflow to the container registry with a token that has only the permissions required to push and pull the application image.
  2. Configure the application image, server address, Rails environment, database connection, persistent volumes, and secrets in the project’s Kamal configuration.
  3. Point the production DNS record at the VPS before enabling the proxy’s SSL option.
  4. Run the first-time host and proxy setup from the application directory:
    bin/kamal setup
  5. Deploy subsequent releases with:
    bin/kamal deploy

Run migrations as part of the release process using the project’s documented migration hook or a controlled one-off Rails command. Confirm that the new image is healthy before allowing traffic to move to it. Keep the prior image available until the release has passed smoke tests so rollback is possible.

Traditional Puma, systemd, and Nginx deployment

In the host-service model, Capistrano or an equivalent script checks out a release, installs gems, precompiles assets when the application requires them, runs migrations, and switches the live release. The exact Capistrano tasks belong to your application; do not assume a migration is safe merely because the deploy command completed.

From the release directory, the essential Rails preparation usually includes:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
RAILS_ENV=production bundle install
RAILS_ENV=production bundle exec rails assets:precompile
RAILS_ENV=production bundle exec rails db:prepare

Use db:prepare when it matches the project’s migration policy; otherwise run the project’s explicit migration task. Review destructive or long-running migrations before production and take a database backup appropriate to your recovery requirements.

Puma should bind privately, commonly to localhost, while Nginx accepts public traffic. A systemd unit needs the correct deploy user, application path, Ruby environment, working directory, and restart policy. An illustrative unit is:

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

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

[Install]
WantedBy=multi-user.target

Change every path, user, and environment-file location to match your server. Then load and start the unit:

sudo systemctl daemon-reload
sudo systemctl enable --now your-app-puma
sudo systemctl status your-app-puma

Nginx should proxy the public hostname to Puma’s private listener and pass the original host and forwarding headers. Keep the upstream port identical to the port in config/puma.rb. After validating the configuration, enable the site and reload Nginx:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
sudo nginx -t
sudo systemctl reload nginx
sudo systemctl status nginx
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Point DNS at the VPS and enable HTTPS

Create the required DNS record for the production hostname and wait until it resolves to the VPS’s public address. Kamal’s proxy can request a Let’s Encrypt certificate when SSL is enabled, but certificate issuance requires the DNS record to point to the server first. Do not enable a certificate request for a hostname that still points elsewhere.

With the traditional Nginx stack, obtain a certificate through an ACME-capable method, configure Nginx for the certificate and private key, redirect HTTP to HTTPS, and verify that renewal is scheduled. Keep port 80 reachable when your chosen HTTP challenge requires it. Test renewal before relying on the certificate in production; a certificate that works today but cannot renew is an outage waiting to happen.

Verify the deployment before launch

Perform checks locally on the VPS and externally from a separate network. Record the expected result for each check so a later release can be compared with the known-good state.

  • PostgreSQL service: systemctl status postgresql shows active, and the dedicated role can connect to the production database over localhost.
  • Schema: The migration task completes with RAILS_ENV=production; inspect the schema version and confirm the application’s required tables exist.
  • Application process: systemctl status your-app-puma is active for the traditional stack, or Kamal reports healthy containers.
  • Logs: Inspect journalctl -u your-app-puma, Nginx error logs, and Rails application logs for boot failures, authentication errors, and repeated restarts.
  • Private request: Request the Puma listener locally with curl using the configured host header. This separates Rails failures from Nginx and DNS failures.
  • Public HTTP: Request the production hostname through Nginx and confirm the expected status code, response body, and forwarding behavior.
  • HTTPS: Visit the HTTPS hostname, inspect the certificate name and expiry, and confirm that HTTP redirects to HTTPS if that is your policy.
  • Restart recovery: Restart PostgreSQL, Puma, and Nginx one at a time during a maintenance window. Confirm that systemd or Kamal brings each service back and that Rails reconnects to PostgreSQL.
  • Failure behavior: Temporarily stop the application process and verify that Nginx returns a controlled error rather than exposing an unrelated service or database port.

Common failure modes and fixes

Symptom Likely cause What to check
Rails says it cannot connect to PostgreSQL. Wrong environment variable, role password, host, or database name. Print the variable names without exposing the password, repeat the localhost psql test, and confirm the process actually receives RAILS_ENV=production.
The pg gem fails to build. PostgreSQL client headers are missing. Confirm libpq-dev is installed and rerun Bundler under the pinned Ruby.
The service starts and immediately exits. Wrong working directory, Ruby shim, secret file, or Puma configuration. Read journalctl, run the same Bundler/Puma command as the service user, and verify every path in the unit.
Nginx returns 502. Puma is stopped, listening on a different port, or bound to a different address. Check Puma status and listener configuration, then test the private endpoint locally.
HTTPS issuance fails. DNS has not propagated to the VPS, port 80 is blocked, or the hostname does not match the request. Resolve the hostname from outside the VPS, inspect firewall rules, and confirm the exact domain in the proxy configuration.
Deploy succeeds but requests fail after a migration. The migration is incompatible with the running application version. Use an expand-and-contract migration sequence, test it against a backup or staging copy, and retain a rollback plan before release.
Memory usage grows with database connections. Too many Puma workers or prepared statements per connection for the VPS size. Count total connections across workers, review Rails’ statement limit, and reduce concurrency or prepared statements only after checking application behavior.

Operational practices that keep the VPS maintainable

  • Keep the Ruby version, Ubuntu release, PostgreSQL major version, Bundler version, and deployment configuration documented beside the application.
  • Separate deploy, runtime, and database credentials. Rotate them without committing replacements to the repository.
  • Back up PostgreSQL and periodically perform a restore test; a backup that has never been restored is not a verified recovery plan.
  • Monitor disk space, memory, PostgreSQL connections, Puma restarts, Nginx error rates, and certificate expiry.
  • Apply operating-system and gem security updates in a controlled release process, then repeat the service-status and HTTPS checks.
  • When adding an external pooler, revisit Rails advisory locks and connection pool sizing rather than copying the direct-PostgreSQL settings unchanged.

Recommended launch sequence

  1. Provision a supported Ubuntu LTS VPS and SSH key.
  2. Update the host, create the deploy user, and restrict inbound ports.
  3. Install and pin the application’s Ruby with rbenv.
  4. Install PostgreSQL, create a dedicated role and database, and verify with psql.
  5. Set Rails production secrets and database configuration through protected environment storage.
  6. Choose Kamal or the Puma/systemd/Nginx stack and perform a first deployment.
  7. Point DNS at the VPS, issue the certificate, and configure HTTPS.
  8. Run the full verification checklist, including logs and restart tests, before sending users to the new hostname.

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.

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