---
title: How to host a Rails app in Canada
description: >-
  Deploy a Rails app and its PostgreSQL database on Canadian infrastructure,
  using the production Dockerfile Rails already generates for you.
date: '2026-09-14'
lastUpdated: '2026-09-14'
keywords:
  - host rails app in canada
  - deploy rails canada
  - rails hosting canadian data residency
  - rails postgresql canada
  - deploy rails with docker coolify
  - rails solid queue production
type: article
author: Ross Hill
locale: en_CA
site_name: MapleDeploy
slogan: Powerful hosting on Canadian soil
organization_url: 'https://mapledeploy.ca/'
logo: 'https://mapledeploy.ca//api/logo/lockup'
creator: MapleDeploy
publisher: MapleDeploy
founding_date: '2026-01-13'
email: hello@mapledeploy.ca
geo_region: CA-ON
geo_placename: Toronto
address_country: CA
area_served: Canada
application_category: DeveloperApplication
app_url: 'https://app.mapledeploy.ca'
llms_txt: 'https://mapledeploy.ca/llms.txt'
offers: >-
  Starter $45/mo, Pro $95/mo, Ultra $195/mo, Ultra 32 $395/mo, Ultra 64 $695/mo
  CAD
in_language: en-CA
canonical_url: 'https://mapledeploy.ca/blog/deploy-rails-canada'
---

