Docker for all environments

Vinicius Negrisolo Vinicius Negrisolo Docker Rails

Should we use Docker 🐳 for local development ❓? It seems easy to argue to not use it, just use local and that’s it. But it’s also easy to find cases where the convenience of Docker play a big role on a daily basis job. In case you want to consider it for any reason you have it read this post and have fun.

Some context

Docker 🐳 is a great tool for packaging your application and its requirements. You can automate every step to automate it’s creation including packages to install, environment variable, manipulating files, running commands, etc. For all these reasons a lot of companies are adopting it on 🚀 production.

On the other hand in order to develop and test a single application sometimes it’s needed to run several other ones. Also application may need some dependencies like key-value store, caching, databases, search engines, messaging services and so on. For modern and well constructed applications this might not be a big problem, but for legacy ones this scenario is a 😱 nightmare.

Unfortunately there are innumerous reasons for not running an application locally so let’s face it and use all the convenience of Docker for your development mode as well.

To reinforce my opinion, for a faster and better development we should always run applications on local host machine. You usually have more control, it’s easily debug it, etc. Anyway, this post is about when this is not possible.

The approach

I’ll use as an example a simple Ruby on Rails application to show how to configure docker on both development and production environments. You can port the same ideas to your own codebase. By the file names you will see that I chose development to be my default environment.

Dockerfile

So let’s start with the Dockerfile that’s used to build Docker images.

FROM ruby:2.4.1-alpine3.6
WORKDIR /app

RUN apk --no-cache add \
    build-base \
    nodejs nodejs-npm \
    postgresql-dev

COPY bin/wait-for /usr/local/bin/

RUN gem install bundler
RUN bundle config --global jobs 4
RUN bundle config --global retry 3

COPY Gemfile* /app/
# rails default to RAILS_ENV=development
RUN bundle install

# docker volume instead
# assets:precompile is useless in dev
FROM ruby:2.4.1-alpine3.6
WORKDIR /app

RUN apk --no-cache add \
    build-base \
    nodejs nodejs-npm \
    postgresql-dev

COPY bin/wait-for /usr/local/bin/

RUN gem install bundler
RUN bundle config --global jobs 4
RUN bundle config --global retry 3

COPY Gemfile* /app/
ENV RAILS_ENV=production
RUN bundle install --without development test

COPY . /app/
RUN bundle exec rails assets:precompile

The idea behind this code snippets is to show that we want to make the images as similar as it possible, but let’s face it, there are differences. In this case library dependencies will change. Another difference is on extra steps for production such as precompiling assets. But it could be more than that.

In production mode I am copying all files from the application (except the ignored ones) into /app folder. I don’t do that in development mode because I want to override it with files I have it on my host machine. In this case I can change them and this will be automatically read by the running container.

Compose the enviroment

Here it comes my docker-compose.yml files:

---
version: "3"
services:
  web_dev:
    build: .
    volumes:
      - "$PWD:/app"
    command: >
      sh -c 'wait-for pg_dev:5432 &&
             bundle exec rails server'
    expose: ["3000"]
    ports: ["3000:3000"]
    depends_on: ["pg_dev"]

  pg_dev:
    image: postgres
    ports: ["5432:5432"]
    environment:
      POSTGRES_PASSWORD: postgres
---
version: "3"
services:
  web_prod:
    build:
      context: .
      dockerfile: Dockerfile.prod
    command: >
      sh -c 'wait-for pg_prod:5432 &&
             bundle exec rails server'
    expose: ["3000"]
    ports: ["3000:3000"]
    depends_on: ["pg_prod"]
    env_file: [".env.prod"]

  pg_prod:
    image: postgres
    ports: ["5432:5432"]
    env_file: [".env.prod"]

First thing is the explicit usage of a suffix _dev or _prod. I’m still not convinced that this is great, but so far that’s not clear to me I preferred to have docker service names very explicitly so I can avoid bad usage of environments. It might be very dangerous to try destroy a development database and ends up destroying a production one.

A difference to be highlighted is the usage of volume entry on development mode. As I mentioned before, as soon as a developer change the code it will be reflected inside the web_dev container.

The way that we deal with environment variables will change as well. In this case for production I am loading these values from a file that’s ignored from my git repo. I am pretty sure that this is not the best solution but that’s also not the scope of this post.

Read my blog post about Wait for Docker container to understand how to do that if you want.

Finally you may want to reuse part of this yml configuration, so take a look into docker-compose override files. This might be a good solution for big projects with extensive configurations.

Ignore files

It’s nice to reinforce that git and docker have its ignore files for different purposes. This is how I set my ignore files to work:

.bundle/
.env.prod
log/
tmp/
.bundle/
.dockerignore
.git/
.gitignore
Dockerfile*
docker-compose*
log/
public/assets/
tmp/

Environment variable file

Finally a simple .env.prod:

POSTGRES_PASSWORD=postgres123
RAILS_SERVE_STATIC_FILES=true
SECRET_KEY_BASE=123abc

Remember this is just used by production environment and this is ignored by git.

Running containers

With all that set here I have some example commands to test both environments:

alias dc="docker-compose"

dc up --build -d
dc logs -f

dc run web_dev bundle exec rubocop
dc run web_dev bundle exec rails db:migrate
dc run web_dev bundle exec rspec
dc run -it web_dev bundle exec rails console

dc down
alias dc="docker-compose -f docker-compose.prod.yml"

dc up --build -d
dc logs -f

dc run web_prod bundle exec rails db:create
dc run web_prod bundle exec rails db:migrate
dc run web_prod bundle exec rake app:calculate
dc run -it web_prod sh

dc down

These are just a sample of how to interact with our containers using docker-compose. Notice that I created an alias just for simplifying 😉 that code snippet, but the real change is on loading the default docker-compose.yml file or setting it to -f docker-compose.prod.yml.

Conclusion

Docker may help us to work on 💩 brown-field projects. Or you just don’t want to install a lot of applications to start with right? In one case or the other I hope you have enjoyed 👍 this reading.


Read also:

Setup Twitter Bootstrap on Phoenix projects JavaScript Elixir Phoenix

This is a short post, more like a straightforward recipe for new Elixir on Phoenix projects to use twitter bootstrap. Kind of sharing some frontend management tips to backend developers like me.

Wait for Docker Container Docker

You may have some docker 🐳 containers 📦 to start your app but there are some startup order to be followed. You are probably using a solution such as docker-compose and wonder why they don’t have this implemented yet? On this blog post I’ll present my solution for this problem, a very simple shell script for waiting a container.