Dockerize Rails 7 App
Published: 2022-10-02
Intro
In this post, I will show you how to Dockerize your Rails 7 app in a development environment. We will be using Tailwind for the CSS and PostgreSQL for the database. This setup includes file syncing of assets on file changes between the host and container which is super nice.
Software used in this post
- Docker - 20.10.18, build b40c2f6
- Ruby - 3.1.2p20
- Rails - 7.0.4
- PostgreSQL - 14.1
Code Repository
The code for this blog can be found on Github here.
Environment Variables
Before we begin, add some environment variables to your rc file. This allows your user and group ID's to be used within the containers.
export UID=$(id -u)
export GID=$(id -g)Dockerfiles
Create the following Dockerfile which is used to create our application container images.
################## RAILS BUILD IMAGE ##################
# Use this to generate a new rails application
FROM ruby:3.1.2-alpine3.16 AS build
# ARGs are passed in via the `--build-arg ` CLI argument
ARG APP_NAME
ARG APP_USER
ARG APP_USER_ID
ARG APP_GROUP_ID
ENV APP_NAME ${APP_NAME}
ENV APP_USER ${APP_USER}
ENV APP_USER_ID ${APP_USER_ID}
ENV APP_GROUP_ID ${APP_GROUP_ID}
# Static variables
ARG BUILD_PACKAGES="build-base"
# Install build deps
RUN echo "http://dl-4.alpinelinux.org/alpine/v3.16/main" >> /etc/apk/repositories \
&& echo "http://dl-4.alpinelinux.org/alpine/v3.16/community" >> /etc/apk/repositories \
&& apk update \
&& apk add ${BUILD_PACKAGES}
# Set working directory
WORKDIR /opt
# Install Rails
RUN gem install rails bundler --no-document
# Create new Rails app
RUN rails new \
--skip-bundle \
--skip-git \
--database=postgresql \
--css=tailwind \
${APP_NAME}
WORKDIR /opt/${APP_NAME}
################## RAILS BASE IMAGE ##################
FROM ruby:3.1.2-alpine3.16 AS rails-base
ARG APP_NAME
ARG APP_USER
ARG APP_USER_ID
ARG APP_GROUP_ID
ENV APP_NAME ${APP_NAME}
ENV APP_USER ${APP_USER}
ENV APP_USER_ID ${APP_USER_ID}
ENV APP_GROUP_ID ${APP_GROUP_ID}
ARG RUN_PACKAGES="build-base tzdata postgresql-dev postgresql-client nodejs yarn"
RUN echo "http://dl-4.alpinelinux.org/alpine/v3.12/main" >> /etc/apk/repositories \
&& echo "http://dl-4.alpinelinux.org/alpine/v3.12/community" >> /etc/apk/repositories \
&& apk update \
&& apk add --no-cache $RUN_PACKAGES
# Default directory
RUN mkdir -p /opt/${APP_NAME}
WORKDIR /opt/${APP_NAME}
COPY --from=build /usr/local/bundle /usr/local/bundle
COPY --from=build /opt/${APP_NAME} /opt/${APP_NAME}
COPY config/Gemfile Gemfile
RUN bundle install \
&& bin/rails tailwindcss:install
COPY config/Procfile.dev Procfile.dev
COPY config/database.yml config/database.yml
COPY config/environments-development.rb config/environments/development.rb
COPY config/bin-dev bin/dev
RUN chmod +x bin/dev
# Cleanup cache
RUN bundle clean --force \
&& rm -rf /usr/local/bundle/cache/*.gem \
&& find /usr/local/bundle/gems/ -name "*.c" -delete \
&& find /usr/local/bundle/gems/ -name "*.o" -delete
# Create app user and group
RUN addgroup -S ${APP_USER} -g ${APP_GROUP_ID} && adduser -u ${APP_USER_ID} -S ${APP_USER} -G ${APP_USER}
# Set directory ownership
RUN chown -R ${APP_USER_ID}:${APP_GROUP_ID} /opt/${APP_NAME}
USER ${APP_USER}
# Add a script to be executed every time the container starts.
EXPOSE 3000
CMD ["bin/dev"]The following base docker-compose.yaml file is used for all stages of development.
version: "3.9"
services:
db:
image: postgres:14.1
volumes:
- ./tmp/db:/var/lib/postgresql/data
environment:
- "POSTGRES_USER=${PGS_USER}"
- "POSTGRES_PASSWORD=${PGS_PASS}"
app:
build:
context: .
dockerDockerfile
image: rails-base
user: ${APP_USER_ID}:${APP_GROUP_ID}
volumes:
- ./${APP_NAME}:/opt/${APP_NAME}
depends_on:
- db
environment:
- "APP_NAME=${APP_NAME}"
- "APP_USER_ID=${APP_USER_ID}"
- "APP_GROUP_ID=${APP_GROUP_ID}"
- "PGS_HOST=${PGS_HOST}"
- "PGS_USER=${PGS_USER}"
- "PGS_PASS=${PGS_PASS}"
- "RAILS_ENV=${RAILS_ENV}"The docker-compose-dev.yaml file is used for the development environment.
version: "3.9"
services:
app:
user: ${APP_USER_ID}:${APP_GROUP_ID}
command: sh -c "bin/rails db:create && bin/rails db:migrate && rm -f tmp/pids/server.pid && bundle exec bin/dev"
ports:
- "3000:3000"The .env.dev file is used to pass environment variables to containers.
APP_NAME=<app-name>
APP_USER=$USER
APP_USER_ID=$UID
APP_GROUP_ID=$GID\
nPGS_HOST=db
PGS_USER=$USER
PGS_PASS=$USER
RAILS_ENV=developmentConfiguration Files
To get our Rails environment working well with Docker, we need to alter some of the configuration files. The alterations are explained below.
The configuration files can be found found in the Github repo in the config directory.
The Gemfile file has the foreman gem added which is used to run the Procfile.
source "https://rubygems.org"
gem "foreman", "~> 0.87.2"
# ... rest of fileThe config/database.yml file is altered to pull data from environment variables.
default: &default
adapter: postgresql
encoding: unicode
# For details on connection pooling, see Rails configuration guide
# https://guides.rubyonrails.org/configuring.html#database-pooling
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
host: <%= ENV.fetch("PGS_HOST") { "db" } %>
username: <%= ENV.fetch("PGS_USER") { "postgres" } %>
password: <%= ENV.fetch("PGS_PASS") { "postgres" } %>
development:
<<: *default
database: <%= "#{ENV.fetch('APP_NAME')}_development" %>
test:
<<: *default
database: <%= "#{ENV.fetch('APP_NAME')}_test" %>
production:
<<: *default
database: <%= "#{ENV.fetch('APP_NAME')}_production" %>The config/environments-development.rb file is altered to allow access to IPv4/6 address and any hostname for development mode.
Rails.application.configure do
# Allow access from any IPv4/6 address
config.web_console.whitelisted_ips = ['0.0.0.0/0', '::/0']
# Allow access from any hostname
config.hosts.clear
# ... rest of file
endAlpine linux does not have the bash shell. The shebang line of the bin/dev file is updated to use the sh shell.
#!/usr/bin/env sh
# ... rest of fileThe Procfile.dev file is updated to bind to all IPv4 addresses (0.0.0.0) instead of localhost.
web: bin/rails server -b "0.0.0.0" -p 3000
# ... rest of fileBuild Containers
Build the rails-base container image.
docker compose \
-f docker-compose.yaml \
-f docker-compose-dev.yaml \
--env-file .env.dev \
build \
--build-arg APP_NAME=<app-name> \
--build-arg APP_USER_ID=$UID \
--build-arg APP_GROUP_ID=$GID \
--build-arg APP_USER=$USERGenerate Rails App
Generate a new Rails app using the rails-base container image.
export APP_NAME="<app-name>" \
&& docker container run -itd --name=rails-tmp rails-base sh \
&& docker container cp rails-tmp:/opt/$APP_NAME $APP_NAME \
&& docker container kill rails-tmp \
&& docker container rm rails-tmpThis generates a Rails application in the <app-name> directory.
Run Containers
Bring up the application containers.
docker compose \
-f docker-compose.yaml \
-f docker-compose-dev.yaml \
--env-file .env.dev \
upBring down the application containers.
docker compose \
-f docker-compose.yaml \
-f docker-compose-dev.yaml \
--env-file .env.dev \
downRun a generator.
docker compose run \
--user $UID:$GID \
app \
bin/rails generate scaffold device name:stringOutro
In this post I showed you how to configure a Docker environment for Ruby on Rails application development. It's a bit of a process but works well when all the peices are together. Look out for a future post where I build a production deployment.
Links
https://github.com/TomFern/dockerizing-ruby
https://semaphoreci.com/community/tutorials/dockerizing-a-ruby-on-rails-application
https://guides.rubyonrails.org/configuring.html#actiondispatch-hostauthorization
https://www.honeybadger.io/blog/testing-rails-with-docker/
https://www.simplethread.com/how-to-create-a-new-rails-7-app-with-tailwind/