Rails 8 on a Free Container: Puma, 512 MB, and a PostgreSQL That Persists
Rails 8 ships with everything a single server needs: Puma behind Thruster, Solid Queue, Solid Cache and Solid Cable on the database, a production Dockerfile in the repository, and SQLite as the default database in production. That last default is the one that bites on a container host, and it is the first thing this guide changes. The rest is about fitting Rails into 512 MB and a quarter of a CPU without it feeling like a compromise.
Tested with Rails 8.1 (the current release; 8.0 receives security fixes only until November 2026) on Ruby 3.3. Nothing below depends on the minor version.
Why SQLite in production does not work here
Rails 8's "no PaaS required" story assumes one persistent server: the SQLite files live on its disk and survive restarts. A container's filesystem does not. Every deploy builds a new image, and a free container that sleeps after 15 minutes without traffic starts from that image again when it wakes. The database, the Solid Queue jobs and the cache all live in storage/*.sqlite3, so they would all vanish. On a container host you need PostgreSQL (or MySQL) for the primary database, and the Solid adapters can use the same database.
# new app
rails new myapp --database=postgresql
# existing app: config/database.yml, production section
production:
primary: &primary_production
url: <%= ENV["DATABASE_URL"] %>
cache:
<<: *primary_production
migrations_paths: db/cache_migrate
queue:
<<: *primary_production
migrations_paths: db/queue_migrate
cable:
<<: *primary_production
migrations_paths: db/cable_migratePointing all four at one DATABASE_URL is fine at this size; Solid Queue, Cache and Cable each create their own tables. Where the database comes from is the usual choice: a SnapDeploy PostgreSQL Mini ($29/month, 50 connections, injected as DATABASE_URL), a $1 DB Sprint Pack for 12 hours of the same, or a free external plan such as Neon (0.5 GB, 100 compute-hours a month).
Use the Dockerfile Rails already gave you
Since Rails 7.1, rails new writes a production Dockerfile, and the Rails 8 version is a good one: a multi-stage build on a slim Ruby image, jemalloc for lower memory use, assets precompiled with a dummy secret, a non-root user, and Thruster in front of Puma listening on port 80. It is better than anything a generic generator produces, including ours. SnapDeploy's generated Ruby Dockerfile builds on ruby:3.2-alpine, runs bundle install without the development and test groups, precompiles assets and starts rails server; it works for simple apps, but Rails 8 wants Ruby 3.2 or newer and the framework's own Dockerfile already knows the right incantations. When a repository contains a Dockerfile, SnapDeploy uses it and reads the port from its EXPOSE line.
Two lines in that Dockerfile matter for a 512 MB container. The CMD is ./bin/thrust ./bin/rails server; leave it. And the ENV block sets RAILS_ENV=production and turns on jemalloc; leave those too. What you add are environment variables in the container settings:
| Variable | Value on a free container | Why |
|---|---|---|
RAILS_MASTER_KEY | contents of config/master.key | Decrypts credentials; without it the app will not boot in production. |
DATABASE_URL | injected by the add-on or pasted | See above. |
WEB_CONCURRENCY | 1 | One Puma worker. Each worker is a full copy of the app in memory; two do not fit in 512 MB with headroom. |
RAILS_MAX_THREADS | 3 | Threads per worker, and the size of the connection pool. Three is plenty on a quarter of a CPU. |
SOLID_QUEUE_IN_PUMA | 1 | Runs the job supervisor inside the Puma process instead of a separate container (below). |
Background jobs on a container that sleeps
The default Rails 8 setup can run Solid Queue as a Puma plugin, and on a free container that is the only arrangement that works. A separate bin/jobs container never receives HTTP requests, so the platform sees it as idle and puts it to sleep after 15 minutes; jobs would run for a quarter of an hour after each deploy and then stop. Inside Puma, jobs run whenever the web container is awake, which for a hobby app is exactly when someone is using it. Recurring jobs (config/recurring.yml) fire on the same condition: while the app is awake. If jobs must run on a schedule regardless of visitors, that is what Always-On ($12/month) is for, and at that point a second Always-On container for bin/jobs is a clean split.
Memory, measured
A fresh Rails 8 app with one Puma worker, three threads and jemalloc settles around 150 to 200 MB resident after warm-up; a mid-sized app with a few dozen gems is closer to 250 to 300 MB. Both fit in 512 MB with room for the occasional spike. The numbers that break it are a second worker (double everything) or Sidekiq in the same container (another full copy of the app). If you see the container restart with exit code 137, it was killed for memory: check WEB_CONCURRENCY first.
Start-up on a quarter vCPU is 10 to 20 seconds for a typical app, mostly Zeitwerk eager loading and Puma booting. On a free container that time is added to the roughly one-minute wake after a sleep, which is why the wake page exists: visitors see a loading page rather than a timeout. Always-On removes both waits.
Deploying
- Commit the Rails-generated Dockerfile (it is already in a new app; for an older app run
rails app:updateor copy one from a freshrails new). - Push to GitHub. In SnapDeploy: Containers → Deploy from GitHub → pick the repository. Add the variables from the table.
- Attach a database: a PostgreSQL add-on injects
DATABASE_URL; otherwise paste one. - Deploy. The first build is a few minutes (gems and assets). Then run the migrations once from the container's shell or add
./bin/rails db:prepare &&in front of the start command for the first deploy. - Open
/up, the health endpoint Rails 7.1+ ships with. It returns 200 when the app booted.
Where free stops for Rails
- Active Storage uploads must go to S3 or a compatible bucket, not the local disk.
- Action Cable works while the container is awake, but WebSocket traffic is not counted as activity (only HTTP responses are), so a page that only holds a socket open will still sleep after 15 minutes.
- Custom domains need Always-On. Free containers are on
containers.snapdeploy.app. - 100 container-hours a month, counted only while awake. A Rails app visited a few times a day uses a fraction; one you keep pinging to avoid the wake uses all of it.
Numbers used: free container 512 MB, 0.25 vCPU, sleeps after 15 minutes, wakes in about 60 seconds; Always-On Small $12/month; PostgreSQL Mini $29/month with 50 connections; DB Sprint Pack $1 for 12 hours. From the free tier docs and add-on pricing.