You can run a production Ruby on Rails application with PostgreSQL on a VPS in two dependable ways: deploy the app as a Docker container with Kamal, or run Ruby, Puma, PostgreSQL, Nginx, and systemd directly on Ubuntu. Both work, but they differ in who owns the server configuration, how deployments roll back, and how much memory and automation you need.
Antihost offers Linux VPS plans with full root access, unlimited traffic on a 1 Gbps port and a 14-day free trial.
This guide takes you from a new Ubuntu LTS VPS to a verified HTTPS deployment. It covers SSH hardening, a pinned Ruby runtime, a least-privilege PostgreSQL database, Rails production configuration, secrets, Puma supervision, Nginx proxying, TLS, migrations, and the checks that catch common deployment failures.
As an Amazon Associate I earn from qualifying purchases.
Choose your deployment model first
Your architecture determines nearly every later command. Kamal is the container-oriented path documented in the current Rails Getting Started guide. The traditional path follows the common Ubuntu layout described by Scaleway in its tutorial reviewed July 28, 2025: rbenv for Ruby, PostgreSQL on the host, Puma as the Rails server, Nginx as the public reverse proxy, Capistrano for releases, and systemd for supervision.
| Decision | Kamal | Traditional VPS stack |
|---|---|---|
| Where Rails runs | Inside a Docker container built from the application image | As a host process launched by Puma |
| Deployment command | bin/kamal setup, then bin/kamal deploy |
Usually Capistrano tasks followed by a systemd restart or reload |
| Downtime and rollback | Kamal is designed for zero-downtime container replacement; rollback is an image or release change | Downtime depends on the release procedure and service restart; rollback means selecting a previous release and restarting Puma |
| Configuration ownership | Application image, proxy settings, registry credentials, and the remote Docker host | Ruby installation, gems, filesystem permissions, systemd units, Nginx, logs, and every host package |
| Resource overhead | Docker and image layers add some overhead and require registry access | Fewer container layers, but more host-level maintenance |
| Best fit | A team that wants repeatable container releases and less drift between servers | An operator who wants direct control of Ubuntu services or is integrating with an existing Capistrano workflow |
The Rails guide says Kamal requires an Ubuntu LTS server with at least 1 GB of RAM, a Docker Hub account and token, and DNS pointing at the server before its proxy can obtain a Let’s Encrypt certificate. The traditional route has no Docker registry requirement, but you must maintain the service stack yourself. Pick one route before creating deployment scripts; mixing pieces from both usually produces unclear ownership and unreliable restarts.
#1 Best Overall
What you need before connecting to the VPS
- An Ubuntu LTS image. The traditional Scaleway procedure uses Ubuntu 22.04; use the LTS release supported by your application and deployment tooling.
- An SSH key loaded on the VPS. Disable password login after confirming key-based access.
- A domain name whose DNS can be changed to the VPS address.
- The application’s required Ruby version, recorded in
.ruby-versionor an equivalent version file. - The Rails production credentials: a master key, database connection information, and any provider API keys required by the application.
- A decision about deployment model: Kamal with Docker, or host Ruby with Puma, Nginx, systemd, and optionally Capistrano.
A 1 GB server is the minimum stated for the Kamal path, not a performance guarantee. Rails, PostgreSQL, Docker image builds, asset compilation, and background jobs can need more memory. If the application swaps heavily during bundle installation or asset compilation, increase the VPS size rather than treating swap activity as normal capacity.
1. Provision and harden the Ubuntu server
Log in with the provider’s initial administrative account, update the system, create a non-administrative deployment account, and limit inbound traffic to what the application needs.
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
sudo apt update
sudo apt -y upgrade
sudo adduser deploy
sudo usermod -aG sudo deploy
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
sudo ufw status verbose
Copy the authorized SSH key to /home/deploy/.ssh/authorized_keys, test a second terminal with ssh deploy@server-address, and only then harden the SSH daemon. Keep an existing administrative session open while testing so a typo cannot lock you out. A typical policy is to prohibit root login and password authentication after key access has been verified:
sudoedit /etc/ssh/sshd_config
# Set these values:
PermitRootLogin no
PasswordAuthentication no
sudo systemctl reload ssh
Do not expose PostgreSQL’s port 5432 to the public internet for a single-server Rails installation. Rails and PostgreSQL can communicate over the loopback interface. If a separate application host or managed database is introduced later, restrict access with a private network or an allowlist rather than opening the port globally.
2. Install and pin Ruby with rbenv
Use the Ruby version required by the application. The distribution’s default Ruby can lag behind the version declared by Rails or by a gem, so install rbenv and select the exact version deliberately.
sudo apt update
sudo apt install -y git curl build-essential autoconf bison
rustc libssl-dev libyaml-dev libreadline-dev zlib1g-dev
libncurses5-dev libffi-dev libgdbm-dev libdb-dev
sudo -iu deploy
git clone https://github.com/rbenv/rbenv.git ~/.rbenv
echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bashrc
echo 'eval "$(rbenv init - bash)"' >> ~/.bashrc
export PATH="$HOME/.rbenv/bin:$PATH"
eval "$(rbenv init - bash)"
git clone https://github.com/rbenv/ruby-build.git "$(rbenv root)"/plugins/ruby-build
rbenv install -l
rbenv install RUBY_VERSION
rbenv global RUBY_VERSION
ruby --version
gem install bundler
rbenv rehash
In the example, replace RUBY_VERSION with the version declared by the application, such as the value in .ruby-version. Do not silently choose a different patch release if the project locks one. Commit the version file and the lockfile so every deployment uses the same runtime and dependency resolution rules.
PC Slower Than It Used to Be?
A free scan shows the junk files, broken settings and background clutter dragging Windows down - then fixes them in one click.Free scan · Windows 10 & 11Outdated Drivers Are Slowing You Down
One free scan finds every outdated or missing driver and matches the right update for your exact hardware.Free scan · exact hardware matchFor a Capistrano deployment, the deploy user should own the application directory and the shared directories used for secrets, logs, and uploaded files. For Kamal, Ruby runs inside the image; the host still needs Docker, SSH access, and enough disk space for image layers.
3. Install PostgreSQL and verify the service
Install PostgreSQL, its contributed extensions, and the client headers needed to compile the pg Ruby gem.
Rank #2
sudo apt update
sudo apt install -y postgresql postgresql-contrib libpq-dev
sudo systemctl enable --now postgresql
sudo systemctl status postgresql --no-pager
Create a role dedicated to this application and a database owned by that role. Avoid using the PostgreSQL superuser from Rails. The safest interactive approach is to create the role without putting its password in shell history:
sudo -u postgres psql
CREATE ROLE myapp LOGIN;
password myapp
CREATE DATABASE myapp_production OWNER myapp;
q
Use a strong, unique password and store it in the deployment secret store or environment, not in Git. Test the same credentials Rails will use before deploying the application:
Recommended Free Tools
psql --host=127.0.0.1 --username=myapp --dbname=myapp_production --password -c 'SELECT current_user, current_database();'
The command should return myapp and myapp_production. If it fails, fix PostgreSQL authentication, the role password, or the database name now; a Rails error later is only a less precise version of the same problem.
4. Configure Rails to use PostgreSQL
Use an explicit production database configuration
Rails’ PostgreSQL adapter uses adapter: postgresql, Unicode encoding, a database name, and a connection pool. A production section in config/database.yml can read credentials from environment variables:
production:
adapter: postgresql
encoding: unicode
database: myapp_production
username: myapp
password: <%= ENV.fetch('DATABASE_PASSWORD') %>
host: 127.0.0.1
port: 5432
pool: <%= ENV.fetch('RAILS_MAX_THREADS', 5) %>
Alternatively, provide a complete DATABASE_URL, for example postgresql://myapp:[email protected]:5432/myapp_production, through the deployment environment. When DATABASE_URL and config/database.yml are both present, Rails merges the settings. Document which source is authoritative for each value; otherwise an old hostname or pool size in one file can unexpectedly override the value you intended to change.
Understand pool size before increasing it
The pool is the number of database connections Rails can check out for a process. Set it high enough for the Puma thread count and any internal concurrency, but remember that every Puma worker and every separate job process can create its own pool. A pool of five in four processes can require up to twenty PostgreSQL connections. Check PostgreSQL’s connection limit before scaling workers.
Account for Rails PostgreSQL defaults
- Rails enables advisory locks by default. If the application connects through an external pooler such as PgBouncer, Rails documents considering
advisory_locks: false, because transaction pooling can make session-level lock behavior unsuitable. - Rails allows up to 1,000 prepared statements per database connection by default. Prepared statements can reduce parsing work, but they consume PostgreSQL memory. Lower
statement_limitor disable prepared statements when the database is memory-constrained and monitoring shows statement growth. - Rails retries only safely retryable database queries by default. The documented default is one retry, configurable with
connection_retries. A retry is not a substitute for fixing exhausted connections, failed storage, or a database that is restarting.
5. Set production mode and protect secrets
Rails selects its environment through RAILS_ENV. Set it to production for every release, migration, and service process that is intended to serve the public application.
export RAILS_ENV=production
export RAILS_MASTER_KEY=your-real-master-key
export DATABASE_URL=postgresql://myapp:[email protected]:5432/myapp_production
The values above illustrate the variables; do not commit real credentials or place them in a repository, Docker image layer, shell history, or a world-readable file. Use the VPS deployment secret mechanism, a protected systemd environment file, or Kamal’s secrets configuration with permissions restricted to the deployment account and service.
Rails encrypted credentials require the matching master key at boot. A missing or incorrect key commonly produces an error before Puma binds to its port, so test the key with a production boot command before switching traffic.
6. Install gems, prepare the database, and compile assets
From the application release directory, install the locked dependencies in deployment mode and run the tasks your project requires.
Quick wins for a faster PC:
Clear out junk files and repair common Windows errorsFree Scan →Fix the driver behind crashes, sound loss and screen glitchesFind Drivers →Repair Windows errors before they cause bigger problemsFix Now →bundle config set --local deployment true
bundle config set --local without development test
bundle install
RAILS_ENV=production bin/rails db:prepare
RAILS_ENV=production bin/rails assets:precompile
db:prepare creates the database when needed and runs pending migrations. Some teams use a separate migration task in their release pipeline; follow the project’s established convention and make sure only one release process runs migrations at a time. Asset compilation is required for applications using the asset pipeline; applications using a different build system should run that system’s production build instead.
Run a local production boot check before involving Nginx:
RAILS_ENV=production bundle exec puma -C config/puma.rb
In another terminal, request the Puma listener with curl. Stop the foreground process after confirming the app responds. If boot fails, inspect the first exception rather than repeatedly restarting the service; missing gems, credentials, database authentication, and asset manifests are the usual categories to resolve.
7. Deploy with Kamal
Kamal builds and runs the Rails application as a Docker container on the VPS. The Rails Getting Started guide describes it as using Docker containers with zero-downtime deployment. You need Docker on the server, a Docker registry account and token, SSH access, and DNS that already resolves to the server before enabling the proxy’s Let’s Encrypt configuration.
- Configure the application image, registry credentials, server address, service name, and environment secrets in the project’s Kamal configuration.
- Install Docker on the Ubuntu VPS and confirm the deploy user can invoke it according to your chosen Docker permissions model.
- Point the domain’s DNS record at the VPS and wait for the record to resolve publicly.
- Run
bin/kamal setup. This prepares the remote host, pulls the required image or builds the initial release, and starts the proxy and application services. - Run
bin/kamal deployfor subsequent releases. Watch the command output for image build, health-check, migration, and proxy status failures.
Enable SSL in the Kamal proxy settings only after the DNS record points to the server and ports 80 and 443 are reachable. Kamal can then ask Let’s Encrypt to issue the certificate for the domain. Certificate issuance will fail if DNS still points elsewhere, the domain is not publicly resolvable, or the firewall blocks the HTTP challenge.
Kamal reduces configuration drift, but it does not remove operational responsibility. Keep registry tokens private, monitor disk usage from old images, define a database backup plan, and verify that the application can reach PostgreSQL from the container’s network. A database on the host may require an address other than the container’s loopback interface; do not assume 127.0.0.1 inside a container means the VPS host.
Rank #4
8. Deploy the traditional Puma, systemd, and Nginx stack
Organize releases and shared files
A common layout is /home/deploy/apps/myapp/releases for timestamped releases, current as a symlink to the active release, and shared for logs, uploaded files, and environment files. Capistrano can create and switch these directories atomically. Ensure the deploy user owns files Puma must read and write, while Nginx only needs to proxy to Puma and read any files it serves directly.
Configure Puma
A minimal production config/puma.rb should bind to a local socket or loopback port, set the environment, and write its PID and state files somewhere the service user can access:
environment ENV.fetch('RAILS_ENV', 'production')
threads ENV.fetch('RAILS_MAX_THREADS', 5), ENV.fetch('RAILS_MAX_THREADS', 5)
bind 'unix:///home/deploy/apps/myapp/shared/tmp/sockets/puma.sock'
pidfile '/home/deploy/apps/myapp/shared/tmp/pids/puma.pid'
state_path '/home/deploy/apps/myapp/shared/tmp/pids/puma.state'
stdout_redirect '/home/deploy/apps/myapp/shared/log/puma.stdout.log', '/home/deploy/apps/myapp/shared/log/puma.stderr.log', true
preload_app!
Create the socket, PID, and log directories before starting the service. Match Puma’s thread count to the Rails database pool and account for every worker process if you use clustered mode.
Supervise Puma with systemd
Save a unit such as /etc/systemd/system/myapp-puma.service, adjusting paths and the Ruby environment for your installation:
[Unit]
Description=Myapp Rails Puma
After=network.target postgresql.service
[Service]
Type=simple
User=deploy
WorkingDirectory=/home/deploy/apps/myapp/current
Environment=RAILS_ENV=production
EnvironmentFile=/home/deploy/apps/myapp/shared/.env
ExecStart=/home/deploy/.rbenv/shims/bundle exec puma -C /home/deploy/apps/myapp/current/config/puma.rb
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
Load and start it with:
sudo systemctl daemon-reload
sudo systemctl enable --now myapp-puma
sudo systemctl status myapp-puma --no-pager
sudo journalctl -u myapp-puma -n 100 --no-pager
A healthy systemd unit should remain active after a shell logout and restart after a process crash. If it enters a restart loop, run the same ExecStart command as the deploy user with RAILS_ENV=production to expose the underlying boot error.
Put Nginx in front of Puma
Nginx accepts public HTTP traffic and proxies it to Puma’s Unix socket:
server {
listen 80;
server_name example.com;
location / {
proxy_pass http://unix:/home/deploy/apps/myapp/shared/tmp/sockets/puma.sock;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_redirect off;
}
}
Enable the site, check the configuration, and reload Nginx:
sudo nginx -t
sudo systemctl reload nginx
sudo systemctl status nginx --no-pager
A 502 response usually means Nginx cannot reach Puma: the socket path differs, the socket directory permissions prevent access, or the Puma service is stopped. Check the Nginx error log and journalctl -u myapp-puma together so you can distinguish a proxy problem from a Rails boot failure.
Best Value
9. Point DNS and enable HTTPS
Create an A record for the application hostname pointing to the VPS IPv4 address, and an AAAA record only if the server is correctly configured for IPv6. Verify resolution from outside the VPS before requesting a certificate.
The Tool Desk
Outbyte PC Repair FREEClear out junk files and repair common Windows errorsFree Scan →Outbyte Driver Updater FREEScan for outdated or missing drivers - takes under a minuteDriver Scan →dig +short example.com
curl -I http://example.com
For Kamal, enable its proxy SSL setting after DNS is correct; the proxy requests a Let’s Encrypt certificate. For the traditional stack, use your chosen ACME-compatible certificate procedure and configure Nginx to listen on 443, redirect HTTP to HTTPS, and pass the original protocol through with X-Forwarded-Proto. Renewals must be tested before the first certificate expires.
Do not terminate TLS at Nginx and then configure Rails as though every request were plain HTTP. Rails URL generation, secure cookies, and redirect behavior depend on correct proxy headers and trusted proxy configuration.
10. Verify the deployment before launch
Run these checks from the VPS and from an external machine. They cover the failure points that a successful package installation does not prove.
- Confirm services:
systemctl status postgresql,systemctl status nginx, and eithersystemctl status myapp-pumaor the Kamal service status. - Confirm database credentials with
psqlusing the same host, database, role, and password supplied to Rails. - Confirm migrations: run
RAILS_ENV=production bin/rails db:prepareor inspect the release task output and verify the expected schema version. - Confirm Rails logs show a successful boot without missing credentials, missing assets, or connection errors.
- Request Puma directly over its local socket or port and verify a valid Rails response.
- Request the public HTTP endpoint through Nginx, then request the HTTPS endpoint and inspect the certificate and redirect.
- Restart each supervised service deliberately and confirm it returns without manual intervention.
- Reboot the VPS during a maintenance window and verify that PostgreSQL, Puma or Kamal, and Nginx return in the correct order.
Common failures and their fixes
Rails cannot connect to PostgreSQL
Check that PostgreSQL is active, the database role exists, the password is current, and the host matches the network layout. A host process can use 127.0.0.1; a container may need the host gateway or a private network address. Test with psql first, then compare the effective Rails environment.
Free tools Windows power users keep installed
One-click scans. No signup required.
Bundler reports an incompatible Ruby or native extension
Read the project’s version file and lockfile, install that Ruby with rbenv, and run ruby --version as the same user that systemd or Capistrano uses. Install libpq-dev before rebuilding the pg gem.
Puma starts manually but not under systemd
Systemd does not load your interactive shell profile in the same way. Use the absolute rbenv shim path, set WorkingDirectory, define RAILS_ENV, provide the environment file, and ensure the service user can read the release and socket directories. The journal normally contains the first missing path or variable.
Nginx returns 502
Verify that Puma is active, the socket exists, and Nginx can traverse every parent directory. Confirm the socket path in puma.rb exactly matches proxy_pass. If Puma is listening on a TCP port instead, proxy to that port rather than a nonexistent socket.
Let’s Encrypt cannot issue a certificate
Check public DNS, confirm ports 80 and 443 reach this VPS, remove conflicting proxy records while issuing the certificate, and verify the hostname in the proxy configuration. Kamal’s certificate request cannot succeed while the domain resolves to another server.
The Tool Desk
Outbyte Driver Updater FREEFix the driver behind crashes, sound loss and screen glitchesFind Drivers →Outbyte PC Repair FREEClear out junk files and repair common Windows errorsFree Scan →Deployments exhaust database connections
Count connections from every Puma worker, thread pool, console, and background process. Reduce pool or worker counts, increase PostgreSQL’s permitted connections only when the VPS has memory for them, and account for the memory used by prepared statements. If an external pooler is present, review advisory-lock and prepared-statement settings rather than copying a single-server configuration unchanged.
Quick Recap
Operational practices after the first release
- Keep Ruby, Rails, PostgreSQL, the operating system, and container images patched on a planned schedule.
- Back up PostgreSQL and test restoring a backup; a deployment is not a recovery plan.
- Monitor disk space, memory, swap, PostgreSQL connections, Puma restarts, Nginx errors, and certificate expiry.
- Make migrations backward-compatible when deploying a new application version so old and new processes can overlap during a rolling or zero-downtime release.
- Keep release logs and record which commit, Ruby version, database migration, and image tag is active.
- Use a staging VPS or equivalent pre-production environment to test migrations, asset builds, and certificate configuration before changing the production domain.
Final launch checklist
- Ubuntu LTS is updated, SSH keys work, root/password SSH access is restricted, and only required firewall ports are open.
- The application’s Ruby version is pinned and the deployment user runs the same Ruby and Bundler versions used during installation.
- PostgreSQL is enabled, the application role is not a superuser, and a direct
psqllogin succeeds. database.ymlandDATABASE_URLhave an intentionally documented relationship, with a pool sized for all application processes.- Production secrets and the Rails master key are outside Git and readable only by the deployment process.
- Database preparation, migrations, and asset compilation complete successfully in production mode.
- Kamal or Puma is supervised, Nginx proxies correctly, and service status remains healthy after a restart.
- DNS resolves to the VPS, HTTPS is valid, HTTP redirects as intended, and certificate renewal has an owner.
- Logs, backups, monitoring, and a rollback procedure are written down before public traffic is enabled.
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.




