Deploying Rails With No Web Server in Front of It
Rails deploys here without drama, but three of its production defaults assume things this platform doesn't provide, and each fails in a way that doesn't point at itself. There's no nginx or Apache in front of your container, so Rails has to serve its own static files or your CSS 404s while the app reports healthy. config/master.key isn't in your image — it's gitignored, correctly — so the key has to arrive as an environment variable or the app refuses to boot. And the image build receives no environment variables, which is exactly when assets:precompile wants a SECRET_KEY_BASE. Sort those three and the rest is an ordinary Rails deployment.
Nothing is in front of you
On a traditional deployment, nginx serves /assets/* straight from disk and forwards everything else to Puma. Here the edge terminates TLS and routes by hostname — your container is the whole web tier.
So Rails must serve its own static files. The setting is config.public_file_server.enabled, which per the Rails guides "configures whether Rails should serve static files from the public directory. Defaults to true. If the server software (e.g. NGINX or Apache) should serve static files instead, set this value to false."
The default is what you want. The catch is that generated production configs often gate it behind an environment variable precisely because a web server is assumed — so open config/environments/production.rb and check. If the line reads from an env var, set that variable on the app; if it's hardcoded false, change it.
The symptom when this is wrong is distinctive: your pages render, every stylesheet and script 404s, and nothing in the logs looks like an error. Nothing probes your app to notice either.
The master key has to be an environment variable
config/master.key is in .gitignore for good reasons, which means it isn't in your repository, which means it isn't in your image. Without it, encrypted credentials can't be decrypted.
Rails is unambiguous about the consequence — config.require_master_key "causes the app to not boot if a master key hasn't been made available through ENV["RAILS_MASTER_KEY"] or the config/master.key file."
So set RAILS_MASTER_KEY as an environment variable on the app, with the contents of your local config/master.key. That's the intended path, and it's the same discipline as any other secret: in the deployment, not in the artifact.
Two related notes. The guides recommend keeping secret_key_base in credentials rather than as a variable — "it is recommended to leave this unset, and instead to specify a secret_key_base in config/credentials.yml.enc" — so the master key is usually the only secret you need to hand over. And if master.key was ever committed, treat it as a leaked credential: it decrypts everything in your credentials file.
Precompiling assets in a build with no environment
Rails wants a secret key base at boot, and assets:precompile boots the app. But the image build receives no environment variables and no build arguments — so anything expecting RAILS_MASTER_KEY or SECRET_KEY_BASE at build time gets nothing.
Recent Rails versions handle this with a dummy value for precompilation, which is what the generated Dockerfile uses. If yours doesn't, provide a throwaway value in the build stage only:
RUN SECRET_KEY_BASE=dummy bundle exec rails assets:precompile
It's used to boot the asset pipeline and never to sign anything real — the actual key arrives at runtime.
Watch the memory here too. If your app bundles JavaScript, precompilation is the step most likely to exceed the build container's roughly 0.75 GB. Building in CI and pushing the image sidesteps both this and the missing-environment problem at once.
Logs go to stdout, or nowhere useful
By default Rails writes to log/production.log — a file inside the container, which is discarded on every deploy and invisible from the panel in the meantime.
Configure the logger to write to STDOUT so output reaches the log stream. Generated production configs usually have this behind a check; confirm yours is active rather than assuming. A Rails app logging to a file it will never show anyone is one of the quieter ways to lose an afternoon.
Size Puma for a fraction of a core
Puma runs workers (processes) and threads, and its defaults are written for a machine with several cores.
A container sees the host's cores, not your quota, so an unconfigured Puma can fork several workers to share a CPU allocation that is a time budget for the whole container. Each worker is also a full copy of your app in memory, against a hard RAM limit.
On a small allocation:
- One worker.
WEB_CONCURRENCY=1. Extra workers multiply memory and split the same CPU budget. - A few threads. Threads are cheap for I/O-bound work, which most Rails requests are. Keep
RAILS_MAX_THREADSmodest and match your database pool to it.
This is the same trap as Node's cluster, Python's default thread pool, and Go before 1.25 — every runtime that sizes concurrency from nproc.
Migrations, deliberately
Putting rails db:migrate in your container's start command means it runs on every boot — every deploy, every configuration change, and every iteration of a crash loop.
Run migrations as a deliberate step from the app's shell instead, or gate them behind a variable you set for one deploy. A half-applied migration during a restart is a bad way to spend an evening.
Where the database goes
There's no managed database, so either run Postgres as its own app and set DATABASE_URL yourself, or use SQLite on a volume — which for a small app is genuinely a reasonable production choice, since the volume is local disk and one process owns it.
Whichever you pick, the database file or connection must not live inside the container's own filesystem.
FAQ
Why do my Rails assets 404 in production?
Nothing serves static files in front of your container, so Rails has to. Check config.public_file_server.enabled in your production config.
Why won't my Rails app boot after deploying?
Most likely the master key. Rails refuses to start if no key is available via ENV["RAILS_MASTER_KEY"] or config/master.key, and the file isn't in your image.
Why does asset precompilation fail during the build?
It boots the app, which wants a secret key base, and the build has no environment variables. Pass a dummy value in the build stage.
How should I configure Puma?
One worker and a small number of threads on a fractional CPU. Defaults assume multiple cores that your container can see but can't use.
Where do Rails logs go?
To a file inside the container unless you configure the logger to write to stdout — do that, or you won't see them.
Can I use SQLite in production?
For a small single-process app, yes, on a volume. Use Postgres when a second app needs the same data.
Wrapping up
Rails carries decades of assumptions about the machine it runs on: a web server in front, a key file on disk, a log directory somebody reads. None of them hold in a container here, and all three fail quietly rather than loudly. Serve your own assets, hand over the master key as a variable, give the build a dummy secret so precompilation can run, and tell Puma it has less machine than it thinks. After that it's just a Rails app.