If you want a Rails app and its database running in Canada, you already have most of the deployment artifact. Since Rails 7.1, `rails new` [includes Docker-related files in the application](https://guides.rubyonrails.org/7_1_release_notes.html) by default, and that Dockerfile is written for production. That means the containerization work is already done for you. What is left is choosing a Canadian machine that can build a Dockerfile, putting PostgreSQL beside it, and getting four or five settings right.

This walks through that on a dedicated VM in Toronto running Coolify, which is what MapleDeploy provides. Most of it applies to any Docker host you pick.

## What the generated Dockerfile already does

Worth reading before you change anything, because people routinely rewrite parts that already work. In the [current template](https://github.com/rails/rails/blob/main/railties/lib/rails/generators/rails/app/templates/Dockerfile.tt):

- It is multi-stage. Build tools and Node modules stay in the throwaway stage, and the final image runs as a non-root `rails` user.
- It sets `RAILS_ENV="production"` and `BUNDLE_WITHOUT="development"`, so you do not need to pass those in.
- It precompiles assets during the build with `SECRET_KEY_BASE_DUMMY=1`. Your master key is not needed at build time, only at runtime.
- Since Rails 8, the default `CMD` starts [Thruster](https://github.com/basecamp/thruster) in front of Puma for X-Sendfile acceleration and asset caching, and the image exposes port 80 instead of 3000. Thruster only terminates TLS when you set `TLS_DOMAIN`, so behind a reverse proxy it stays in HTTP-only mode, which is what you want here.
- The entrypoint runs `./bin/rails db:prepare` when the container command ends in `./bin/rails server`. That is true with Thruster too, since `./bin/thrust ./bin/rails server` still ends in those two arguments.

That last point matters for the migration question below.

## Step 1: the server and the build pack

Create your server, open Coolify, and add your repository as a new application. The generic version of this is in our guide to [deploying your first app](/docs/deploy-your-first-app), so here are only the Rails-specific answers.

Set the build pack to **Dockerfile**, not Nixpacks. Nixpacks infers a build for projects that do not ship one, and it does that well. Rails already ships a production Dockerfile written for this purpose, so there is nothing to infer.

Set **Ports Exposes** to `80`. This is a common Rails deploy failure on a Docker platform, because 3000 is the usual assumption, and Rails changed it when Thruster arrived. If you generated the app with `--skip-thruster`, use `3000` instead. Check the `EXPOSE` line in your own Dockerfile rather than trusting either number.

## Step 2: PostgreSQL on the same server

Add PostgreSQL as a one-click database in the same Coolify instance. Coolify shows a **Postgres URL (internal)** value that resolves over the server's Docker network, so your app reaches the database without the connection ever leaving the machine. That also means one region to verify instead of two, which is the argument we make at more length in [where to run managed PostgreSQL in Canada](/blog/managed-postgresql-canada).

Paste that URL into your application's environment variables as `DATABASE_URL`. Rails merges it on top of `config/database.yml` rather than replacing the file, and [the configuring guide describes exactly how that merge resolves](https://guides.rubyonrails.org/configuring.html#connection-preference).

Then check something people miss. If you generated a Rails 8 app with Postgres and did not pass `--skip-solid`, your production block is a multi-database config: `primary`, `cache`, `queue`, and `cable`, each pointing at a separate database name. `DATABASE_URL` only covers the primary one. [Non-primary connections read their own variables](https://github.com/rails/rails/blob/main/railties/lib/rails/generators/rails/app/templates/config/databases/postgresql.yml.tt), formed by joining the connection name to `_DATABASE_URL`:

```
DATABASE_URL=postgres://user:pass@host:5432/app_production
QUEUE_DATABASE_URL=postgres://user:pass@host:5432/app_production_queue
CACHE_DATABASE_URL=postgres://user:pass@host:5432/app_production_cache
CABLE_DATABASE_URL=postgres://user:pass@host:5432/app_production_cable
```

Create those extra databases in your Postgres instance, or trim the production block down to a single primary if you are not using the Solid adapters. Either is fine. What breaks things is deploying with three of the four unset and not noticing.

## Step 3: the rest of the environment

`RAILS_MASTER_KEY` is the one secret the container genuinely needs. It is the contents of `config/master.key`, and Rails will refuse to boot without it when `config.require_master_key` is set. Leave it as a runtime variable. The build does not read it.

`RAILS_MAX_THREADS` sets the Active Record connection pool size in the generated `database.yml`, defaulting to 5. If you also run multiple Puma workers through `WEB_CONCURRENCY`, your total connection count is workers multiplied by threads, and Postgres has a finite `max_connections`. Do that arithmetic before you scale either number up.

## Step 4: migrations

You have two reasonable options, and one that looks right and is not.

**Let the entrypoint do it.** `db:prepare` creates the database if it is missing and migrates it if it exists. For most applications this is enough, and it is already wired up.

**Use Coolify's post-deployment command.** In the deployment lifecycle settings, a post-deployment command runs in the newly built container after the deployment completes. `bundle exec rails db:migrate` there gives you a migration step you can see and re-run in the deployment log.

The trap is the **pre-deployment** field sitting next to it. That one executes in the *existing* container, before the new one starts, so it runs your migration against the old code. Use it for cache warming or a maintenance flag, not for migrations that ship with the release.

## Step 5: background jobs

From Rails 8, [Solid Queue is the default Active Job backend](https://guides.rubyonrails.org/active_job_basics.html), and it is database-backed, so it needs no Redis. Two ways to run it:

- Run the supervisor with `bin/jobs` as a second Coolify resource pointing at the same repository and image. Jobs then scale and restart independently of your web process.
- Or set `SOLID_QUEUE_IN_PUMA`, which runs the supervisor inside Puma. A default Rails 8 app already generates the `plugin :solid_queue if ENV["SOLID_QUEUE_IN_PUMA"]` line in `config/puma.rb`, so the variable is usually the only thing you add. [Solid Queue's own README](https://github.com/rails/solid_queue) documents both, and recommends a separate queue database while supporting a single shared one.

Sidekiq remains the common choice for high-throughput queues, and it needs Redis: [Sidekiq requires Redis 7.0+, Valkey 7.2+, or Dragonfly 1.27+](https://github.com/sidekiq/sidekiq). Redis and Dragonfly are one-click resources in Coolify, running as containers on the same VM, so this is a small addition rather than another vendor. Valkey is not on Coolify's one-click list, so that one needs a Docker Compose resource instead.

## Step 6: domain, TLS, and the health check

Point an A record at your server's IP, put the full URL in Coolify's **Domains** field, and redeploy. Coolify requests the certificate from Let's Encrypt and renews it. The full version is in [custom domains](/docs/custom-domains).

Two Rails settings are worth revisiting at this point. Since Rails 8.1, a default `rails new` generates `config.assume_ssl` and `config.force_ssl` **commented out**. Rails 8.0 generated them already enabled. Behind Coolify's proxy you generally want both on, so check `config/environments/production.rb` and uncomment them if your generator left them commented.

Then set the Coolify health check path to `/up`, which Rails routes to `rails/health#show` by default. If you turned on `force_ssl`, an HTTP health check will get a redirect instead of a 200. The generated production config ships the fix commented out one line below:

```ruby
config.ssl_options = { redirect: { exclude: ->(request) { request.path == "/up" } } }
```

## One more Rails-specific gotcha: uploads

The generated config sets `config.active_storage.service = :local`, which writes to `storage/` inside the container. Container filesystems do not survive a redeploy. Either attach a persistent storage volume to that path in Coolify, or switch Active Storage to an S3-compatible service. Pick one deliberately, because the failure mode is user uploads quietly disappearing on your next deploy rather than an error you can see.

## What we manage, and what you manage

Worth being precise, because "managed" means different things on different platforms.

**We manage:** the VM in Toronto, the operating system and its security patching, the Coolify installation and its updates, uptime monitoring on your instance, and weekly full-server snapshots. Those snapshots are a disaster recovery layer and can be up to seven days old, so configure Coolify's [S3 database backups](/docs/backups) for anything you cannot afford to lose.

**You manage:** your Rails code, your Dockerfile, your environment variables, your migrations, your job supervisor, your Postgres tuning, and how you divide the server's RAM and CPU among the web process, the worker, and the database.

We do not review your Dockerfile, your Rails configuration, or your dependency versions. The steps above are yours to work through, and they are the same steps you would work through on any Docker host. You get root on the VM, so those decisions stay yours to make.

Plans are flat CAD per month, starting at $45 for 4 GB of RAM and 2 vCPUs, with no per-service billing for the database or the worker. They still share the plan's RAM and CPU, so size accordingly. Starter and Pro include a 30-day free trial, long enough to deploy the real app, run a real migration, and see what your build actually needs before you pay for it. If the reason you are reading this is jurisdiction rather than pricing, our page on [Canadian infrastructure](/canadian-hosting) covers ownership, residency, and the CLOUD Act question in full.

{% cta-section title="Put your Rails app on Canadian infrastructure" %}
Dockerfile builds, one-click PostgreSQL, and a dedicated Toronto VM. 30 days free on Starter and Pro.
{% /cta-section %}
