Dockerize a Ruby on Rails Application

Khimananda Oli 7 min read Programming and Languages
Dockerize a Ruby on Rails Application

By Khimananda Oli | Last reviewed: August 2026

You need to Dockerize a Ruby on Rails application to eliminate environment drift, speed up CI pipelines, and prepare for Kubernetes or cloud-native deployment. While Rails is mature, its dependency tree (native gems, Node.js assets, system libraries) makes naive containerization slow and insecure. This guide provides a battle-tested, multi-stage approach that produces lean, secure images suitable for SOC 2 compliant environments.

How do you structure a multi-stage Dockerfile to Dockerize a Ruby on Rails application?

The most common mistake when teams first attempt to Dockerize a Ruby on Rails application is using a single-stage build. This results in massive images containing compilers, headers, and source code that have no business being in production. A multi-stage build separates the "build" environment from the "runtime" environment, ensuring your final artifact contains only what is strictly necessary to serve requests.

Multi-Stage Build ArchitectureBUILD STAGEruby:3.3-alpine + build-basebundle install --deploymentNode/Yarn + Asset PrecompileCompiled Assets + Vendor GemsRUNTIME STAGEruby:3.3-alpine (Minimal)COPY --from=builder /app/vendorCOPY --from=builder /app/publicNon-root User + EntrypointArtifact Transfer
Separating build dependencies from runtime artifacts significantly reduces attack surface and image size when you Dockerize a Ruby on Rails application.

In practice, your Dockerfile should define a builder stage first. This stage installs heavy packages like build-base, postgresql-dev, nodejs, and yarn. You run bundle install and rake assets:precompile here. The second stage, often called runtime, starts fresh from a minimal base image. It copies only the compiled gems, precompiled assets, and application code from the builder. This pattern is essential for keeping images under 300MB and passing security scans.

Optimizing layer caching for Gemfile changes

Docker caches layers sequentially. If you copy your entire application before running bundle install, every code change invalidates the gem cache, forcing a full reinstall. Always copy Gemfile and Gemfile.lock first, install dependencies, and only then copy the rest of the source code. This simple reordering can reduce CI build times from minutes to seconds for typical commits.

What are the best practices to secure and optimize Rails containers?

Security is not optional when you Dockerize a Ruby on Rails application for production. Running as root inside a container is a critical vulnerability; if an attacker escapes the application sandbox, they gain root access to the host namespace. Always create a dedicated rails user with a non-login shell and assign ownership of the application directory. Use USER rails before the entrypoint command.

