published: 2nd of October 2022
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.
The code for this blog can be found on Github here.
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)
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
PGS_HOST=db
PGS_USER=$USER
PGS_PASS=$USER
RAILS_ENV=development
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 file
The 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
end
Alpine 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 file
The 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 file
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=$USER
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-tmp
This generates a Rails application in the <app-name> directory.
Bring up the application containers.
docker compose \
-f docker-compose.yaml \
-f docker-compose-dev.yaml \
--env-file .env.dev \
up
Bring down the application containers.
docker compose \
-f docker-compose.yaml \
-f docker-compose-dev.yaml \
--env-file .env.dev \
down
Run a generator.
docker compose run \
--user $UID:$GID \
app \
bin/rails generate scaffold device name:string
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.
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/