Skip to content
← All posts
5 min read Dawid Skłodowski

Dockerising a Rails app for dev and CI

Docker has matured from a curiosity into the default way to get a consistent environment. A guide to Dockerising a Rails app for development and CI — docker-compose for the full stack, the layer-cache discipline, and where Docker helps versus where it adds friction.

A few years ago Docker was a promising curiosity; today it is mature, and docker-compose has made running a whole application stack — app, database, cache, background workers — a single command. For a Rails team, the biggest payoff is no longer “ship containers to production” (though you can); it is consistency: every developer, and your CI, runs the exact same environment, and “works on my machine” stops being a sentence anyone says. Here is how to Dockerise a Rails app sensibly for development and CI — and an honest note on where it earns its keep and where it adds friction.

The Dockerfile, with the cache discipline

The image starts from a Ruby base and installs the system libraries and gems. The single most important thing — the same lesson from the early Docker days, still the one people get wrong — is to copy the Gemfiles and bundle before copying the rest of the code, so editing application code does not invalidate the (slow) gem-install layer:

FROM ruby:2.5

RUN apt-get update -qq && apt-get install -y build-essential libpq-dev nodejs \
    && rm -rf /var/lib/apt/lists/*

WORKDIR /app

# bundle layer — only rebuilt when the Gemfile changes
COPY Gemfile Gemfile.lock ./
RUN bundle install --jobs 4

# now the code
COPY . .

EXPOSE 3000
CMD ["bundle", "exec", "rails", "server", "-b", "0.0.0.0"]

Order it the other way and every code change reinstalls every gem — a two-minute wait that makes people abandon Docker. With the layering right, your gems reinstall only when the Gemfile actually changes.

docker-compose: the whole stack in one command

This is what makes Docker genuinely pleasant in 2018. A Rails app is not one process — it is the app, a database, usually Redis, often a background worker. docker-compose declares all of them, wired together, in one file:

# docker-compose.yml
version: "3"
services:
  web:
    build: .
    command: bundle exec rails server -b 0.0.0.0
    volumes:
      - .:/app                 # live code, no rebuild on edit
      - bundle:/usr/local/bundle
    ports:
      - "3000:3000"
    depends_on: [db, redis]
    environment:
      DATABASE_URL: postgres://postgres@db/app_development
      REDIS_URL: redis://redis:6379
  db:
    image: postgres:10
  redis:
    image: redis:4
  worker:
    build: .
    command: bundle exec sidekiq
    depends_on: [db, redis]

volumes:
  bundle:
docker-compose up      # the entire stack — web, db, redis, worker — starts together

A new developer clones the repo and runs one command to get the exact stack everyone else has — right Ruby, right Postgres version, right Redis, no “first install these fourteen things” README. Two practical touches make the dev experience good: mount your code as a volume (.:/app) so edits are live without rebuilding the image, and mount a named volume for the bundle so gems persist across container restarts instead of reinstalling. Services reach each other by name (the app connects to db and redis as hostnames), which is the compose networking handling the wiring for you.

Docker in CI: the same environment, end to end

The same image and compose setup pay a second dividend in CI. Instead of your CI provider’s bespoke environment — a particular Ruby, a particular Postgres, configured their way — you run your tests inside the same containers you develop in. The CI config builds the image (or pulls a cached one) and runs the suite against the same Postgres and Redis services:

# CI: build, then run the suite in the container against real db/redis services
test:
  - docker-compose build
  - docker-compose run web bundle exec rails db:create db:schema:load
  - docker-compose run web bundle exec rspec

The win is that “passes in CI” and “passes on my machine” finally mean the same thing, because they are the same environment — eliminating the whole class of “green locally, red in CI” mysteries caused by environment drift. (This pairs naturally with the parallel-testing work: run the suite across containers/nodes the same way.) For build speed, cache the image layers between CI runs and consider a multi-stage build so the final image carries only what it needs.

Where Docker helps, and where it adds friction

An honest assessment, because Docker is not free:

  • It genuinely helps: onboarding (one command to a working stack), environment consistency (dev = CI, and optionally = production), running multiple projects with conflicting dependency versions side by side, and matching production’s OS and library versions exactly. For a team of more than one or a project with several services, these are real, daily wins.
  • It adds friction: there is a learning curve, builds and disk usage are heavier, and on macOS in 2018 the file-system performance of mounted volumes is noticeably slow (Docker runs in a VM there), which can make a volume-mounted Rails app feel sluggish — a real, well-known pain point. For a solo developer on a simple app, a native rbenv + local Postgres setup may still be faster and simpler day to day.

The judgment: Docker’s value rises with team size, number of services, and how much environment drift has bitten you. For a multi-service app and a team, it is worth the friction; for a one-person, one-service project, weigh it honestly rather than adopting it because it is the fashion.

Verdict

Docker has matured into the default way to get a consistent, reproducible environment, and for a Rails team its biggest payoff is consistency, not deployment: docker-compose up gives every developer and your CI the same full stack — app, database, cache, workers — from one file and one command, ending “works on my machine” for good. Get the Dockerfile layer-caching right (Gemfile before code), mount code and bundle as volumes for a fast dev loop, and run CI in the same containers so green means green everywhere. Weigh the real friction — the learning curve and macOS volume performance — against the size of your team and stack. For most teams in 2018 the trade is clearly worth it, and the reproducibility it buys is the foundation everything else — parallel CI, matching production, painless onboarding — is built on.