# Ruby and Rails apps

> Install Ruby, deploy Rails, Sinatra or other Rack apps from Git with Bundler and Puma, precompile assets, connect a database and set environment variables.

Source: https://zopanel.net/docs/ruby  
Updated: 2026-10-09

Ruby applications run on ZoPanel as server apps: ZoPanel installs gems with Bundler, builds the release, starts the app as a service of the hosting account behind nginx and switches to new releases with no downtime. This page covers Ruby on Rails, Rack apps (Sinatra, Hanami…) and plain Ruby servers; ports, blue/green releases and logs work as described in [Node.js and Python apps](/docs/apps-node-python).

## Install Ruby on the server

An administrator installs Ruby once:

1. Open **Runtimes**.
2. In the **Ruby** card ("Ruby with Bundler, for Rails and Sinatra."), click **Install**.

ZoPanel installs the distribution's Ruby with Bundler and the libraries native gems need:

```text
ruby-full ruby-bundler build-essential libyaml-dev zlib1g-dev libssl-dev
libpq-dev libsqlite3-dev default-libmysqlclient-dev pkg-config
```

So gems such as `pg`, `mysql2`, `sqlite3`, `psych` and `nokogiri` compile without extra steps. The card then shows the installed version.

There is one Ruby per server, the one the operating system ships:

| Operating system | Ruby | Newest Rails it runs |
| --- | --- | --- |
| Ubuntu 22.04 | 3.0 | Rails 7.1 |
| Debian 12 | 3.1 | Rails 7.2 |
| Ubuntu 24.04 | 3.2 | Rails 8 |
| Debian 13 | 3.3 | Rails 8 |