For optimization, prefer Alpine Linux over Debian-based images. Alpine uses musl libc and BusyBox, resulting in a base image around 5MB versus 80MB+ for slim Debian variants. However, be aware that some native gems require specific compilation flags on musl. Test thoroughly. If you encounter persistent compatibility issues, ruby:3.3-slim is a safe fallback that still offers significant size savings over the default tag.

  • Remove cache directories: Run rm -rf /usr/local/bundle/cache/*.gem after installation to save space.
  • Use .dockerignore: Exclude .git, log/*, tmp/*, and node_modules to prevent context bloat and accidental secret leakage.
  • Pinned versions: Never use latest. Pin Ruby, Alpine, and Node versions explicitly for reproducible builds.
  • Health checks: Define a HEALTHCHECK instruction hitting /up (Rails 8+ default) or a custom endpoint to enable orchestrator-aware restarts.

How do you configure Docker Compose for local Rails development?

While production uses a single optimized image, local development requires volume mounts for hot reloading and separate services for databases and caches. When you Dockerize a Ruby on Rails application for dev, parity with production matters, but developer experience cannot be sacrificed. Use docker compose (v2 syntax) to orchestrate this stack.

<!-- docker-compose.yml -->
services:
  web:
    build:
      context: .
      target: development # Separate target with dev tools
    command: ["./bin/rails", "server", "-b", "0.0.0.0"]
    volumes:
      - .:/app:cached
      - bundle_cache:/usr/local/bundle
    ports:
      - "3000:3000"
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_started
    environment:
      DATABASE_URL: postgres://postgres:password@postgres/myapp_dev
      REDIS_URL: redis://redis:6379/1

  postgres:
    image: postgres:16-alpine
    volumes:
      - pg_data:/var/lib/postgresql/data
    environment:
      POSTGRES_PASSWORD: password
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      timeout: 5s
      retries: 5

volumes:
  bundle_cache:
  pg_data:

Note the use of named volumes for bundle_cache. This persists installed gems across container restarts, avoiding redundant reinstalls. The :cached mount option on macOS/Windows improves filesystem performance significantly. For teams working with Docker networking and volumes, understanding these persistence patterns prevents data loss and speeds up onboarding.

Local Development TopologyRails Web ContainerPort 3000 | Volume MountHot Reload EnabledPostgreSQL 16Persistent VolumeHealth Check ActiveRedis CacheSession / Queue StoreEphemeral DataTCP 5432TCP 6379
Service isolation in Docker Compose allows independent scaling and debugging of database and cache layers alongside the Rails app.

How does containerized Rails compare to traditional VPS deployment?

Many teams in Nepal and globally still deploy Rails directly to VPS instances using Capistrano or systemd. While simpler initially, this approach accumulates technical debt. Understanding the trade-offs helps justify the effort to Dockerize a Ruby on Rails application.

CriteriaTraditional VPS (Capistrano/Systemd)Containerized (Docker/K8s)
Environment ParityLow. Drift occurs as OS packages update independently.High. Immutable image guarantees identical dev/stage/prod.
Onboarding TimeHours to days. Manual setup scripts often break.Minutes. docker compose up replicates full stack.
ScalingVertical or manual horizontal. Slow response to spikes.Automatic horizontal scaling via HPA in Kubernetes.
Rollback SpeedSlow. Requires redeploying previous code revision.Instant. Re-tag or revert to previous immutable image digest.
Security PatchingIn-place updates risk breaking app. Requires testing.Rebuild image with patched base. Zero downtime rollout.
ComplexityLow initial, high long-term maintenance.Higher initial learning curve, lower operational toil.

For solo developers or small projects, a VPS remains viable. But for any team targeting growth, compliance, or multiple environments, containers provide a foundation that pays dividends. If you are managing databases alongside this transition, review PostgreSQL administration essentials to ensure your containerized DB strategy aligns with backup and replication needs.

How do you handle secrets and environment variables securely?

Never bake secrets into your Docker image. This is a frequent failure point when engineers first Dockerize a Ruby on Rails application. Images are often pushed to registries where they may be scanned or accessed by broader teams. Secrets must be injected at runtime.

In development, use env_file in Docker Compose pointing to a gitignored .env file. In production, leverage your orchestrator’s native secrets management. For Kubernetes, use Secrets mounted as environment variables or files. For AWS ECS, use Secrets Manager integration. For standalone Docker Swarm, use docker secret. Rails 8’s encrypted credentials work well inside containers too, provided the master key is injected securely via environment variable or mounted secret file.

Avoiding common pitfalls with asset compilation

Asset precompilation often fails in containers due to missing Node.js or incorrect paths. Ensure your build stage includes the exact Node version specified in your .nvmrc or package.json. Set RAILS_ENV=production during the build so assets compile with production settings (digests, minification). If using Propshaft or Sprockets, verify that the output directory matches your runtime PUBLIC_ASSETS_PATH. Debugging asset issues in a running container is painful; get it right in the build stage.

Runtime Secret Injection PatternSecrets Manager(Vault / AWS SM / K8s)DATABASE_URLSECRET_KEY_BASERails Container(Immutable Image)ENV VariablesMounted FilesInject at Runtime ONLY❌ Never in Dockerfile
Secrets must never be baked into image layers; inject them dynamically at runtime to maintain security and compliance.

Next Steps for Production Readiness

Successfully containerizing your Rails app is just the beginning. To make this truly production-grade, integrate image scanning into your CI pipeline using tools like Trivy or Grype. Automate base image updates to patch vulnerabilities without manual intervention. Consider adopting CI/CD best practices to streamline the path from commit to deployment. If you need help auditing your current container strategy or designing a compliant infrastructure for your Rails platform, reach out to discuss your architecture.

Frequently Asked Questions

Use the official ruby:3.4-slim-bookworm image for production builds. It includes Debian Bookworm with minimal packages, reducing attack surface and image size compared to full variants while maintaining glibc compatibility for native gem extensions.

Never bake secrets into images. Use Docker secrets, environment variables injected at runtime, or external vaults like HashiCorp Vault. Configure credentials.yml.enc decryption keys via ENV variables passed during container startup, not during build.

Missing build dependencies cause repeated native gem compilation. Install build-essential, libpq-dev, and nodejs before bundling. Use multi-stage builds to cache the vendor/bundle layer separately from application code changes.

Avoid Alpine for Rails unless necessary. Musl libc causes compatibility issues with gems like nokogiri and pg. Slim Debian variants offer better compatibility with only marginally larger image sizes and fewer debugging headaches in 2026.

Set workers based on available CPU cores and RAM. Use WEB_CONCURRENCY environment variable rather than hardcoding. Bind to 0.0.0.0:3000 since containers have isolated networking. Enable preload_app for faster worker boot times and reduced memory usage.

Run migrations as a separate init container or one-off task before deploying web containers. Never auto-migrate on web server startup in production. This prevents race conditions when scaling multiple replicas simultaneously during deployments.

Use multi-stage builds, remove build tools after bundle install, clean apt caches, and exclude test gems in production. Target under 400MB for slim-based Rails images by removing unnecessary system packages and documentation files.

Yes. Define services for web, postgres, redis, and sidekiq. Mount source code as volumes for live reloading. Use tmpfs for tmp and log directories to avoid permission issues between host and container filesystems.

Precompile assets during the build stage using RAILS_ENV=production and SECRET_KEY_BASE=dummy. Copy compiled public/assets to the final runtime stage. This avoids needing Node.js or yarn in the production container entirely.

Create a dedicated /up endpoint returning 200 OK without database queries. Use this for Docker HEALTHCHECK and load balancer probes. Reserve /ready for readiness checks that verify database and cache connectivity separately.

Match container UID/GID to host user. Add USER directive in Dockerfile matching your development UID. On Linux, use chown or docker compose user configuration. macOS Docker Desktop handles this automatically through VirtioFS in 2026.

No. Always create a non-root user in the Dockerfile. Running as root risks container escape vulnerabilities and violates CIS Docker benchmarks. Use USER rails directive after installing dependencies but before copying application code.

Override the entrypoint with docker run -it --entrypoint bash to inspect the environment. Check logs with docker logs, verify environment variables with printenv, and test database connectivity manually before troubleshooting Rails-specific issues.

Order Dockerfile layers from least to most frequently changing. Place Gemfile and bundle install before copying application code. Use BuildKit cache mounts for bundler and apt to persist downloads across rebuilds without inflating image layers.

Install tzdata package and set TZ environment variable in Dockerfile. Configure Rails.application.config.time_zone independently. Mismatched system and application timezones cause subtle bugs with scheduled jobs and timestamp comparisons in production.