2026-03-23 | PreviewProof Team
Preview-Ready Rails: A Working Docker Compose Setup with Seed 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.
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-alpineTwo 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.
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/0RAILS_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.
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 = :sidekiqendMailer 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.
#!/usr/bin/env bashset -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:precompilefi
echo "==> Cleaning stale PID"rm -f tmp/pids/server.pid
exec bin/rails server -b 0.0.0.0 -p 3000db: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.
u.password = "preview123" u.role = :adminend
5.times do |i| Project.find_or_create_by!(name: "Project #{i + 1}", owner: admin)endFor 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:
- Separate Redis per preview (preferred). Each preview environment gets its own Redis container in the Compose file. Costs almost nothing, isolates completely.
- Namespaced keys on a shared Redis. Set
REDIS_URL=redis://shared:6379/0and use a per-previewREDIS_NAMESPACEenv 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.
RAILS_ENV=previewSECRET_KEY_BASE= # generated per previewDATABASE_URL= # injected by platformREDIS_URL= # injected by platformAPP_HOST= # injected by platform; e.g. pr-123.previews.example.comSENDGRID_API_KEY=test-key # mock or test-mode keySTRIPE_SECRET_KEY=sk_test_... # test mode onlySee 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.