Tutorial

Rails 8 on a Free Container: Puma, 512 MB, and a PostgreSQL That Persists

SnapDeploy Team 2026-10-19 Updated 2026-10-19 8 min read
railsrubypostgresqldockerfree-tier

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_migrate

Pointing 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_KEYcontents of config/master.keyDecrypts credentials; without it the app will not boot in production.
DATABASE_URLinjected by the add-on or pastedSee above.
WEB_CONCURRENCY1One Puma worker. Each worker is a full copy of the app in memory; two do not fit in 512 MB with headroom.
RAILS_MAX_THREADS3Threads per worker, and the size of the connection pool. Three is plenty on a quarter of a CPU.
SOLID_QUEUE_IN_PUMA1Runs 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

  1. Commit the Rails-generated Dockerfile (it is already in a new app; for an older app run rails app:update or copy one from a fresh rails new).
  2. Push to GitHub. In SnapDeploy: Containers → Deploy from GitHub → pick the repository. Add the variables from the table.
  3. Attach a database: a PostgreSQL add-on injects DATABASE_URL; otherwise paste one.
  4. 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.
  5. 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.

Ready to Deploy?

Deploy free. 10 deploys a day, 100 hours a month, no credit card.

Run this yourself: Get a dedicated NVIDIA T4 (16 GB) for a flat $499/mo or A10G (24 GB) for $999/mo — never shared, never spot, no hourly metering. See dedicated GPU hosting →

One-click Ollama, vLLM, ComfyUI, Whisper, PyTorch & more — or deploy your own GitHub repo or Docker image. Compare plans.

FIRST OF ITS KIND

Sprint Pack — ₹99 for 24 hours

The first managed container PaaS to offer a one-time 24-hour Always-On pass under $5. Unlimited deploys + Always-On for one Small (512 MB) container — including WebSockets & real-time apps. No subscription, no auto-renewal. Stack two for 48 hours.

Need a managed database too? DB Sprint Pack — ₹99 / 12h for Postgres, MySQL, MariaDB, Mongo, Redis, or RabbitMQ.

Tip: Need 24 hours of Always-On for a demo, weekend, or quick test — or WebSockets & real-time apps? Sprint Pack is ₹99 one-time — no subscription, no auto-renewal.

Need a managed Postgres / MySQL / Mongo / Redis instead? DB Sprint Pack — ₹99 / 12h.

Take SnapDeploy with you

Deploy, monitor and wake your containers from your iPhone.

Download on the App Store

Get DevOps Tips & Updates

Container deployment guides, platform updates, and DevOps best practices. No spam.

Unsubscribe anytime. We respect your privacy.

More Articles