2026-03-23 | PreviewProof Team

Preview-Ready Rails: A Working Docker Compose Setup with Seed Data

railspreview environmentsdocker composesidekiqseed data

Rails has the largest install base of any preview-target stack and the most varied. Rails 7 with Hotwire, Rails 6 legacy apps, Rails-API-only with a JS frontend, Rails plus Sidekiq. Most preview guides treat Rails as one thing — it isn’t, and that’s why generic advice falls over the moment you wire it up against a real codebase.

This post walks through preview-readiness decisions specific to Rails with the configuration excerpts a working reference repo would include.

The reference stack

The shape we’ll target: Rails 7.1 (Hotwire enabled), Postgres 16, Redis 7, Sidekiq for background jobs, Action Cable for any WebSocket needs. This is a representative shape for most production Rails apps in 2026. The decisions here generalize to Rails 6 with minor changes.

docker-compose.yml: the development baseline

A preview-ready repo starts with a working docker-compose.yml that runs the full stack locally. The local file is the source of truth; for previews, the same docker-compose.yml is parameterized — built images instead of bind mounts, env-var-injected hostnames, preview-specific settings — so the shape of the stack stays identical to what developers run on their laptops.

docker-compose.yml
services:
web:
build: .
command: bin/rails server -b 0.0.0.0 -p 3000
ports:
- "3000:3000"
environment:
DATABASE_URL: postgres://postgres:postgres@db:5432/app_development
REDIS_URL: redis://redis:6379/0
RAILS_ENV: development
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
sidekiq:
build: .
command: bundle exec sidekiq
environment:
DATABASE_URL: postgres://postgres:postgres@db:5432/app_development
REDIS_URL: redis://redis:6379/0
RAILS_ENV: development
depends_on:
- db
- redis
db:
image: postgres:16
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: app_development
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 3s
retries: 5
redis:
image: redis:7-alpine

Two things to notice. First, the web and sidekiq services use the same image — a common Rails pattern that keeps deploys simple. Second, the database has a healthcheck and the web service waits for it. This matters more in preview environments, where every boot is a cold start.

docker-compose.yml: preview-ready

Once the stack moves to ephemeral previews, the same docker-compose.yml shifts to use built images, env-var-injected hostnames, and preview-specific settings. Three things change versus local: RAILS_ENV is preview (or production-with-overrides), assets need to be precompiled, and the entrypoint must run migrations and seeds before the server boots.

docker-compose.yml
services:
web:
image: ghcr.io/myorg/myapp:${GIT_SHA}
command: ./bin/preview-entrypoint.sh
environment:
RAILS_ENV: preview
RAILS_SERVE_STATIC_FILES: "true"
RAILS_LOG_TO_STDOUT: "true"
DATABASE_URL: postgres://postgres:postgres@db:5432/app_preview
REDIS_URL: redis://redis:6379/0
SECRET_KEY_BASE: ${SECRET_KEY_BASE}
APP_HOST: ${PREVIEW_HOST}
sidekiq:
image: ghcr.io/myorg/myapp:${GIT_SHA}
command: bundle exec sidekiq -c 2
environment:
RAILS_ENV: preview
DATABASE_URL: postgres://postgres:postgres@db:5432/app_preview
REDIS_URL: redis://redis:6379/0

RAILS_SERVE_STATIC_FILES=true matters because preview environments rarely have a CDN in front of them. Without it, all your asset requests will 404 in production-like envs. This is one of the most common Rails preview gotchas — easy to miss, easy to debug for an hour.

A preview Rails environment

Add config/environments/preview.rb rather than reusing production. It inherits from production but loosens the bits that don’t make sense for ephemeral environments — eager-load is on, but caching can stay simple, and you don’t want force-SSL since previews often run on a shared TLS-terminating proxy.

config/environments/preview.rb
require_relative "production"
Rails.application.configure do
config.cache_classes = true
config.eager_load = true
config.consider_all_requests_local = false
config.action_controller.perform_caching = false
config.force_ssl = false
config.log_level = :info
config.action_mailer.delivery_method = :test
config.active_job.queue_adapter = :sidekiq
end

Mailer set to :test delivery is intentional — preview environments should never send real email. For testing email-driven flows, see stripe/twilio/sendgrid test mode in previews for the broader pattern; the same mocked-third-party approach applies here.

