After many years using manual tools to deploy my Ruby on Rails applications, I changed my production deployment strategy to use Docker containers few years ago, and it was a great decision!
This tutorial is for begginers that want to dockerize their new (or existing) Rails application.
ℹ️ You can download all files on my Github repository.
Table Of Contents
- Why dockerize my Rails application?
- Starting with a basic Rails application
- Let's start!
- Going production!
- Updating the containers
- Configure your web server
- Final tip!
Why dockerize my Rails application?
Turn you Rails application into docker containers for production deployment can speed up your deployment, prevent errors and make it much easier to move from one service provider to another. It made my life easier (and I bet it can make yours too!).
Once in a container, all dependencies will always be installed and the system already configured to run an optimized version of you application.
It's also easy to update your container, run tests, use CI/CD and automatically deploy new version.
Starting with a basic Rails application
For demonstration purposes, this Rails application is a simple contact list running Rails 6.1. The user can add, view, edit and delete records from the database. All records are saved on a local PostgreSQL instance.
The setup
Everything is setup on the local machine. Rails connects to PostgreSQL through port 5432 and serves the application to browsers through port 3000.
To follow this tutorial, I assume you already have your Rails application working and running. If not, you can use the tutorial application for your tests.
Follows a screenshot of the application running.
Let's start!
There are few steps to build a container to run our application.
- Adjust your database connection configuration.
- Write a
dockerfile
and build the container. - Upload your container to any could provider (free and optional).
- Write a Docker Compose file to execute your application on production.
The config/database.yml
file
This is the basic Rails configuration to connect the application to the datbase. It assumes there's a local instance of PostgreSQL running with username and password configured as below.
default: &default
adapter: postgresql
port: 5432
pool: 5
timeout: 5000
host: localhost
username: postgres
password: postgres
development:
<<: *default
database: devel_db
test:
<<: *default
database: test_db
production:
<<: *default
database: production_db
As you can see, all database connection configuration is fixed to the current environment. This can work for local environments, but as a good practice we should change them to environment variables, so we can adjust easily for any new environment we publish our app.
Let's change host
, username
and password
for now. The others settings can also be changed, but I don't want this tutorial to be too complex 😉
host: <%= ENV['DB_HOST'] || 'localhost' %>
username: <%= ENV['DB_USERNAME'] || 'postgres' %>
password: <%= ENV['DB_PASSWORD'] || 'postgres' %>
⚠️ Notice that we set default values for everything.
The Dockerfile
The next step is create a file called Dockerfile
on the root path of your application. This file will instruct docker on how to build the application based on a image you choose.
I decided to use the image ruby:2.7.6-bullseye
because it's based on Debian 11 (Bullseye) and I like (and I'm used to) this distribuition.
FROM ruby:2.7.6
# Directory where the app will be installed on the container
WORKDIR /app
# Install NodeJS 14 repository
RUN curl -fsSL https://deb.nodesource.com/setup_14.x | bash -
# Update the system and install NodeJS
RUN apt-get update && apt-get -y install \
nodejs \
vim \
&& rm -rf /var/lib/apt/lists/*
# Install ruby gems
COPY Gemfile Gemfile.lock ./
RUN bundle install
# Install Yarn globally
RUN npm install -g yarn
# Run `yarn install`
COPY package.json yarn.lock ./
RUN yarn install
# Copy the application file to the container
COPY . .
# Pre-compile assets for production
RUN RAILS_ENV=production bundle exec rails assets:precompile
# Sets the default command that will run when the container starts
CMD ["bundle", "exec", "rails", "server", "-b", "0.0.0.0"]
ℹ️ More information of
Dockerfile
can be found on this link.
The .dockerignore
file
There are files and folders that we do not want to be copied to our docker container because they are not relevant to the application or they will be generated when the container is created. They are build folders, temporary files, log, etc.
Create a .dockerignore
file on your project root folder and add these lines:
node_modules/
log/
tmp/
Building the container
Now it's time to build our application container.
⚠️ IMPORTANT: The
tag
attribute is the name of the container. Be sure to change it to your Doker Hub repository name, or use any name if you do not intent to push it to a cloud.
docker build . --tag fshayani/dockerizing_rails_application:latest
After the container is built successfully, we will upload it to Docker Hub. This way I can download it automatically on my production server.
ℹ️ This is a optional step. You can skip this step if you intent to build the container and use it on the same machine.
docker push fshayani/dockerizing_rails_application:latest
Going production!
Now that we have built our app container (and maybe pushed it to a cloud; did you?), it's time to move to the production server and deploy that application!
We need tell Docker how to pull and start the container and this can be done easly with docker compose
.
ℹ️ There are others technologies to deploy containers in production instead of Docker Compose, like Kubernetes, Docker Swarm, AWS ECS, etc; but that's not the scope of this tutorial.
The Docker Compose file
Create a docker-compose.yml
file on production server. It can be placed anywhere, but be organized! 😉
services:
app: # That's the name of our application container
image: fshayani/dockerizing_rails_application:latest # The container Docker will use
ports:
- 3000:3000 # Map localhost:3000 to the container's port 3000
environment:
- DB_HOST=db # The name of our PostgreSQL container
- DB_USERNAME=postgres # PostgreSQL credentials
- DB_PASSWORD=postgres
- RAILS_ENV=production # We are on Production now!
- RAILS_SERVE_STATIC_FILES=true # Assets were already built by Dockerfile ;)
- RAILS_LOG_TO_STDOUT=true # So we can see Rails logs with `docker compose logs` command
- RAILS_MASTER_KEY=my_ultra_top_secret_master_key!! # Use you master key. There are better ways to keep this secret.
depends_on:
- db # Will start the container `db` before
db:
image: postgres:14
environment:
- POSTGRES_USER=postgres # Will create a instance of PostgreSQL with this credentials
- POSTGRES_PASSWORD=postgres
volumes:
- postgres:/var/lib/postgresql/data # Map PostgreSQL data to a persistant volume called `postgres`
volumes:
postgres: # Create a persistant volume on local machine, so we do not loose our DB on restarts
Starting up our application
Now that everything is ready, let's start our application on the production server!
⚠️ ATTENTION: If that's our first time to run the application on the server,
probably your database will be empty. You have two options:dump
andrestore
your database on thedb
container or create a new from scratch running this command first:
docker compose run --rm app bundle exec rails db:setup
docker compose up
and open your browser on http://localhost:3000 😄 You will see your application running!
ℹ️ TIP: Add the flag
-d
to the command to run it on the background.
Accessing Rails console
With Docker Compose, we can interact with our container and run any command on it (including bash
if necessary).
docker compose exec app bundle exec rails console
Updating the containers
When you make a new version of your application and want to deploy it to the production, all you need to do is:
- Build the new image.
- Push it to the cloud again.
- Pull it on the production server the production server (
docker compose pull
). - Rebuild your docker compose stack (
docker compose down; docker compose up
).
Configure your web server
Now that you application is deployed on the production server, you will have to configure any web server (Apache, Nginx, etc) to reverse proxy any calls to your domain to the port 3000 on localhost
.
That's a topic for another post ;)
Final tip!
There a application called Watchtower that can automatically fetch any new container you upload to Docker Hub and automatically pulls it and rebuild the docker compose stack. Take a look at it ;)
Top comments (1)
Great stuff! For a production image I’d use a multi-stage build that installs dependencies and compiles assets in one step and then copies the files in the second step.
Your image will be around 1,5GB. With my suggestion and using an alpine image you’d cut that down to 400MB.