EthioDeploy
DocsPricingDomainsBlogChangelog
Deploy guides/Rails

Deploy a Rails app

Rails is detected from the Gemfile and built with nixpacks, not the Dockerfile Rails generates. Set RAILS_ENV and your master key, point the health check at /up, add Postgres, and push.

Last updated October 29, 2026

What we run by default
Detected by
Gemfile + config/application.rb
Install
bundle install
Build
bundle exec rake assets:precompile (with propshaft or sprockets)
Start
bundle exec bin/rails server -b 0.0.0.0 -p $PORT -e $RAILS_ENV

Override any of them in Settings → Build.

Fix this first: Set RAILS_ENV and RAILS_MASTER_KEY before the first deploy

The default start command passes -e $RAILS_ENV to rails server, and nothing sets RAILS_ENV for you. Add RAILS_ENV=production to the project's env vars. Production also needs RAILS_MASTER_KEY (the contents of config/master.key, which git ignores) or a SECRET_KEY_BASE. Without one, Rails raises a missing secret_key_base error.

Prepare your app

We build Ruby apps with nixpacks. The Dockerfile that rails new generates since Rails 7.1 isn't used, and neither is Thruster. You can keep the file for other hosts. The build:

  • Ruby version comes from .ruby-version (every rails new writes one), the ruby line in the Gemfile, or Gemfile.lock. It's compiled with rbenv during the build, so Ruby builds take longer than Node or Python ones.
  • Install runs bundle install. If the repo has a package.json, its dependencies are installed too.
  • Build runs bundle exec rake assets:precompile when the Gemfile has propshaft or sprockets. API-only apps skip it.
  • Start runs bin/rails server on 0.0.0.0 and the PORT we set, so there is nothing to configure for the port.

If the repo has a Procfile, its web line is used as the start command instead. Rails only generates Procfile.dev, which is ignored.

Environment variables

Add these in the project's settings before the first deploy:

RAILS_ENV=production
RAILS_MASTER_KEY=...       # contents of config/master.key

Rails reads SECRET_KEY_BASE from the environment first, then from the encrypted credentials. If you don't use credentials, set SECRET_KEY_BASE to the output of bin/rails secret instead of the master key. The build runs assets:precompile in production mode, and env vars are available during the build, so the same values work there. See Environment variables.

Production settings

The platform terminates HTTPS and forwards plain HTTP to your app. Tell Rails so in config/environments/production.rb:

config.assume_ssl = true
config.force_ssl = true

Both lines are already there, commented out. Rails turns them on itself only when you generate the app with --skip-kamal.

Host authorization is off in production until you set config.hosts. If you set it, include your <project-slug>.ethiodeploy.app address and skip the check for the health route, as Rails' own comments suggest:

config.hosts = [
  "myapp.ethiodeploy.app",
  "myapp.com"
]
config.host_authorization = { exclude: ->(request) { request.path == "/up" } }

Health check

Every deploy requests your health check path (/ by default) and only goes live once it returns a 2xx or 3xx status. The platform keeps checking it after that. If / returns 404 or needs a login, add a route like /healthz that returns 200 and set it as the Health Check Path in Settings. Otherwise the deploy fails and the previous version keeps serving.

A new Rails app has no root route, so / returns 404 in production. Rails 7.1 and later add /up, which returns 200 once the app boots. Set the Health Check Path in Settings to /up. The check is sent with Host: <project-slug>.ethiodeploy.app, so a config.hosts list without that address fails it with a 403, unless /up is excluded.

Add Postgres

Add a Postgres addon and DATABASE_URL is injected automatically. Rails reads it with no code changes and maps its postgres:// scheme to the PostgreSQL adapter. Add the driver if the app started on SQLite:

bundle add pg

DATABASE_URL only fills in the primary database. Rails 8 also lists cache, queue and cable databases under production in config/database.yml, and those still point at SQLite files or a local server. Move them into the main database (next section) and keep one entry:

production:
  <<: *default
  url: <%= ENV["DATABASE_URL"] %>

Run migrations before the server starts by setting the Start command in Settings → Build to:

bundle exec rails db:prepare && bundle exec bin/rails server -b 0.0.0.0 -p ${PORT:-3000}

db:prepare loads the schema into an empty database and runs pending migrations otherwise, so a new model goes live with the push that adds it. Browse the data with the built-in schema browser and SQL editor.

Solid Queue, Solid Cache and Solid Cable

Rails 8 stores jobs, cache entries and Action Cable messages in separate databases by default. The Solid gems' READMEs describe how to use your main database instead:

  1. Copy db/queue_schema.rb, db/cache_schema.rb and db/cable_schema.rb into normal migrations, then delete the schema files.
  2. Remove config.solid_queue.connects_to from production.rb, the database: line under production in config/cache.yml, and connects_to from config/cable.yml.
  3. Push. db:prepare runs the new migrations.

Addons are private to the project that owns them, so a separate worker project can't reach the database. Run Solid Queue inside Puma instead: set SOLID_QUEUE_IN_PUMA=1 and Rails 8's config/puma.rb loads the plugin.

File uploads

New apps store Active Storage files on the local disk (config.active_storage.service = :local). Each deploy starts a fresh container, so those files are lost on the next push. Use the amazon service in config/storage.yml with any S3-compatible storage, and add the aws-sdk-s3 gem.

Deploy a Rails app for free

