Introduction
These are the configurations I use to deploy a Rails 6 app. Note that this is a small hobby app so I’m using docker-compose
to deploy it, for more serious apps you’d want to use docker swarm
or kubernetes
.
If you don’t have a server ready or docker
and docker-compose
installed on your local dev machine then have a read through this blog post first.
For this build I’m not going to dockerize NGINX as I have it installed on the server.
Create a Dockerfile
Step one is to create a Dockerfile
in the root directory of your project. For this build I’ll be using ruby:2.6.5-alpine3.11
image. alpine
is a much slimmer Linux docker image and will reduce our overall image sizes.
# Extend from the official Ruby image
FROM ruby:2.6.5-alpine3.11
ENV RAILS_ENV production
ENV RAILS_SERVE_STATIC_FILES true
ENV RAILS_LOG_TO_STDOUT true
# Install required libraries on Alpine
# note: build-base required for nokogiri gem
# note: postgresql-dv required for pg gem
RUN apk update && apk upgrade && \
apk add tzdata postgresql-dev && \
apk add postgresql-client && \
apk add nodejs yarn && \
apk add build-base
# Throw errors if Gemfile has been modified since Gemfile.lock
RUN bundle config --global frozen 1
# Copy Gemfile so we can cache gems
COPY Gemfile Gemfile.lock ./
# Fix issue with sassc gem
RUN bundle config --local build.sassc --disable-march-tune-native
# Install Ruby gems
RUN bundle install --without development test
# Copy all application files
COPY . .
# Precompile assets
RUN SECRET_KEY_BASE=`bundle exec rails secret` bundle exec rails assets:precompile
# Run entrypoint.sh script
RUN chmod +x entrypoint.sh
CMD ["/entrypoint.sh"]
In order to precompile the assets during the image build a SECRET_KEY_BASE
needs to be set. We don’t want to hard-code a value into our Dockerfile though as we’ll set this later via an environment variable, so this is a “hack” to get the build to pass. If you know of a better way please let me know.
Note: if you’re only planning to use this setup to test your app locally then don’t set the
RAILS_ENV
toproduction
.
Note that you’ll only need the RUN bundle config --local build.sassc --disable-march-tune-native
line if you’re running into the following error:
/usr/local/bundle/gems/ffi-1.11.1/lib/ffi/library.rb:112: at 0x00007fdf4c5d7575
ruby 2.6.5p114 (2019-10-01 revision 67812) [x86_64-linux-musl]
-- Control frame information -----------------------------------------------
c:0084 p:---- s:0473 e:000472 CFUNC :open
c:0083 p:0022 s:0467 e:000466 BLOCK /usr/local/bundle/gems/ffi-1.11.1/lib/ffi/library.rb:112 [FINISH]
c:0082 p:---- s:0458 e:000457 CFUNC :each
c:0081 p:0113 s:0454 e:000453 BLOCK /usr/local/bundle/gems/ffi-1.11.1/lib/ffi/library.rb:109 [FINISH]
c:0080 p:---- s:0447 e:000446 CFUNC :map
c:0079 p:0069 s:0443 e:000442 METHOD /usr/local/bundle/gems/ffi-1.11.1/lib/ffi/library.rb:99
c:0078 p:0079 s:0436 e:000435 CLASS /usr/local/bundle/gems/sassc-2.2.1/lib/sassc/native.rb:11
c:0077 p:0007 s:0432 e:000431 CLASS /usr/local/bundle/gems/sassc-2.2.1/lib/sassc/native.rb:6
c:0076 p:0014 s:0429 e:000428 TOP /usr/local/bundle/gems/sassc-2.2.1/lib/sassc/native.rb:5 [FINISH]
...
[NOTE]
You may have encountered a bug in the Ruby interpreter or extension libraries.
Bug reports are welcome.
For details: https://www.ruby-lang.org/bugreport.html
Aborted (core dumped)
/usr/local/bundle/gems/ffi-1.11.1/lib/ffi/library.rb:112: [BUG] Illegal instruction at 0x00007f128c9ec575
ruby 2.6.5p114 (2019-10-01 revision 67812) [x86_64-linux-musl]
...
You can read more about this error here.
Create a docker-compose.yml file
Next create a docker-compose.yml
file in the root directory of your project.
To find out which version you can define at the top of your file check docker -v
and compare to the version reference here.
version: "3.7"
services:
rails:
build:
context: .
env_file:
- rails.env
environment:
DB_USERNAME: your_db_user
DB_DATABASE: your_db_name
DB_PORT: 5432
DB_HOST: db
RAILS_MAX_THREADS: 5
PORT: 3000
ports:
- 3000:3000
restart: always
depends_on:
- "db"
db:
image: postgres:10.12-alpine
env_file:
- db.env
environment:
POSTGRES_USER: your_db_user
PGDATA: /var/lib/postgresql/data/pgdata
restart: always
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:
I’ve set both the rails
app and the db
(PostgreSQL) to always restart with restart: always
. If the container crashes or you reboot the server these services will start up automatically.
The Rails app depends on the database being up to run, we’ve set this with the depends on: - db
line. We’re also creating a pgdata
volume so the database data is persisted.
Note that we’re using an env_file:
to set some environment variables as well as environment:
for other variables. You can choose which variables you want committed to your version control system and which not to commit.
Create the .env files
Now we’ll create rails.env
and db.env
, you can name the files anything as long as they match the configuration in your docker-compose.yml
file.
The rails.env
file:
DB_PASSWORD=your_db_password
SECRET_KEY_BASE=rails_secret_key # generate this with 'rails secret'
The db.env
file
POSTGRES_PASSWORD=your_db_password
Add the .env files to your .gitignore
We don’t want to commit our secret keys to a version control system so add the following to your .gitignore
file:
# Ignore environment files
*.env
Create a .dockerignore file
When copying our files into the docker
container we don’t want folders such as node_modules
to be copied. Also we don’t want any files with sensitive information such as .env
or our master.key
to be copied into the container.
You can use the file below as an example:
# Ignore bundler config.
.bundle
# Ignore all logfiles and tempfiles.
log/
tmp/
# Ignore uploaded files in development.
storage/
public/assets
# Byebug history
.byebug_history
# Ignore master key for decrypting credentials and more.
config/master.key
# Node modules, yarn etc.
public/packs
public/packs-test
node_modules
yarn-error.log
yarn-debug.log*
.yarn-integrity
# Ignore environment files
*.env
# Ignore database dumps
*.dump
# Misc files
.dockerignore
.git
.cache
Create an entrypoint file
Create an entrypoint.sh
file in the project’s root directory:
#!/bin/sh
# Note: !/bin/sh must be at the top of the line,
# Alpine doesn't have bash so we need to use shell.
# Docker entrypoint script.
# Don't forget to give this file execution rights via `chmod +x entrypoint.sh`
# which I've added to the Dockerfile but you could do this manually instead.
# Wait until Postgres is ready before running the next step.
while ! pg_isready -q -h $DB_HOST -p $DB_PORT -U $DB_USERNAME
do
echo "$(date) - waiting for database to start."
sleep 2
done
# If the database exists, migrate. Otherwise setup (create and migrate)
echo "Running database migrations..."
bundle exec rails db:migrate 2>/dev/null || bundle exec rails db:create db:migrate
echo "Finished running database migrations."
# Remove a potentially pre-existing server.pid for Rails.
echo "Deleting server.pid file..."
rm -f /tmp/pids/server.pid
# Start the server.
echo "Starting rails server..."
bundle exec rails server
Although we’ve set depends_on: - db
in our docker-compose.yml
file, this only waits for the PostgreSQL container to run, however the database will still be starting up and we don’t want to try running any migrations etc. until it’s ready to accept connections hence the while ! pg_isready
line.
Note: Alpine doesn’t have
bash
installed so make sure yourentrypoint.sh
is ashell
script, or you can addapk add bash
to yourDockerfile
.
Shoutout: migrations check taken from here.
Configure the Rails app production settings
Before we can spin up the containers with docker-compose up
we also need to configure the app’s database in config/database.yml
:
production:
<<: *default
database: <%= ENV['DB_DATABASE'] %>
username: <%= ENV['DB_USERNAME'] %>
password: <%= ENV['DB_PASSWORD'] %>
host: <%= ENV['DB_HOST'] %>
We’re mapping the environment variables to the ones we set in the docker-compose.yml
file.
Note: if you’re only planning to use this setup to test your app locally then don’t change the production
settings in your database.yml
but the development
settings instead.
Testing the build in development
To test everything is working properly before deploying you can run docker-compose up --build
and visit http://localhost:3000
.
Note that this will run your app in production
mode so if you want a more elegant solution you can create multiple compose files and pass in your build arguments via the compose file.
Deploying the app
If you’re planning on deploying the app using docker-compose and docker contexts then follow this blog post. Again I only recommend this for smaller hobby apps, e.g. a personal blog, and not applications that require high availability and rolling upgrades - for that look to using docker swarm
or kubernetes
.
If this is your first time containerizing an app then docker-compose
is a good way of getting started however.