You certainly won't need anything installed other than Docker to create Ruby apps...
The idea is to mount the current folder into a temporary Ruby container using the official Ruby image from Docker Hub, then install Rails inside this temporary container, and then create the project skeleton using the rails new
command.
# Start bash inside a Ruby container:
docker run --rm -v $(pwd):/usr/src -w /usr/src -ti ruby:2.3.1 bash ; cd my_app
A quick explanation of the command flags and options for our initial docker run
command:
-
--rm
will remove the container and it's contents after we finish - the project skeleton will remain in our filesystem, tho, as we're... -
-v $(pwd):/usr/src
...mounting the current directory inside the container's/usr/src
directory. All of the files in our current directory will be accessible inside our temporary container at the specified/usr/src
path. -
-w /usr/src
will start our container in this directory, so we won't need to navigate through the container's filesystem to do our business. -
-ti
enables the interactive mode. Mandatory if we need to issue commands and review the output when we do our stuff. -
ruby:2.3.1
is the base image and version we're using for our temporary container. -
bash
is the command we're using to start the container.
Curious about the ; cd my_app
part? That's so when we finish the following step, we'll be inside our newly created project folder...
Now, once Docker downloaded the image and started the container - and we're inside of it, and using the bash command-line prompt, we can do our business:
# We install Rails first...
gem install rails
# Then let's just make sure we're on the folder we want to create our project
# in:
ls -lah
# Did you see the files that existed in your workspace?
# Next, let's make sure Rails was installed and can run the project create
# command:
rails new --help
# Last, we'll create the project skeleton (your options may vary) - but let's
# disable running bundler after the project creation, as it may take a couple
# of minutes, and we'll end up removing this container, so the install is of
# no use here:
rails new my_app --database=postgresql --skip-bundle
# We've finished! Let's exit the container and look for our newly generated
# project skeleton:
exit
# Now you'll notice that your'e looking to the actual project files in your
# host :)
Now we'll need to create and/or change a couple of files so we can start the services our app will use (i.e. Database, etc) and to start our app in the future. At the very least, we'll need to create/change the following files:
On most of the cases, we'll need to add software apart from what is available in the official ruby image we used earlier on (i.e. add NodeJS as the javascript runtime for asset compilation via Sprockets). That's where having a Dockerfile to create our version of the base image comes handy.
Create a file named dev.Dockerfile
at the root of the project:
# 1: Use ruby 2.3.1 as base:
FROM ruby:2.3.1
# 2: We'll set the application path as the working directory
WORKDIR /usr/src/app
# 3: We'll add the app's binaries path to $PATH:
ENV HOME=/usr/src/app PATH=/usr/src/app/bin:$PATH
# 4: Install node as a javascript runtime for asset compilation. Blatantly
# ripped off from the official Node Docker image's Dockerfile. GPG keys
# listed at https://github.com/nodejs/node
RUN set -ex \
&& for key in \
9554F04D7259F04124DE6B476D5A82AC7E37093B \
94AE36675C464D64BAFA68DD7434390BDBE9B9C5 \
0034A06D9D9B0064CE8ADF6BF1747F4AD2306D93 \
FD3A5288F042B6850C66B31F09FE44734EB7990E \
71DCFD284A79C3B38668286BC97EC7A07EDE3FC1 \
DD8F2338BAE7501E3DD5AC78C273792F7D83545D \
B9AE9905FFD7803F25714661B63B535A4C206CA9 \
C4F0DFFF4E8C1A8236409D08E73BC641CC11F4C8 \
; do \
gpg --keyserver ha.pool.sks-keyservers.net --recv-keys "$key"; \
done \
&& export NPM_CONFIG_LOGLEVEL=info \
&& export NODE_VERSION=6.3.1 \
&& curl -SLO "https://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION-linux-x64.tar.xz" \
&& curl -SLO "https://nodejs.org/dist/v$NODE_VERSION/SHASUMS256.txt.asc" \
&& gpg --batch --decrypt --output SHASUMS256.txt SHASUMS256.txt.asc \
&& grep " node-v$NODE_VERSION-linux-x64.tar.xz\$" SHASUMS256.txt | sha256sum -c - \
&& tar -xJf "node-v$NODE_VERSION-linux-x64.tar.xz" -C /usr/local --strip-components=1 \
&& rm "node-v$NODE_VERSION-linux-x64.tar.xz" SHASUMS256.txt.asc SHASUMS256.txt
# 5: Install the current project gems - they can be safely changed later
# during development via `bundle install` or `bundle update`:
ADD Gemfile* /usr/src/app/
RUN set -ex && bundle install
Just as the .gitignore
file let's us exclude files from the Git project, we can use a .dockerignore
file to keep unwanted files in the project - mostly log files, undesired artifacts, etc - from making it into our image and prevent changes on these from invalidating the image cache:
Create a .dockerignore
file at the root of the project:
# Ignore version control files:
.git/
.gitignore
# Ignore docker and environment files:
*Dockerfile
docker-compose*.yml
*.env
bin/entrypoint-dev
.dockerignore
bin/checkdb
# Ignore log files:
log/*.log
# Ignore temporary files:
tmp/
# Ignore test files:
.rspec
Guardfile
spec/
# Ignore OS artifacts:
**/.DS_Store
.rspec
# 3: Ignore Development container's Home artifacts:
# 3.1: Ignore bash / IRB / Byebug history files
.bash_history
.byebug_hist
.byebug_history
.pry_history
.guard_history
# 3.3: bundler stuff
.bundle/*
An 'entrypoint' in Docker-land is an executable script that is run at the container start, and it's the perfect place to - for example - check if the project's database is installed, or install it otherwise. That way, the database is created automatically when we start working on the project. This is an very robust example development entrypoint: create the entrypoint-dev
file in bin
folder:
#! /bin/bash
set -e
: ${APP_PATH:="/usr/src/app"}
: ${APP_TEMP_PATH:="$APP_PATH/tmp"}
: ${APP_SETUP_LOCK:="$APP_TEMP_PATH/setup.lock"}
: ${APP_SETUP_WAIT:="5"}
# 1: Define the functions lock and unlock our app containers 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: Specify a default command, in case it wasn't issued:
if [ -z "$1" ]; then set -- rails server -p 3000 -b 0.0.0.0 "$@"; fi
# 4: Run the checks only if the app code is going to be executed:
if [[ "$1" = "rails" || "$1" = "sidekiq" ]]
then
# 5: Wait until the setup 'lock' file no longer exists:
while [ -f $APP_SETUP_LOCK ]; do wait_setup; done
# 6: 'Lock' the setup process, to prevent a race condition when the
# project's app containers will try to install gems and setup the
# database concurrently:
lock_setup
# 7: Check if the database exists, or setup the database if it doesn't,
# as it is the case when the project runs for the first time.
#
# We'll use a custom script `check_db` (inside our app's `bin` folder),
# instead of running `rails db:version` to avoid loading the entire rails
# app for this simple check:
# rails db:version || setup
# simple check:
bundle exec checkdb || setup # see `bin/setup` or just add `rails db:setup`
# 8: 'Unlock' the setup process:
unlock_setup
# 9: 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 [[ "$2" = "s" || "$2" = "server" ]]; then rm -rf /usr/src/app/tmp/pids/server.pid; fi
fi
# 10: Execute the given or default command:
exec "$@"
Be sure to add execute permissions to the file by running chmod +x bin/entrypoint-dev
after creating this file.
This is the docker-compose.yml
. You may want to do something like this:
version: "2"
volumes:
postgres-data:
driver: local
app-gems:
driver: local
services:
postgres:
image: postgres:9.5.4 # We'll use the official postgres image.
volumes:
# Mounts a persistable volume inside the postgres data folder, so we
# don't lose the created databases when this container is removed.
- postgres-data:/var/lib/postgresql/data
environment:
# The password we'll use to access the databases:
POSTGRES_PASSWORD: s0m3p455
web:
build:
context: .
dockerfile: dev.Dockerfile
# The name our development image will use:
image: my-namespace/my-app:development
command: rails server -b 0.0.0.0 -p 3000
ports:
# This will bind your port 3000 with the container's port 3000, so we can
# use 'http://localhost:3000' to see our Rails app:
- 3000:3000
links:
# Makes the postgres service a dependency for our app, and also makes it
# visible at the 'db' hostname from this container:
- postgres:db
entrypoint: /usr/src/app/bin/entrypoint-dev
volumes:
# Mounts the app code (".") into the container's "/usr/src/app" folder:
- .:/usr/src/app
# Mounts a persistable volume in the installed gems folder, so we can add
# gems to the app without having to build the development image again:
- app-gems:/usr/local/bundle
# Keeps the stdin open, so we can attach to our app container's process and
# do stuff such as `byebug` or `binding.pry`:
stdin_open: true
# Allows us to send signals (CTRL+C, CTRL+P + CTRL+Q) into the container:
tty: true
environment:
# Notice that this is the DB we'll use:
DATABASE_URL: postgres://postgres:s0m3p455@db:5432/my_app_development
# We'll use this env variable to make the log output gets directed
# to Docker:
RAILS_LOG_TO_STDOUT: "true"
We'll need to remove any configuration related to the environment (hostnames, users, passwords) from the config/database.yml
file so it can use the data in the DATABASE_URL
environment variable instead:
default: &default
encoding: unicode
# Schema search path. The server defaults to $user,public
schema_search_path: partitioning,public
# For details on connection pooling, see rails configuration guide
# http://guides.rubyonrails.org/configuring.html#database-pooling
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
# Minimum log levels, in increasing order:
# debug5, debug4, debug3, debug2, debug1,
# log, notice, warning, error, fatal, and panic
# Defaults to warning.
min_messages: log
development:
<<: *default
# Warning: The database defined as "test" will be erased and
# re-generated from your development database when you run "rake".
# Do not set this db to the same as development or production.
test:
<<: *default
# Production database configuration - bigger pool size, lower log level:
production:
<<: *default
min_messages: notice
Edit the config/environments/development.rb
file so it includes the if ENV["RAILS_LOG_TO_STDOUT"].present?
block:
Rails.application.configure do
# ....
if ENV["RAILS_LOG_TO_STDOUT"].present?
logger = ActiveSupport::Logger.new(STDOUT)
logger.formatter = config.log_formatter
config.logger = ActiveSupport::TaggedLogging.new(logger)
end
end
We're almost done, but due to the fact that we've not created any migration or table just yet, If we try to start the project now, when the development-entrypoint
script hits the setup
command, it will fail after creating the database, because there's no database schema to install...
So we'll need to create the database (and also re-create the missing Gemfile.lock file!) and work out our first database migration before we can finally commit some code into Git and share it with our fellow teammates:
# This will run bash again in our development container, this time with the
# database online via Compose:
docker-compose run --rm web bash
# Now that we're inside, we must use bundler to re-create the Gemfile.lock,
# which is missing because it resides in the image we built only... but we
# want it on the code to be able to commit it into Git:
bundle
# Next, let's create the database:
rails db:create
# This is the ideal point where we should create our first scaffold:
rails g scaffold post title:string body:text
# Now we run 'migrate' to create our first schema dump:
rails db:migrate
# Let's exit back into our host:
exit
# We should be now back in our host :)
Now we're ready to share our code. The only thing needed from now on to run the project is the following commands:
# Start the whole project:
docker-compose up -d # Visit http://localhost:3000 to see it running!
# Attach our terminal to the 'web' container (it must be running) - notice that
# we must use the created container name, not the service name:
docker attach myapp_web_1
# Run one-off commands when the web container is not running:
docker-compose run --rm web bash �# or `rails console`, etc
# Run commands inside a running container:
docker-compose exec web bash
You and your teammates can try these same commands on a freshly cloned copy of the project, and let them see it running automagically in a matter of minutes.
Ref: https://github.com/IcaliaLabs/guides/wiki/Creating-a-new-Rails-application-project-with-Docker