The Free plan costs nothing and needs no card. It includes:

  • One project, live over HTTPS at <project-slug>.ethiodeploy.app.
  • 256 MB of memory and half a CPU core.
  • A managed Postgres, MySQL or Redis addon with 512 MB of storage. It doesn't expire.
  • A redeploy on every push, the same as the paid plans.

The catch is sleep. After 30 minutes without traffic the app stops, and the next request starts it again. We hold that request for up to 30 seconds while the app boots, so the first visitor after a quiet spell waits a little.

Rails' config/puma.rb runs one Puma process with 3 threads unless WEB_CONCURRENCY or RAILS_MAX_THREADS is set. Leave WEB_CONCURRENCY unset on the Free plan: each extra worker is another process in the same 256 MB. Add the Postgres addon from the section above so your data survives deploys.

The Solid Queue plugin forks its own worker and dispatcher processes by default. To keep them inside the Puma process, replace the plugin line in config/puma.rb with:

if ENV["SOLID_QUEUE_IN_PUMA"]
  plugin :solid_queue
  solid_queue_mode :async
end

solid_queue_mode only exists once the plugin is loaded, so keep it inside the same check. Jobs stop while the app sleeps, so recurring tasks only run while it's awake.

When the app has to stay awake or needs your own domain, Pro is $10 or 1,399 ETB a month: always on, 1 GB of memory and a 5 GB database.

Deploy

  1. Sign in with GitHub and install the EthioDeploy app on the repo.
  2. Click “New Project” and pick the repo.
  3. Watch the build in the dashboard. When the health check passes, your app is live at <project-slug>.ethiodeploy.app. Every push to the default branch redeploys, and the old version keeps serving until the new one is healthy.

Troubleshooting

ArgumentError: Missing `secret_key_base` for 'production' environment+

Cause: Neither RAILS_MASTER_KEY nor SECRET_KEY_BASE is set.

Fix: Add RAILS_MASTER_KEY from config/master.key, or a SECRET_KEY_BASE from bin/rails secret.

Deploy fails: GET / returned 404+

Cause: The app has no root route, and / is the default health check path.

Fix: Set the Health Check Path in Settings to /up.

Deploy fails with a 403, and the logs say Blocked hosts+

Cause: config.hosts is set but doesn't include the <project-slug>.ethiodeploy.app address the health check sends.

Fix: Add that address to config.hosts, or exclude /up with config.host_authorization.

PG::ConnectionBad on start+

Cause: There is no Postgres addon, or a cache, queue or cable entry in database.yml still points at a local server.

Fix: Add the addon and reduce production in database.yml to one entry with url: <%= ENV["DATABASE_URL"] %>.

Pages load but CSS and JS return 404+

Cause: assets:precompile didn't run: the Gemfile has neither propshaft nor sprockets, or a custom Build command replaced it.

Fix: Make the Build command bundle exec rake assets:precompile, plus anything else you need.

Build fails: Please specify ruby's version in .ruby-version file+

Cause: The repo has no .ruby-version and no ruby line in the Gemfile or Gemfile.lock.

Fix: Commit a .ruby-version file, for example 3.4.1.

Still stuck? Check the deployment's build log in the dashboard, or see Troubleshooting.

Questions

Yes. The Free plan runs one project with a 512 MB Postgres database at no cost. The app sleeps after 30 minutes without traffic and wakes on the next request, and the container has 256 MB of memory, so keep Puma to one process. Pro ($10/month) keeps it always on with 1 GB.

No. Rails apps are built with nixpacks, which doesn't read the Dockerfile, so Thruster and the Dockerfile's port aren't used. The app runs bin/rails server on the PORT we set. You can keep the file.

Set the start command to bundle exec rails db:prepare && bundle exec bin/rails server -b 0.0.0.0 -p ${PORT:-3000}. db:prepare loads the schema into an empty database and runs pending migrations on every deploy.

The one in .ruby-version, which rails new creates. Without it, the ruby line in the Gemfile or Gemfile.lock is used.

Ready to deploy?

Sign in with GitHub and pick your repo. No Dockerfile, no YAML.

More guides

ExpressNestJSGo

All guides on the deploy guides page. Custom domains, env vars, addons and billing are in the docs.

On this page
Prepare your appEnvironment variablesProduction settingsHealth checkAdd PostgresSolid Queue, Solid Cache and Solid CableFile uploadsDeploy a Rails app for freeDeployTroubleshootingQuestions
Deploying with an agent?Point it at llms.txt. Every guide is in one file there.Open llms-full.txt →
EthioDeploy

Agentic first development and deployment, started in Ethiopia, built for developers everywhere.

Telegram community
Product
PricingAI coding agentAgent mode docsDomainsCLIFree backend hostingHosting in EthiopiaChangelog
Deploy guides
Next.jsNode.jsPythonSvelteKitTanStack StartDjangoFastAPIFlaskExpressNestJSLaravelRailsGoTelegram botLovableBoltReplitv0Google AI StudioBase44Firebase StudioClaude Code & Cursor
Compare
vs Herokuvs Vercelvs Rendervs Railwayvs Fly.iovs NetlifyHeroku alternativesVercel alternativesNetlify alternativesRender alternativesRailway alternatives
Resources
DocsBlogAboutSupportContactllms.txt
© 2026 EthioDeployPay by card in USD or bank transfer in ETBTermsPrivacy