The entrypoint: migrate, seed, boot

The web container’s entrypoint script is where preview-environment correctness lives.

bin/preview-entrypoint.sh
#!/usr/bin/env bash
set -e
echo "==> Running migrations"
bundle exec rails db:prepare
echo "==> Seeding (idempotent)"
bundle exec rails db:seed
echo "==> Precompiling assets if missing"
if [ ! -d "public/assets" ]; then
bundle exec rails assets:precompile
fi
echo "==> Cleaning stale PID"
rm -f tmp/pids/server.pid
exec bin/rails server -b 0.0.0.0 -p 3000

db:prepare is the right command for ephemeral envs — it creates the database if it doesn’t exist, runs pending migrations otherwise, and is idempotent. db:setup drops and recreates, which is wrong here. db:migrate fails on a fresh database. See database migration patterns for preview environments for the broader version of this argument.

The seed file should be idempotent. Model.find_or_create_by! everywhere; never Model.create! without a guard.

db/seeds.rb
admin = User.find_or_create_by!(email: "[email protected]") do |u|
u.password = "preview123"
u.role = :admin
end
5.times do |i|
Project.find_or_create_by!(name: "Project #{i + 1}", owner: admin)
end

For factory-based seeds — factory_bot_rails plus deterministic Faker — see seeding Postgres for preview environments. The pattern is the same as Rails fixtures but generates richer data.

Sidekiq: queue scoping and concurrency

Sidekiq holds a connection to Redis. If two preview environments share a Redis instance, they’ll process each other’s jobs. Two ways to scope correctly:

  1. Separate Redis per preview (preferred). Each preview environment gets its own Redis container in the Compose file. Costs almost nothing, isolates completely.
  2. Namespaced keys on a shared Redis. Set REDIS_URL=redis://shared:6379/0 and use a per-preview REDIS_NAMESPACE env var. Works, but requires Sidekiq config plus discipline that anyone reading from Redis directly respects the namespace.

Run Sidekiq with reduced concurrency in previews (-c 2). Preview environments don’t need production worker counts and the cost savings add up. See background jobs in ephemeral preview environments for the broader pattern.

Asset pipeline

Rails 7 ships with propshaft or sprockets-rails and importmap-rails. Either way, asset precompilation needs to happen at preview boot — the manifest needs to exist before the server starts serving requests.

The right place is in the entrypoint, conditionally (skip if public/assets exists, which it will in containers built with assets compiled at image build time). Bake assets into the image during CI when possible — it cuts preview boot time significantly.

Action Cable

If the app uses Action Cable, the WebSocket origin needs to allow the preview’s dynamic hostname. Set config.action_cable.allowed_request_origins to a regex matching your preview domain pattern, or to [/.*/] in the preview environment specifically. Production should keep tight origins.

Env template

A .env.preview.example checked into the repo is the contract between the application and the preview platform.

.env.preview.example
RAILS_ENV=preview
SECRET_KEY_BASE= # generated per preview
DATABASE_URL= # injected by platform
REDIS_URL= # injected by platform
APP_HOST= # injected by platform; e.g. pr-123.previews.example.com
SENDGRID_API_KEY=test-key # mock or test-mode key
STRIPE_SECRET_KEY=sk_test_... # test mode only

See environment variables for ephemeral preview environments for the patterns around build-vs-runtime injection and per-PR vs shared values.

What a working repo looks like

A reference Rails preview-ready repo includes the files above plus a bin/preview-up script wrapping docker compose up against the preview-shaped docker-compose.yml and a CI step that builds the image. None of this is exotic; the value is having all of it consistent in one place.

Rails is forgiving when you get the shape right. Idempotent migrations, scoped Sidekiq, precompiled assets, neutralized mailer, runtime-injected env vars — previews behave the way reviewers expect. The configuration above works with whatever preview platform or self-hosted setup you bring to it.

If you’d rather not wire up the per-PR orchestration yourself, PreviewProof consumes Compose-shaped configs like the one above directly. The Rails repo you already have becomes a preview-ready repo without rewriting the deployment model, and the verification layer on top is what makes it a fit for teams where reviewer sign-off is a formal step rather than an assumption.