Why to Dockerize your database
Dockerizing helps ease the pain of dependencies
Getting a new developer set up with a Rails app (or any app) can be tedious. Part of the tedium is the chore of manually installing all the dependencies: Ruby, RVM, Rails, PostgreSQL, Redis, etc.
It would be really nice if that developer could just run a single command and have all that app’s dependencies installed on their computer rather than having to install dependencies manually.
This ideal is in fact possible if you fully Dockerize your app. If you want to, you can create a Docker setup that will make it so you don’t have to install anything manually: no Ruby, no Rails, no RVM, no PostgreSQL, no Redis, nothing. It’s very nice.
Fully Dockerizing has drawbacks
Unfortunately, fully Dockerizing a Rails app isn’t without trade-offs. When working with a Dockerized app, there’s a performance hit, there are some issues with using binding.pry
, and system specs/system tests in such a way that you can see them run in a browser is next to impossible.
None of these obstacles is insurmountable, but if you don’t want to deal with these issues, you can choose to Dockerize just some of your app’s dependencies instead of all of them.
Partial Dockerization
The Docker setup I use at work is a hybrid approach. I let Docker handle my PostgreSQL and Redis dependencies. I install all my other dependencies manually. This makes it so I don’t have to live with the downsides of full Dockerization but I still get to skip installing some of my dependencies. Any dependency I can skip is a win.
The example I’m going to show you shortly is an even simpler case. Rather than Dockerizing PostgreSQL and Redis, we’re only going to Dockerize PostgreSQL. I’m doing it this way in the interest of showing the simplest possible example.
Dockerizing for development vs. production
I want to add a note for clarity. The Docker setups I’ve been discussing so far are all development setups. There are two ways to Dockerize an app: for a development environment and for a production environment. Development environments and production environments of course have vastly different needs and so a different Docker setup is required for each. In a production environment we wouldn’t run PostgreSQL and a Rails server on the same machine. We’d have a separate database server instead. So I want to be clear that this Docker setup is for development only.
How to Dockerize your database
In order to Dockerize our database we’re going to use Docker Compose. Docker Compose is a tool that a) lets you specify and configure your development environment’s dependencies, b) installs those dependencies for you, and c) runs those dependencies.
Initializing the Rails app
Before we do anything Docker-related, let’s initialize a new Rails app that uses PostgreSQL.
$ rails new my_app -d postgresql
Adding a Docker Compose config file
Here’s the Docker Compose config file. It’s called docker-compose.yml
and goes at the project root. This file, again, is what specifies our development environment’s dependencies. I’ve annotated the file to help you understand what’s what.
# docker-compose.yml
---
version: '3.8'
# The "services" directive lists all the services your
# app depends on. In this case there's only one: PostgreSQL.
services:
# We give each service an arbitrary name. I've called
# our PostgreSQL service "postgresql".
postgres:
# Docker Hub hosts images of common services for
# people to use. The postgres:13.1-alpine is an
# image that uses the Alpine Linux distribution,
# very lightweight Linux distribution that people
# often use when Dockerizing development environments.
image: postgres:13.1-alpine
# PostgreSQL has to put its data somewhere. Here
# we're saying to put the data in /var/lib/postgresql/data.
# The "delegated" part specifies the strategy for
# syncing the container's data with our host machine.
# (Another option would be "cached".)
volumes:
- postgresql:/var/lib/postgresql/data:delegated
# This says to make our PostgreSQL service available
# on port 5432.
ports:
- "127.0.0.1:5432:5432"
# This section specifies any environment variables
# that we want to exist on our Docker container.
environment:
# Use "my_app" as our PostgreSQL username.
POSTGRES_USER: my_app
# Set POSTGRES_HOST_AUTH_METHOD to "trust" to
# allow passwordless authentication.
POSTGRES_HOST_AUTH_METHOD: trust
volumes:
postgresql:
storage:
Next we’ll have to change config/database.yml
ever so slightly in order to get it to be able to talk to our PostgreSQL container. We need to set the username
to my_app
and set the host
to 127.0.0.1
.
default: &default
adapter: postgresql
encoding: unicode
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
# This must match what POSTGRES_USER was set to in docker-compose.yml.
username: my_app
# This must be 127.0.0.1 and not localhost.
host: 127.0.0.1
development:
<<: *default
database: my_app_development
test:
<<: *default
database: my_app_test
production:
<<: *default
database: my_app_production
username: my_app
password: <%= ENV['DB_PASSWORD'] %>
init.sql
If we put a file called init.sql
at the project root, Docker will find it and execute it. It’s necessary to have an SQL script that creates a user called my_app
or else PostgreSQL will give us an error saying (truthfully) that there’s no user called my_app
.
CREATE USER my_app SUPERUSER;
It’s very important that the init.sql
file is in place before we proceed. If the init.sql
file is not in place or not correct, it can be a difficult error to recover from.
Using the Dockerized database
Run docker-compose up
to start the PostgreSQL service.
$ docker-compose up
Now we can create the database.
$ rails db:create
As long as the creation completed successfully, we can connect to the database.
$ rails db
Now we’re connected to a PostgreSQL database without having had to actually install PostgreSQL.
Is this tutorial missing step to actually spin up docker container with PostgreSQL service?
Wow, looks like I did in fact miss that part! Thanks for pointing that out. I’ve updated the post to include it.
Great! Thanks for the great blog.
Thanks!