Containerization is a game-changer in software development, delivering key advantages like streamlined deployment, scalability, and consistency across development, testing, and production environments.
In this post, I’m going to walk you through a configuration for dockerizing your Ruby on Rails app and dive into each detail so you know exactly how it works.
Install Docker Desktop if you’re on Mac or Windows, or a Docker Engine if you’re using Linux.
Here’s the structure of the project that will be dockerized:
your-project-name/
│
├── backend (rails backend)
│ ├── Gemfile
│ ├── Gemfile.lock
│ └── ... (other rails files)
├── frontend (frontend folder)
│ ├── package.json
│ └── ... (other frontend files)
│
├── bin (place for script)
│ ├── run (script that run docker)
│ └── ... (other scripts)
│
├── docker
│ ├── development
│ ├──├── frontend.Dockerfile
│ └──└── backend.Dockerfile
│
└── docker-compose.yml
Most folks just want to copy the config and hit the ground running. Below, I’ve shared my configuration and broken it down for you.
Tech stack for this sample project:
FROM ruby:3.3.0-bookworm
RUN set -eux; \
apt-get update; \
apt-get -y upgrade
ARG UID=1000
RUN set -eux; \
useradd -s /bin/bash -u ${UID} -m backend; \
mkdir -p /backend/vendor/bundle; \
chown -R backend:backend /backend
USER backend
WORKDIR /backend
ENV BUNDLE_PATH=vendor/bundle
ENV BUNDLE_APP_CONFIG=$BUNDLE_PATH
ENV BUNDLE_USER_CACHE=vendor/bundle/cache
# This will force using gems with native extensions instead of pre-compiled versions.
# Using precompiled versions leads to compatibility issues in the case of ARM platform.
RUN bundle config set force_ruby_platform true
FROM node:20.11.1-bookworm
ARG UID=1000
RUN set -eux; \
if [ "${UID}" = "1000" ]; then \
# this image already has "node" user with UID 1000
usermod -l frontend -s /bin/bash node; \
groupmod -n frontend node; \
else \
useradd -s /bin/bash -u ${UID} -m frontend; \
fi; \
mkdir -p /frontend/node_modules; \
chown -R frontend:frontend /frontend
USER frontend
WORKDIR /frontend
ENV NODE_ENV=development
version: "3.9"
services:
backend: &backend
build:
context: .
dockerfile: docker/development/backend.Dockerfile
volumes:
- ./backend:/backend:cached
- backend_tmp:/backend/tmp
- bundle:/backend/vendor
command: bin/rails s -b 0.0.0.0
ports:
- "3000:3000"
tty: true
stdin_open: true
depends_on:
- db
db:
image: postgres:14
volumes:
- db_data:/var/lib/postgresql/data
environment:
POSTGRES_PASSWORD: password
frontend:
build:
context: .
dockerfile: docker/development/frontend.Dockerfile
volumes:
- ./frontend:/frontend:cached
- node_modules:/frontend/node_modules
command: yarn dev --port 3001 --host 0.0.0.0
ports:
- "3001:3001"
volumes:
bundle:
db_data:
node_modules:
backend_tmp:
driver_opts:
type: tmpfs
device: tmpfs
o: uid=1000,gid=1000
Change default in backend/config/database.yml to:
default: &default
adapter: postgresql
encoding: unicode
host: <%= ENV.fetch("DB_HOST", "db") %>
username: postgres
password: <%= ENV.fetch("DB_PASSWORD", "password") %>
To get everything up and running, you need to build images (or pull), install gem, prepare db, and install packages for the frontend. You can find all of these scripts in this repository.
For simplicity, I combined all scripts into one bin/setup file.
#!/bin/bash
#exit immediately if any command within the script or session exits with a non-zero status
set -e
docker compose build
docker compose run --rm backend /bin/bash -c 'bundle install'
docker compose run --rm backend /bin/bash -c 'bundle exec rails db:prepare'
docker compose run --rm backend /bin/bash -c 'bundle exec rails db:seed'
docker compose run --rm frontend /bin/bash -c 'yarn install' #change yarn to your package manager
Give permission to run scripts from the bin folder using chmod +x -R ./bin/ from the project root folder. Then run this script bin/setup.
Congratulations! Now you can run docker using docker compose up.
Now let’s talk more about the specific configuration of the backend.Dockerfile.
FROM ruby:3.3.0-bookworm, why bookworm, and what is it?
Bookworm is the name of the Debian version. Most of the time, we don’t care about the size of the docker image, so I recommend picking the latest Debian version.
Also, I recommend always explicitly specifying the version of an image – never pick the latest as it can upgrade and break the application.
RUN set -eux; \
apt-get update; \
apt-get -y upgrade
These commands update and upgrade the software packages within a Docker container, ensuring they are up-to-date and secure. The script exits on errors and automatically confirms all prompts for a smooth, non-interactive process.
ARG UID=1000
RUN set -eux; \
useradd -s /bin/bash -u ${UID} -m backend; \
mkdir -p /backend/vendor/bundle; \
chown -R backend:backend /backend
It creates a new user named backend with a specified user ID, sets up a directory for the application, and assigns ownership of this directory to the new user. Then it creates a directory for Ruby dependencies and sets the application user as its owner for secure and organized access.
frontend.Dockerfile has almost the same configuration.
The main thing to understand in docker-compose.yml is how volumes are mounted.
Volumes for the backend service:
volumes:
- ./backend:/backend:cached
- backend_tmp:/backend/tmp
- bundle:/backend/vendor
This volume mounts the backend directory on the host machine to /backend inside the container. With :cached for optimized I/O performance.
A named volume backend_tmp is mounted to /backend/tmp inside the container, managed by Docker for data persistence.
This bundle is another named volume mounted to /backend/vendor, ensuring the persistence of Ruby dependencies (gems) across container restarts.
volumes:
- db_data:/var/lib/postgresql/data
This volume is used to store the Postgres database within db_data, ensuring that data remains persistent across container restarts.
volumes:
- ./frontend:/frontend:cached
- node_modules:/frontend/node_modules
The local frontend directory is mounted to /frontend inside the container, with :cached for optimized I/O performance.
Ensures persistent storage of Node.js dependencies in node_modules, avoiding loss on container rebuild.
volumes:
bundle:
db_data:
node_modules:
backend_tmp:
driver_opts:
type: tmpfs
device: tmpfs
o: uid=1000,gid=1000
These are simply declared, allowing Docker to manage them for persistence.
Specified with driver_opts to configure as tmpfs, meaning it’s stored in memory, not persisted on the host disk, with uid=1000, gid=1000 for file ownership. Ideal for temporary data not requiring persistence across restarts.
If you take scripts from this repository, you can smoothly debug a rails application by running this script bin/run_and_attach. This allows you to debug inside the same terminal.
By dockerizing your Rails app, you’re setting yourself up for success with an application that’s portable and easy to deploy.
Whether you host it on a server, use a PaaS like Heroku, or orchestrate it with Kubernetes, Docker lays a solid foundation.
Well done on dockerizing your Rails app! Here’s to smooth sailing in your app development and deployment!
Check out our newsletter