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(everyrails newwrites one), therubyline in theGemfile, orGemfile.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 apackage.json, its dependencies are installed too. - Build runs
bundle exec rake assets:precompilewhen theGemfilehaspropshaftorsprockets. API-only apps skip it. - Start runs
bin/rails serveron0.0.0.0and thePORTwe 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:
- Copy
db/queue_schema.rb,db/cache_schema.rbanddb/cable_schema.rbinto normal migrations, then delete the schema files. - Remove
config.solid_queue.connects_tofromproduction.rb, thedatabase:line underproductioninconfig/cache.yml, andconnects_tofromconfig/cable.yml. - Push.
db:prepareruns 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
- Sign in with GitHub and install the EthioDeploy app on the repo.
- Click “New Project” and pick the repo.
- 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
All guides on the deploy guides page. Custom domains, env vars, addons and billing are in the docs.