- Home
- Blog
- Ruby & Rails Core
- Rails 8.1 Devcontainers for Teams: Stop Setup Drift
Rails 8.1 Devcontainers for Teams: Stop Setup Drift
VS Code + Codespaces configs that prevent 'works on my machine' bugs
A production-minded Rails 8.1 devcontainer setup that standardizes Ruby, Node, Postgres, and tooling across a team—plus the real failure modes and how to avoid them.
Start from a real production bug
The bug wasn’t in production code.
It was in the team.
One developer shipped a change that passed locally and failed in CI with:
-
pggem failing to compile - a different Node version producing a different asset build
- subtle timezone/locale differences changing a couple of specs
- “it worked on my machine” becoming a daily ritual
The scary part: these failures waste time and hide real regressions because people stop trusting tests.
The wrong mental model
“If we document the setup steps, the environment is standardized.”
READMEs rot. Local machines diverge.
- Ruby patches drift
- Homebrew updates break native gems
- Postgres versions differ
- Node tooling changes out from under you
- macOS + Linux behave differently (file watchers, paths, case sensitivity)
The real problem isn’t missing steps.
It’s uncontrolled variance.
Why the naive approach fails
The naive setup looks like this:
brew install postgresqlrbenv install ...bundle installyarn install- “If it fails, google the error”
This fails in predictable ways:
-
Native extension hell (
pg,nokogiri,ffi,grpc) - Different runtimes (Ruby/Node) produce different outputs
- Different database engines cause different query plans and behavior
- CI becomes “the real environment”, so dev becomes guesswork
The correct approach
A devcontainer is not “Docker for dev”.
It’s a contract:
- Pin runtime versions
- Make the container the default dev environment
- Make onboarding a clone-and-run operation
- Keep the environment close to CI
You want the same answer from:
- your laptop
- a teammate’s laptop
- Codespaces
- CI
Minimal, production-minded setup
1) Use Docker Compose as the source of truth
docker-compose.yml
services:
app:
build:
context: .
dockerfile: Dockerfile.dev
volumes:
- ..:/workspaces/app:cached
command: sleep infinity
environment:
RAILS_ENV: development
DATABASE_URL: postgres://postgres:postgres@db:5432/app_development
REDIS_URL: redis://redis:6379/0
depends_on:
- db
- redis
ports:
- "3000:3000"
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: postgres
volumes:
- pgdata:/var/lib/postgresql/data
ports:
- "5432:5432"
redis:
image: redis:7
ports:
- "6379:6379"
volumes:
pgdata:
Notes:
- Pin Postgres/Redis images to avoid “latest” surprises.
- Persist database state in a named volume.
2) Devcontainer config that works locally and in Codespaces
.devcontainer/devcontainer.json
{
"name": "rails-app",
"dockerComposeFile": ["../docker-compose.yml"],
"service": "app",
"workspaceFolder": "/workspaces/app",
"customizations": {
"vscode": {
"extensions": [
"shopify.ruby-lsp",
"ms-azuretools.vscode-docker",
"esbenp.prettier-vscode"
]
}
},
"postCreateCommand": "bin/devcontainer-setup",
"forwardPorts": [3000, 5432, 6379],
"remoteUser": "vscode"
}
This makes “open in container” behave the same across machines.
3) Put the setup logic in code, not tribal knowledge
bin/devcontainer-setup
#!/usr/bin/env bash
set -euo pipefail
# Ruby deps
bundle config set path vendor/bundle
bundle install
# JS deps (choose yarn/pnpm/npm to match your app)
corepack enable || true
yarn install --frozen-lockfile
# DB
bin/rails db:prepare
Keep this script idempotent. Make it safe to run repeatedly.
What to standardize (and what not to over-standardize)
Standardize:
- Ruby version (and Bundler)
- Node version (and package manager)
- Postgres version
- Redis version
- system packages required for native gems
- a single setup entrypoint (
bin/devcontainer-setup)
Don’t over-standardize:
- editor themes/settings
- personal shell prompts
- private key material (handle secrets properly)
Edge cases and production pitfalls
File performance on macOS
Mounting large Rails repos into Docker can be slow on macOS. Use:
-
:cachedor:delegatedmounts where appropriate - avoid bind-mounting heavy temp dirs (node_modules, tmp/cache) if needed
User/UID mismatches
If files created in the container are owned by root, your host can become painful.
Use a non-root user (remoteUser) and ensure your Dockerfile sets it correctly.
Codespaces prebuilds
If your repo is large, enable Codespaces prebuilds to reduce time-to-first-run.
But keep postCreateCommand fast and deterministic.
Secrets
Don’t bake secrets into images. Prefer:
-
.envlocally - Codespaces secrets in GitHub
- encrypted credentials in Rails where appropriate
Database extensions and Postgres tooling
If your production DB uses extensions (e.g., pg_trgm, citext), install them in the container and enable them in schema/migrations so dev matches production reality.
A good “definition of done” for devcontainers
A new teammate should be able to:
- Clone repo
- Open in VS Code
- “Reopen in Container”
- Run
bin/rails testandbin/dev
…and get the same results as CI.
Rule of thumb
If CI is your only consistent environment, your team is debugging the wrong problems.
Devcontainers aren’t about Docker purity. They’re about reducing variance so you spend your time on real application bugs.
Was this article helpful?
Your feedback helps us improve our content
How We Verify Conversions
Every conversion shown on this site follows a strict verification process to ensure correctness:
- Compare results on same dataset — We run both SQL and ActiveRecord against identical test data and verify results match
-
Check generated SQL with
to_sql— We inspect the actual SQL Rails generates to catch semantic differences (INNER vs LEFT JOIN, WHERE vs ON, etc.) - Add regression tests for tricky cases — Edge cases like NOT EXISTS, anti-joins, and predicate placement are tested with multiple scenarios
- Tested on Rails 8.1.1 — All conversions verified on current Rails version to ensure compatibility
Last updated: January 16, 2026
Try These Queries in Our Converter
See the SQL examples from this article converted to ActiveRecord—and compare the SQL Rails actually generates.
Deep Dive into ActiveRecord
Raza Hussain
Full-stack developer specializing in Ruby on Rails, React, and modern JavaScript. 15+ years upgrading and maintaining production Rails apps. Led Rails 4/5 → 7 upgrades with 40% performance gains, migrated apps from Heroku to Render cutting costs by 35%, and built systems for StatusGator, CryptoZombies, and others. Available for Rails upgrades, performance work, and cloud migrations.
More on Ruby & Rails Core
LEFT JOIN + WHERE in ActiveRecord: The Trap That Turns It Into INNER JOIN
LEFT JOIN queries often break the moment you add a WHERE on the joined table—silently turning into INNER JOIN behavior. Learn the correct Rails patterns (ON vs WHERE, scoped associations, where.missing/where.associated, EXISTS) and how to verify generated SQL.
Ruby 4.0.0 Is Out: What Rails & ActiveRecord Devs Actually Need to Know
Ruby 4.0.0 is released (Dec 25, 2025). For Rails apps, the real risks are stdlib gem changes (CGI), Net::HTTP behavior shifts, and Bundler 4. Here’s what breaks first, how to fix safely, and what Ruby 4 performance does—and doesn’t—solve.
N+1 Isn’t a Rails Problem: It’s a Query-Shaping Problem
Copied SQL into Rails and still got N+1? Rewrote in ActiveRecord and still got N+1? The fix is a set-based mental model: where N+1 really comes from, how to verify it, and the AR patterns that eliminate it safely.
Responses (0)
No responses yet
Be the first to share your thoughts