A basic infrastructure with Docker, Chef, and Rails
Before we get started
This tutorial expects you to have knowledge of the basic Docker concepts such as images, containers, volumes, and linking. If you aren’t familiar, take a look at Getting Started with Docker. In this post, we’ll be building infrastructure in Vagrant with the lightweight resources provided by Chef-Docker.
Our chef kitchen, where we’ll configure our environment specific variables, can be found here: Example Chef Kitchen
What isn’t covered
- Zero downtime deploys - Vamsee Kanakala’s talk on Zero Downtime Deployments with Docker at Garden City Ruby is helpful.
- Backing up your containers
- Deployment to production - I have been deploying to DigitalOcean via Chef solo, however that’s a bit outside the scope of this post.
What are we trying to build?
It’s hard to explain even a simple infrastructure without a diagram. So this is what we’ll be building step-by-step:
In this diagram, each square represents a Docker container built from a Docker image. Everything a container needs to run is either found in the container (e.g. configuration files) or by linking to other containers.
The rails application is linked to a gem cache container to avoid downloading gems everytime we deploy a container.
Gem Cache
Before each section, I’ll reference the chef recipe and the Dockerfile here:
In the Docker world, containers are ephemeral, which means data written to containers disappear when we redeploy a container. Docker volumes allow us to store data outside of a container. In our case, we never want to redownload a gem if it already has been downloaded.
Let’s take a look at the data volume recipe:
include_recipe 'docker'
# 1
cookbook_file 'Dockerfile' do
path '/tmp/Dockerfile'
source 'gem-cache/Dockerfile'
end
# 2
docker_image 'ubuntu' do
tag 'gem-cache'
source '/tmp'
action :build_if_missing
end
#3
docker_container 'gem-cache' do
image 'ubuntu:gem-cache'
container_name 'gem-cache'
detach true
action :run
end
-
The first thing we need to do is copy over our Dockerfile into the context of the Docker build to provide the Docker daemon access to the files when building the image.
-
Next we create the Docker volume image. We set the context of the build to be
/tmp
, which is where we copied our Dockerfile scripts. -
Finally, we use the image to run a container. This container is set to run bash via the
CMD
directive in the Dockerfile.
Postgres
Let’s look at the chef recipe to build our Docker image, since there’s a lot more going on:
# 1
FROM austenito/ruby:2.1.2
# 2
RUN mkdir /postgres
ADD Berksfile /postgres/Berksfile
ADD solo.json /postgres/solo.json
ADD solo.rb /postgres/solo.rb
WORKDIR /postgres
# 3
RUN bash -c 'source /usr/local/share/chruby/chruby.sh; chruby 2.1.2'
RUN berks vendor ./cookbooks
RUN chef-solo -c solo.rb -j solo.json
# 4
VOLUME ["/etc/postgresql", "/var/log/postgresql", "/var/lib/postgresql"]
# 5
EXPOSE 5432
USER postgres
CMD ["/usr/lib/postgresql/9.3/bin/postgres",
"-D", "/var/lib/postgresql/9.3/main", "-c",
"config_file=/etc/postgresql/9.3/main/postgresql.conf"]
-
Pull a pre-built Ruby 2.1.2 image from Docker hub. It has ruby 2.1.2 and chruby installed.
-
Add files in the Docker daemon context to our image.
-
Run chef-solo to build and configure postgres with the files we copied into our image.
-
Expose directories to other containers to allow backups of our data. If we didn’t do this and the container is deleted, all of our data would be lost.
-
Run postgres.
Our chef recipe is similar to the gem cache with a few differences:
- We expose a port (5432) to other containers to allow tcp connections to this container
- We use env to set environmental variables before we run our container
- We set which volumes from other containers that available in this container with volumes_from
...
if `sudo docker ps -a | grep postgres`.size == 0
docker_container 'postgres' do
image 'austenito/postgres:9.3'
container_name 'postgres'
port "5432:5432"
detach true
env ["POSTGRES_USER=#{node['postgresql']['user']}",
"POSTGRES_PASSWORD=#{node['postgresql']['password']}"
]
volumes_from 'gem-cache'
action :run
end
end
Rails application
Our Rails application container uses the link
directive to provide
container linking. This exposes environment
variables with ip and port information of postgres container.
...
docker_container 'rails-example' do
image 'austenito/rails-example'
container_name 'rails-example'
detach true
link ['postgres:db']
volumes_from 'gem-cache'
action :run
port '3000:3000'
end
When the rails-example container starts up, we want to be able to bundle, precompile our assets, migrate, then start our server. Specifying this inline via the CMD
directive
is a bit cumbersome, so we can specify a script to the CMD
directive,
run.sh.
The script clones the latest rails-example repository, bundles, migrates, and starts unicorn.
Nginx
Unlike the postgres container, which uses a chef recipe for configuration, we will configure nginx manually via apt and copying over our nginx.conf file. Using the nginx chef recipe turns out to be a bit too compilicated for our simple infrastructure example.
Other than that, there is nothing new to see in the recipe or the Dockerfile. Our Dockerfile installs nginx and we run a container with nginx with our chef recipe.
Putting it all together
We’re ready to deploy our infrasture to vagrant. Run the following commands:
git clone https://github.com/austenito/docker-chef-rails-example
bundle
berks
vagrant plugin install vagrant-omnibus
vagrant up
If everything went right the first time (it always does right?), you can visit the example app at http://localhost:8080/.
Let’s ssh into our vagrant box by running vagrant ssh
and poke around. First let see what images we created:
sudo docker images
REPOSITORY TAG IMAGE ID
austenito nginx a10b38bf0975
austenito/rails-example latest 38c2ed56c811
austenito/postgres 9.3 88c026cf2326
ubuntu gem-cache 11f9a661754b
ubuntu 14.04 c4ff7513909d
austenito/ruby 2.1.2 c794944b5fa2
These images were built by the docker_image
directive in our chef recipes. They are used to create and run the containers below:
sudo docker ps -a
IMAGE PORTS NAMES
austenito:nginx 0.0.0.0:80->80/tcp nginx
austenito/rails-example:latest 0.0.0.0:3000->3000/tcp nginx/rails_example,rails-example
austenito/postgres:9.3 0.0.0.0:5432->5432/tcp nginx/rails_example/db,postgres,rails-example/db
ubuntu:gem-cache gem-cache
What if I don’t want to build my images from the ground up?
Great question! Part of the power of Docker is the ability to push your images (ala git style) to Docker Hub so you don’t have to rebuild images.
Ruby takes the longest to compile and I’ve set the docker_image
directive in the postgres Dockerfile to point to austenito/ruby:2.1.2
.