Rails 8.0 requires Ruby 3.2 or newer and Rails 7.2 requires Ruby 3.1 or newer ([Rails upgrade guide](https://guides.rubyonrails.org/upgrading_ruby_on_rails.html)). A `ruby "x.y"` line in the `Gemfile` must match the server's Ruby, otherwise Bundler stops. The **Version** field and the `Gemfile` do not select another Ruby. Every build log starts with the Ruby in use, for example "Using the system Ruby 3.2.3", and adds a warning when the `Gemfile` asks for another version. If your app needs another Ruby version, deploy it with a Dockerfile instead (see [Deploy with Docker](/docs/docker-deploy)).

## Deploy

1. In **Websites → New website**, choose **Git deploy**, enter the **Repository URL** and **Branch** (and **Root directory** for monorepos), then click **Create website**. For an existing website, use its **Deploy** tab.
2. ZoPanel detects Ruby from the `Gemfile` and proposes the commands below.
3. Add your settings in **Environment variables** and click **Save & redeploy**.

### Detected commands

Every Ruby app gets the same install command. Gems go into the release (`vendor/bundle`), never system-wide, and development and test groups are skipped:

```bash
bundle config set --local path vendor/bundle && bundle config set --local without 'development test' && bundle install --jobs 4
```

| Detected as | When | Build command | Start command |
| --- | --- | --- | --- |
| Ruby on Rails | `config/application.rb` exists or the `Gemfile` lists `rails` | `RAILS_ENV=production SECRET_KEY_BASE=precompile bundle exec rails assets:precompile`, only when `app/assets/config/manifest.js` or `app/javascript` exists | `RAILS_ENV=production bundle exec rails server -b 127.0.0.1 -p $PORT` |
| Rack (Sinatra, Hanami…) | `config.ru` exists | – | `bundle exec rackup -o 127.0.0.1 -p $PORT -E production` |
| Ruby | anything else | – | `bundle exec ruby app.rb -o 127.0.0.1 -p $PORT` (check it) |

For Rails, **Persistent paths** are set to `storage` and `log`, and the deploy log notes "Set SECRET_KEY_BASE and DATABASE_URL in the environment variables". A `web:` line in a `Procfile` always provides the start command instead.

To change a command, turn **Auto-detect** off in the **Build & run** card, edit it and click **Save & deploy**.

### Puma

`rails server` starts Puma, the default server of new Rails apps (`gem "puma"` in the `Gemfile`). The `-b` and `-p` options bind it to `127.0.0.1:$PORT`. Puma's thread count comes from `config/puma.rb` (`RAILS_MAX_THREADS`, 3 by default in Rails 8). To start Puma directly, use for example:

```bash
bundle exec puma -C config/puma.rb -b tcp://127.0.0.1:$PORT -e production
```

For Rack apps on Rack 3 (Sinatra 4 and later), the `rackup` command is a separate gem: add `gem "rackup"` and `gem "puma"` to the `Gemfile` ([Sinatra README](https://sinatrarb.com/intro.html)).

## Environment variables

Set these in **Environment variables** on the **Deploy** tab. They are available during the build and at runtime; `PORT` is set automatically.

| Variable | Value |
| --- | --- |
| `SECRET_KEY_BASE` | A long random secret, for example the output of `bin/rails secret` or `openssl rand -hex 64`. Rails uses it instead of the one in encrypted credentials. |
| `RAILS_MASTER_KEY` | Only if the app reads other values from `config/credentials.yml.enc`. |
| `DATABASE_URL` | Connection string (see below). |
| `RAILS_LOG_TO_STDOUT` | `1` for apps older than Rails 7.1, so logs appear in **Application output**. Rails 7.1 and later log to stdout by default. |
| `SOLID_QUEUE_IN_PUMA` | `1` to run Solid Queue jobs inside Puma (Rails 8 default `config/puma.rb`). |

The build command sets a placeholder `SECRET_KEY_BASE` for `assets:precompile` only, so the precompile does not need your real secret.

## Database

**SQLite (Rails 8 default).** New Rails 8 apps keep their production databases in `storage/` (`storage/production.sqlite3` and the cache, queue and cable databases). `storage` is a persistent path, so the data survives every deploy. Nothing else is needed besides migrations.

**MariaDB or PostgreSQL.** Create a database under **Databases** and set `DATABASE_URL`; the host is `127.0.0.1` ([Rails database configuration](https://guides.rubyonrails.org/configuring.html#configuring-a-database)):

```dotenv
# MariaDB with the mysql2 gem
DATABASE_URL=mysql2://alice_app:your-password@127.0.0.1:3306/alice_app
# MariaDB with the trilogy gem (Rails 7.1+)
DATABASE_URL=trilogy://alice_app:your-password@127.0.0.1:3306/alice_app
# PostgreSQL with the pg gem
DATABASE_URL=postgresql://alice_app:your-password@127.0.0.1:5432/alice_app
```

URL-encode special characters in the password (for example `@` as `%40`).

### Migrations

Migrations do not run by themselves. Turn **Auto-detect** off and append them to the build command:

```bash
RAILS_ENV=production SECRET_KEY_BASE=precompile bundle exec rails assets:precompile && RAILS_ENV=production bundle exec rails db:prepare
```

`db:prepare` creates missing databases and runs pending migrations. The build runs before the new release goes live, while the old one keeps serving, so write migrations the running release can tolerate.

## SSL, static files and uploads

- nginx terminates SSL and sends `X-Forwarded-Proto`. Rails 8 sets `config.assume_ssl` and `config.force_ssl` in production, and Rails 7.1 sets `config.force_ssl`: keep **Free SSL (Let's Encrypt)** on, otherwise secure cookies and redirects break sign-in over plain HTTP.
- nginx forwards every request to the app, including files in `public/`. Rails 7.1 and later serve them by default; for Rails 7.0 set `RAILS_SERVE_STATIC_FILES=1`.
- Active Storage's local disk writes to `storage/`, which is persistent. Add any other folder that must survive a release, such as `public/uploads`, to **Persistent paths**.

## Releases, logs and restarts

- A new release starts in the idle slot and must answer the **Health check path** (default `/`) with a status below 500 within 90 seconds; redirects count as healthy. Rails 7.1 and later include a `/up` route you can use. Only then does nginx switch over and the old process stop.
- **Releases** keeps the last 5 builds for **Rollback** (the database is not rolled back).
- **Application output** shows the last 400 lines the app writes; turn on **Live** to follow it. The `log/` folder is persistent too.
- The service restarts automatically if Puma exits. Memory and CPU count toward the account's package.

## Troubleshooting

| Message or symptom | What to do |
| --- | --- |
| `bundle: command not found` in the install step | Ask the administrator to install Ruby under **Runtimes**. |
| "Your Ruby version is …, but your Gemfile specified …" | The `Gemfile` asks for another Ruby than the server's (the build log warns about it before the install). Relax the `ruby` line (for example `ruby "~> 3.2"`, or remove it), or deploy with a Dockerfile. |
| A gem fails to compile | Read the build log in **Tasks**; the usual headers are installed with Ruby. Other native libraries must be installed by the administrator. |
| "the app did not respond on port … within 90s (it must listen on $PORT)" | Bind to `127.0.0.1:$PORT` (`-b 127.0.0.1 -p $PORT`). |
| "the app exited during startup" | Read **Application output**: often `SECRET_KEY_BASE` or `DATABASE_URL` is missing, or a migration has not run. |
| `ActiveRecord::PendingMigrationError` | Add `bundle exec rails db:prepare` to the build command (see Migrations). |
| Sign-in loops or cookies are lost | The site runs without SSL while Rails forces SSL. Issue the certificate (see [SSL certificates](/docs/ssl)). |
| The repository is detected as Node.js | A `package.json` at the root (jsbundling, cssbundling) takes priority over the `Gemfile`. Turn **Auto-detect** off, set **Runtime** to `ruby`, **Type** to **Server app (process)** and enter the Rails commands above; put `npm ci &&` in front of the build command if assets need Node.js. |

## Related

- [Node.js and Python apps](/docs/apps-node-python)
- [Git deploy](/docs/git-deploy)
- [Databases](/docs/databases)
- [SSL certificates](/docs/ssl)
- [Rails guides](https://guides.rubyonrails.org/)
