Skip to content

Instantly share code, notes, and snippets.

@natevick
Last active May 20, 2022 11:01
Show Gist options
  • Save natevick/96c22b18b6cb31fa458fafba32fa000f to your computer and use it in GitHub Desktop.
Save natevick/96c22b18b6cb31fa458fafba32fa000f to your computer and use it in GitHub Desktop.
Base Rails Docker Development Environment
default: &default
adapter: postgresql
encoding: unicode
pool: <%= ENV.fetch("RAILS_MAX_THREADS", 5) %>
username: <%= ENV.fetch("DATABASE_USER", 'postgres') %>
host: <%= ENV.fetch("DATABASE_HOST", 'localhost') %>
port: 5432
development:
<<: *default
database: docker_development
test:
<<: *default
database: docker_test
production:
<<: *default
database: docker_production
username: docker
password: <%= ENV['DOCKER_DATABASE_PASSWORD'] %>
version: '3.7'
services:
rails:
build:
context: ./docker/ruby
args:
- RUBY_VERSION=2.6.5
- BUNDLE_JOBS=15
- BUNDLE_RETRY=2
- NODE_VERSION=12
- INSTALL_PG_CLIENT=true
- UID=500
- GID=500
environment:
- DATABASE_USER=postgres
- DATABASE_HOST=postgres
command: bundle exec rails server -p 3000 -b '0.0.0.0'
entrypoint: docker/ruby/entrypoint.sh
volumes:
- .:/app:cached
- gems:/gems
- node_modules:/app/node_modules
- packs:/app/public/packs
- rails_cache:/app/tmp/cache
ports:
- "3000:3000"
user: ruby
tty: true
stdin_open: true
depends_on:
- postgres
postgres:
image: postgres:11
environment:
- POSTGRES_HOST_AUTH_METHOD=trust
volumes:
- postgres:/var/lib/postgresql/data
volumes:
gems:
postgres:
node_modules:
packs:
rails_cache:
ARG RUBY_VERSION=2.6
FROM ruby:$RUBY_VERSION
ARG DEBIAN_FRONTEND=noninteractive
ARG NODE_VERSION=11
RUN curl -sL https://deb.nodesource.com/setup_$NODE_VERSION.x | bash -
RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && \
echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list
RUN apt-get update && apt-get install -y \
build-essential \
nodejs \
yarn \
locales \
git \
netcat \
vim \
sudo
ARG UID
ENV UID $UID
ARG GID
ENV GID $GID
ARG USER=ruby
ENV USER $USER
RUN groupadd -g $GID $USER && \
useradd -u $UID -g $USER -m $USER && \
usermod -p "*" $USER && \
usermod -aG sudo $USER && \
echo "$USER ALL=NOPASSWD: ALL" >> /etc/sudoers.d/50-$USER
ENV LANG C.UTF-8
ENV BUNDLE_PATH /gems
ENV BUNDLE_HOME /gems
ARG BUNDLE_JOBS=20
ENV BUNDLE_JOBS $BUNDLE_JOBS
ARG BUNDLE_RETRY=5
ENV BUNDLE_RETRY $BUNDLE_RETRY
ENV GEM_HOME /gems
ENV GEM_PATH /gems
ENV PATH /gems/bin:$PATH
ARG INSTALL_PG_CLIENT=false
RUN if [ "$INSTALL_PG_CLIENT" = true ]; then \
apt-get install -y postgresql-client \
;fi
RUN mkdir -p "$GEM_HOME" && chown $USER:$USER "$GEM_HOME"
RUN mkdir -p /app && chown $USER:$USER /app
WORKDIR /app
RUN mkdir -p node_modules && chown $USER:$USER node_modules
RUN mkdir -p public/packs && chown $USER:$USER public/packs
RUN mkdir -p tmp/cache && chown $USER:$USER tmp/cache
USER $USER
RUN gem install bundler
#! /bin/bash
set -e
: ${APP_PATH:="/app"}
: ${APP_TEMP_PATH:="$APP_PATH/tmp"}
: ${APP_SETUP_LOCK:="$APP_TEMP_PATH/setup.lock"}
: ${APP_SETUP_WAIT:="5"}
# 1: Define the functions to lock and unlock our app container's setup
# processes:
function lock_setup { mkdir -p $APP_TEMP_PATH && touch $APP_SETUP_LOCK; }
function unlock_setup { rm -rf $APP_SETUP_LOCK; }
function wait_setup { echo "Waiting for app setup to finish..."; sleep $APP_SETUP_WAIT; }
# 2: 'Unlock' the setup process if the script exits prematurely:
trap unlock_setup HUP INT QUIT KILL TERM EXIT
# 3: Wait for postgres to come up
echo "DB is not ready, sleeping..."
until nc -vz postgres 5432 &>/dev/null; do
sleep 1
done
echo "DB is ready, starting Rails."
# 4: Specify a default command, in case it wasn't issued:
if [ -z "$1" ]; then set -- bundle exec rails server -p 3000 -b 0.0.0.0 "$@"; fi
# 5: Run the checks only if the app code is executed:
if [[ "$3" = "rails" ]]
then
# Clean up any orphaned lock file
unlock_setup
# 6: Wait until the setup 'lock' file no longer exists:
while [ -f $APP_SETUP_LOCK ]; do wait_setup; done
# 7: 'Lock' the setup process, to prevent a race condition when the
# project's app containers will try to install gems and set up the
# database concurrently:
lock_setup
# 8: Check if dependencies need to be installed and install them
bundle install
yarn install
# 9: Run migrations or set up the database if it doesn't exist
# Rails >= 6
bundle exec rails db:prepare
# Rails < 6
# bundle exec rake db:migrate 2>/dev/null || bundle exec rake db:setup
# 10: 'Unlock' the setup process:
unlock_setup
# 11: If the command to execute is 'rails server', then we must remove any
# pid file present. Suddenly killing and removing app containers might leave
# this file, and prevent rails from starting-up if present:
if [[ "$4" = "s" || "$4" = "server" ]]; then rm -rf /app/tmp/pids/server.pid; fi
fi
# 12: Replace the shell with the given command:
exec "$@"
@luchiago
Copy link

