# Deploy Ruby on Rails to a VPS with Puma and Postgres Deploy Rails 8 to your own VPS without Docker or Kamal: precompiled Ruby, gems, assets, db:prepare and Solid Cache, Queue and Cable on PostgreSQL. To deploy Ruby on Rails to a VPS with ox, add the repo as a project, let ox detect puma, the gems, the asset build and `db:prepare`, set `RAILS_MASTER_KEY`, and deploy. ox installs a precompiled Ruby, runs puma as a systemd service behind Caddy, with no Docker, no Kamal and no nginx to configure, and gives each of Rails 8's databases its own PostgreSQL database. Migrations run after a snapshot of every database, and traffic moves to the new release only after `/up` answers. ## What ox detects in a Rails repo An app made with `rails new --database=postgresql` deploys with no ox.toml at all. From `Gemfile.lock`, `config/` and the Ruby version file, ox proposes: - Ruby from `.ruby-version`, the `ruby` line in the `Gemfile`, or `RUBY VERSION` in `Gemfile.lock`, installed as a precompiled build, so nothing compiles Ruby on your server. - `bundle config set --local deployment true && bundle config set --local without development:test && bundle install` as the install step. Gems go into the release's own `vendor/bundle`, so two releases never share or break each other's gems. - `RAILS_ENV=production SECRET_KEY_BASE_DUMMY=1 bundle exec rails assets:precompile` as a build command, when the app uses Propshaft, Sprockets, jsbundling, cssbundling or Vite Ruby. - `RAILS_ENV=production bundle exec rails db:prepare` as the migrate step. It runs pending migrations and loads the schema of a database that is still empty, which the Solid Cache, Queue and Cable databases need on the first deploy. - `RAILS_ENV=production bundle exec puma -C config/puma.rb -b tcp://127.0.0.1:$PORT` as the start command, and `/up` as the health check when `config/routes.rb` routes `/up` to `rails/health#show`, as Rails 7.1 and later generate. - PostgreSQL when `pg` is in the lock, plus one more PostgreSQL database for each extra production database in `config/database.yml` (`cache`, `queue`, `cable`), and Redis when `redis` or `sidekiq` is. - The apt packages that native gems build against, such as `libvips-dev` for `ruby-vips` or `libpq-dev` for a `pg` without a Linux build in the lock. `build-essential` always comes with Ruby. - `RAILS_MASTER_KEY` as a required variable when `config/credentials.yml.enc` is in the repo. ## The ox.toml for Rails Write one when you want a domain, a Solid Queue worker or uploads that survive deploys. Once a repo has an ox.toml, its services are exactly the ones it lists, so the four databases are declared here. ox names each database URL after its entry, so `cache` gives `CACHE_DATABASE_URL`, which Rails reads for its `cache` database. ox.toml for a Rails 8 app: ```toml domains = ["example.com"] [app] start = "RAILS_ENV=production bundle exec puma -C config/puma.rb -b tcp://127.0.0.1:$PORT" health = "/up" [build] install = "bundle config set --local deployment true && bundle config set --local without development:test && bundle install" commands = ["RAILS_ENV=production SECRET_KEY_BASE_DUMMY=1 bundle exec rails assets:precompile"] migrate = "RAILS_ENV=production bundle exec rails db:prepare" [workers] jobs = "RAILS_ENV=production bin/jobs" [services] postgres = {} cache = { type = "postgres" } queue = { type = "postgres" } cable = { type = "postgres" } [storage] keep = ["storage"] ``` ox sets no framework variables of its own, so each command carries `RAILS_ENV=production`. The `-b` flag makes puma listen only on the port ox gives it, behind Caddy. `[workers] jobs` runs Solid Queue as its own service, and `[storage] keep` links `storage` to a writable folder that survives deploys, because a release's own files are read-only while it runs. Ruby and the packages still come from detection, and `ox check` shows them. ## Rails credentials and variables for production ox provides `DATABASE_URL`, one `*_DATABASE_URL` per extra database, `PUBLIC_URL` and `PUBLIC_HOST`. Rails reads `DATABASE_URL` for the primary database and `CACHE_DATABASE_URL`, `QUEUE_DATABASE_URL` and `CABLE_DATABASE_URL` for the others, and they win over the names and passwords in `config/database.yml`, so the file `rails new` wrote works unchanged. Set `RAILS_MASTER_KEY` to the contents of `config/master.key` on the dashboard or with `ox vars set RAILS_MASTER_KEY`, which asks for the value. Never commit `master.key`. Until the key is set, a deploy stops before it builds and names the missing key. ## Check and deploy the Rails app Run `ox check` in the repo. For a `rails new` app with the ox.toml above it prints the plan, then the variables, then `Ready to deploy.` ox check, last lines: ```text Provided by ox: PORT, HOST, OX_ENV, OX_PROJECT, OX_RELEASE, OX_DATA_DIR, PUBLIC_URL, PUBLIC_HOST, CABLE_DATABASE_URL, CACHE_DATABASE_URL, DATABASE_URL, QUEUE_DATABASE_URL Set on the dashboard before the first deploy: RAILS_MASTER_KEY Ready to deploy. ``` Then deploy with `ox deploy --wait`. The first deploy installs Ruby and every gem, so it takes a few minutes. If the health check fails, [Troubleshooting](https://deploywithox.com/docs/troubleshooting#health-check-failed) explains the message. ## If ox check says no These are the messages `ox check` prints for a Rails repo it cannot deploy yet, and what to do about each. - `Gemfile.lock is missing: run bundle install and commit it (ox installs gems in deployment mode, which needs the lock)`. Commit the lock. - `a Ruby app needs a pinned Ruby, and ox never guesses one: add .ruby-version (or [tools] ruby = "3.4")`. Commit a `.ruby-version`; `rails new` writes one. - `Gemfile.lock has no Linux platform, so bundle install will refuse it on the server: run bundle lock --add-platform x86_64-linux and commit the lock`. This happens when the lock was made on a Mac. - `Rails with SQLite keeps its database in storage/, which is read-only in a release: add an ox.toml with [storage] keep = ["storage"]`. With that line the database lives in a folder that survives deploys. - `Solid Queue runs jobs only in a worker: add [workers] jobs = "RAILS_ENV=production bin/jobs"`. This is a hint: jobs wait in the queue until a worker runs. Jekyll sites are not supported yet, and ox says so instead of guessing. ## Next steps - [Set RAILS_MASTER_KEY and other variables](https://deploywithox.com/docs/operate/variables#set) from the dashboard or the CLI. - [Add your own domain with HTTPS](https://deploywithox.com/docs/operate/domains): one A record, and Caddy gets the certificate. - [Read and search the app's logs](https://deploywithox.com/docs/operate/logs), live or for a time range. - [Roll back a bad deploy](https://deploywithox.com/docs/operate/rollback) to a kept release without a rebuild. - [See the daily PostgreSQL backups](https://deploywithox.com/docs/operate/backups) and restore one. Last updated 2026-10-08. The page as HTML: https://deploywithox.com/docs/guides/rails