Docker Compose: Create a Development Environment
Learn how to set up an efficient development environment for Ruby on Rails applications using Docker Compose. Our tutorial guides you through the process of containerizing, synchronizing application code, and configuring databases and services.
Application development can be complex, especially when setting up development environments. Docker offers an elegant solution by enabling applications to be encapsulated within containers. These containers are isolated, portable, and provide consistent environments, making development, troubleshooting, and deployment of applications easier.
The setup involves the following steps:
- Synchronize application code on the host with the code in the container to facilitate changes during development.
- Persist application data between container restarts.
- Configure Sidekiq workers to process jobs as expected.
Requirements
Before starting this tutorial, you will need:
- A local development or server system with Ubuntu 18.04.
- Docker and Docker Compose installed according to the instructions in the relevant guides.
Step 1: Clone Project and Add Dependencies
git clone https://github.com/do-community/rails-sidekiq.git rails-docker
cd rails-docker
nano Gemfile
gem ‘pg’, ‘~>1.1.3’
# Comment out sqlite gem
# gem ‘sqlite3’
# Comment out spring-watcher-listen gem
# gem ‘spring-watcher-listen’, ‘~> 2.0.0’
Step 2: Configure Application for PostgreSQL and Redis
nano config/database.yml
default: &default
adapter: postgresql
encoding: unicode
database: <%= ENV[‘DATABASE_NAME’] %>
username: <%= ENV[‘DATABASE_USER’] %>
password: <%= ENV[‘DATABASE_PASSWORD’] %>
port: <%= ENV[‘DATABASE_PORT’] || ‘5432’ %>
host: <%= ENV[‘DATABASE_HOST’] %>
pool: <%= ENV.fetch(“RAILS_MAX_THREADS”) { 5 } %>
timeout: 5000
development:
<<: *default
test:
<<: *default
production:
<<: *default
nano .env
DATABASE_NAME=rails_development
DATABASE_USER=sammy
DATABASE_PASSWORD=shark
DATABASE_HOST=database
REDIS_HOST=redis
Step 3: Write Dockerfile and Entry Scripts
nano Dockerfile
FROM ruby:2.5.1-alpine
ENV BUNDLER_VERSION=2.0.2
RUN apk add –update –no-cache \
binutils-gold \
build-base \
curl \
file \
g++ \
gcc \
git \
less \
libstdc++ \
libffi-dev \
libc-dev \
linux-headers \
libxml2-dev \
libxslt-dev \
libgcrypt-dev \
make \
netcat-openbsd \
nodejs \
openssl \
pkgconfig \
postgresql-dev \
python \
tzdata \
yarn
RUN gem install bundler -v 2.0.2
WORKDIR /app
COPY Gemfile Gemfile.lock ./
RUN bundle config build.nokogiri –use-system-libraries
RUN bundle check || bundle install
COPY package.json yarn.lock ./
RUN yarn install –check-files
COPY . ./
ENTRYPOINT [“./entrypoints/docker-entrypoint.sh”]
mkdir entrypoints
nano entrypoints/docker-entrypoint.sh
#!/bin/sh
set -e
if [ -f tmp/pids/server.pid ]; then
rm tmp/pids/server.pid
fi
bundle exec rails s -b 0.0.0.0
chmod +x entrypoints/docker-entrypoint.sh
nano entrypoints/sidekiq-entrypoint.sh
#!/bin/sh
set -e
if [ -f tmp/pids/server.pid ]; then
rm tmp/pids/server.pid
fi
bundle exec sidekiq
chmod +x entrypoints/sidekiq-entrypoint.sh
Step 4: Define Services with Docker Compose
With Docker Compose, we can run the multiple containers required for our setup. We will define our composition services in our main `docker-compose.yml` file. A service in Compose is a running container, and the service definitions you include in your `docker-compose.yml` file contain information on how each container image runs. The Compose tool allows you to define multiple services to create multi-container applications.
Our application setup will include the following services:
- The application itself
- The PostgreSQL database
- Redis
- Sidekiq
We will also include a bind mount as part of our setup so that any code changes we make during development are immediately synchronized with the containers that need access to that code.
Open the `docker-compose.yml` file:
nano docker-compose.yml
First, add the service definition for the application:
version: ‘3.4’
services:
app:
build:
context: .
dockerfile: Dockerfile
depends_on:
– database
– redis
ports:
– “3000:3000”
volumes:
– .:/app
– gem_cache:/usr/local/bundle/gems
– node_modules:/app/node_modules
env_file: .env
environment:
RAILS_ENV: development
The application service definition includes the following options:
– Build: Defines the configuration options for building the application image.
– Depends On: Specifies dependencies that must be provided before starting the app service.
– Ports: Maps host port 3000 to container port 3000.
– Volumes: Defines the volume mounts for the app service.
– Env File and Environment: Sets environment variables passed to the container.
Next, add the following code below the application service definition to define your database service:
database:
image: postgres:12.1
volumes:
– db_data:/var/lib/postgresql/data
– ./init.sql:/docker-entrypoint-initdb.d/init.sql
Unlike the app service, the database service pulls a Postgres image directly from Docker Hub and uses the `db_data` volume to persist application data between container starts.
Then, add the Redis service definition:
redis:
image: redis:5.0.7
Finally, add the Sidekiq service definition:
sidekiq:
build:
context: .
dockerfile: Dockerfile
depends_on:
– app
– database
– redis
volumes:
– .:/app
– gem_cache:/usr/local/bundle/gems
– node_modules:/app/node_modules
env_file: .env
environment:
RAILS_ENV: development
entrypoint: ./entrypoints/sidekiq-entrypoint.sh
Our Sidekiq service resembles our app service in some respects. It uses the same build context and image, environment variables, and volumes. However, it depends on the app, Redis, and database services, so it is started last. Additionally, it uses an entry point that overrides the entry point set in the Dockerfile and starts the Sidekiq service.
Step 5: Add Volume Definitions Below the Sidekiq Service Definition
volumes:
gem_cache:
db_data:
node_modules:
Our top-level `volumes` key defines the `gem_cache`, `db_data`, and `node_modules` volumes. When Docker creates volumes, the contents of the volume are stored in a part of the host filesystem, `/var/lib/docker/volumes/`, managed by Docker. The contents of each volume are stored in a directory under `/var/lib/docker/volumes/` and mounted to each container that uses the volume. This way, our application service’s data persists in the `db_data` volume, even if we remove and recreate the database service.
The finished file looks like this:
version: ‘3.4’
services:
app:
build:
context: .
dockerfile: Dockerfile
depends_on:
– database
– redis
ports:
– “3000:3000”
volumes:
– .:/app
– gem_cache:/usr/local/bundle/gems
– node_modules:/app/node_modules
env_file: .env
environment:
RAILS_ENV: development
database:
image: postgres:12.1
volumes:
– db_data:/var/lib/postgresql/data
– ./init.sql:/docker-entrypoint-initdb.d/init.sql
redis:
image: redis:5.0.7
sidekiq:
build:
context: .
dockerfile: Dockerfile
depends_on:
– app
– database
– redis
volumes:
– .:/app
– gem_cache:/usr/local/bundle/gems
– node_modules:/app/node_modules
env_file: .env
environment:
RAILS_ENV: development
entrypoint: ./entrypoints/sidekiq-entrypoint.sh
volumes:
gem_cache:
db_data:
node_modules:
Save and close the file when you are finished editing.