luchiago commented May 1, 2020

Hi. I was reading your article, btw it's a good tutorial, about dockerizing a rails app and I tried to implement it on my app. I was having trouble with RUN mkdir -p commands in dockerfile bc it wasn't finding the command mkdir, so I had to remove ENV PATH /gems/bin:$PATH. The same for chown. I guess the problem is setting the variable PATH with ENV PATH /gems/bin:$PATH. What are your thoughts?

@natevick
Copy link
Author

natevick commented May 1, 2020

Hmm, I have not run into that issue. I’ll check it on a fresh rails project tomorrow.

@luchiago
Copy link

luchiago commented May 1, 2020

Alright. Let me know, please.

@natevick
Copy link
Author

natevick commented May 1, 2020

I did find a couple of typos in docker-compose file that I fixed, but other than that it built the container as expected with no changes to the Dockerfile. What version of Ruby are you using for your app? I've used this config with Ruby 2.2-2.7 without much tweaking, but older versions run on an unsupported version of Debian.

@luchiago
Copy link

luchiago commented May 2, 2020

I use ruby 2.6.5. I'll try again using those files, following the article, and send you updates about it

@natevick
Copy link
Author

@luchiago any luck? I have made a few more tweaks to the files over the last week.

@luchiago
Copy link

@nvick I didn't try with the modifications yet, but I will. Could you explain the reason for this ENV PATH /gems/bin:$PATH?

@natevick
Copy link
Author

Yes, we need to prepend the path with where the gem executables are. The prepend is required to override the prepend that is done by the official Ruby Docker container for the gem executable location. https://github.com/docker-library/ruby/blob/a6b23d587aa4ce804f69b40d3fb48bc27c4a39db/2.6/buster/Dockerfile#L83

@luchiago
Copy link

luchiago commented May 11, 2020

There's a problem with permissions ERROR: Cannot start service rails: OCI runtime create failed: container_linux.go:349: starting container process caused "exec: \"docker/ruby/entrypoint.sh\": permission denied": unknown

I tried to solve with chown +x file

@luchiago
Copy link

Yes, we need to prepend the path with where the gem executables are. The prepend is required to override the prepend that is done by the official Ruby Docker container for the gem executable location. https://github.com/docker-library/ruby/blob/a6b23d587aa4ce804f69b40d3fb48bc27c4a39db/2.6/buster/Dockerfile#L83

Now I get it, thanks.

@natevick
Copy link
Author

There's a problem with permissions ERROR: Cannot start service rails: OCI runtime create failed: container_linux.go:349: starting container process caused "exec: \"docker/ruby/entrypoint.sh\": permission denied": unknown

Make sure the entrypoint is executable: chmod +x docker/ruby/entrypoint.sh

@luchiago
Copy link

Are the gems gonna be cached? Or every time I run docker-compose up they will be installed?

@natevick
Copy link
Author

They are cached in a docker volume.

@natevick
Copy link
Author

The entrypoint does a check for Gemfile changes on every docker-compose up, but if there are no changes it moves on.

@luchiago
Copy link

Cool man. It works now. Thank you so much for your help and for these files

@natevick
Copy link
Author

Glad it works and helps!

@bovender
Copy link

Great Gist, even two years on ;-) I also love the blog on hint.io. But allow me one question: In entrypoint.sh, you call unlock_setup followed by while [ -f $APP_SETUP_LOCK ]; do wait_setup; done -- but if I am not mistaken, the call to unlock_setup will basically render the locking mechanism ineffective? It seems to me that this function call is erroneous, or am I missing something?

@natevick
Copy link
Author

natevick commented Feb 4, 2022

@bovender Thanks for the kind words! I have gone back and forth on that section of the entrypoint with my team over the years. I honestly believe we landed there because if a setup.lock file did not get cleaned up for some reason when the entrypoint was run again it would stall the starting of the app. Feel free to experiment with it to see what works best in your situation.

Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment