In this article I will assume we have a ruby on rails application that is using a database to store some data, and capistrano for deployment and you want to deploy in on your server but instead of deploying directly on your server you want to dockerize the whole project to give it a separate environment and keep it away from your other applications.
Being here probably you already know, docker is a good way to separate give your application a separate environement to work on, you can limit resources and limit access to filesystem, also using docker means your server reuquirements is only the Dockerfile
and on your server you don’t need anything axcept git
and docker
installed, this makes it easier for your to destroy the whole server and you still can get it up by a single command docker-compose up
as long as you have your data volumes in place.
Also for dockerized applications it is easier to migrate to other server, you’ll need to move your data directories or data volumes to the same path on the new server and then execute docker-compose up
on the new server and your whole stack of application and database and any other services is up at an instance.
Lets make this tutorial as simple as possible, let’s assume we have a rails application, and it needs a postgres server, nothing else, no background jobs, no redis, no elasticsearch, actually after this tutorial it will be easier for you to add these services to the stack without much effort.
first we’ll need to add a Dockerfile
for your application, a Dockerfile
is a file which contain your configuration for how to prepare a machine for your application and how to run the main process, and it should depend on a base image, you should use the nearest base image to your requirements, for me I found the ruby official image suitable and it has 3 variats i used the smallest one ruby:slim
So we’ll do the following:
/app
Gemfile
and Gemfile.lock
to the temperory directory and install our gems with bundle install
1FROM ruby:2.3.0-slim
2ENV LANG=C.UTF-8
3RUN apt-get update && apt-get install -qq -y build-essential nodejs libopencv-dev libpq-dev postgresql-client-9.5 imagemagick --fix-missing --no-install-recommends
4
5ENV app /app
6RUN mkdir -p $app
7ENV INSTALL_PATH $app
8ENV RAILS_ENV production
9EXPOSE 3000
10CMD foreman start
11
12COPY Gemfile* /tmp/
13WORKDIR /tmp
14RUN bundle install --without="development test" -j4
15
16WORKDIR $app
17ADD . $app
18RUN mkdir -p tmp/pids
Note: we copied Gemfile
first before the application because docker is versioning each step, so if you didn’t modify your Gemfile
it won’t re-install the gems and will use the cached version instead, that speed building the image, if you just copied the application each time, docker will see that there is a change in the step so it’ll install the gems form scratch each time, and as the image is always a clean machine it will download and install the gems, and for native extensions it will recompile it, and we all know how nokojiri
is a pain (and basically any other native extension gem).
docker-compose.yml
¶docker-compose
is a tool provided by docker team to define images that works as a network, start and stop it together, and it works as a unit, as docker best practice is to run one process per container, and our application needs more than one process (rails application, postgres process), we’ll need another container to hold our database process and link it to our application container.
Our docker-compose.yml
file could be in the following format:
1version: '2'
2services:
3 db:
4 image: "postgres:9.5.2"
5 restart: always
6 env_file:
7 - .env
8 expose:
9 - '5432'
10 volumes:
11 - /root/data/news/db:/var/lib/postgresql/data
12
13 web:
14 build: .
15 depends_on:
16 - db
17 links:
18 - db
19 volumes:
20 - /root/data/news/uploads:/app/public/system
21 - /tmp/news/assets:/app/public/assets
22 restart: always
23 ports:
24 - '127.0.0.1:3000:3000'
25 env_file:
26 - .env
In the previous file we defined 2 services each of them assigned a proper name:
Dockerfile
in the current directory .
, it needs the other container to be up first as specified in depends_on
, and links to the db
container in the same network, also I linked 2 directories for a local directories, the uploads directory and the assets directory, so when you destroy an image and build a new one these are the olny directories that will be kept, also i set the restart: always
to restart the machine whenever docker is restarted, and opened port 3000 to local machine only so that it will be accessible from an HTTP srever (nginx or so), if you wish to map the port to 80 and make it accessible from outside the machine you can replace it with 80:3000
and removing 127.0.0.1
will make it public, also we’ll load an environment file .env
which will be defined later.postgres
official image, it will open port 5432
to the other services inside thsi docker-compose network only ( this means you can make another docker-compose file with this port open and it won’t conflict) and also it means that the database is not accessible form the host machine and could be accessed only from the web
container, also mapped the data directory to local directory on host..env
¶we’re having all of our secrets in one environement file for both containers as follows:
1RAILS_ENV=production
2SECRET_KEY_BASE=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
3DATABASE_NAME=production
4POSTGRES_USER=postgres
5POSTGRES_PASSWORD=postgres
6DATABASE_HOST=db
secrets.yml
file read it from ENV[:SECRET_KEY_BASE]
for production.ENV
in your database.yml
for production section.database.yml
production section.database.yml
.so our database.yml
file should read Env variables in production like the following
1production:
2 adapter: postgresql
3 encoding: unicode
4 pool: 5
5 timeout: 5000
6 host: <%= ENV['DATABASE_HOST'] %>
7 database: <%= ENV['DATABASE_NAME'] %>
8 username: <%= ENV['POSTGRES_USER'] %>
9 password: <%= ENV['POSTGRES_PASSWORD'] %>
it’s reading all most of the configuration fron our ENV
, and these variables are passed from the .env
by docker-compose
, and it’s only one .env
file for all docker containers, so it’s easier modify/create form your CI like jenkins.
you can test this setup by issuing docker-compose up
it should pull images and build yours and launch the stack, the only thing is that our database is not created yet, you can do that by isuing rake db:setup
inside your web container like so docker-compose run web rake db:setup
. You should be able to access your application from http://localhost:3000
.
I created a set of tasks that is hooking to the capistrano deployment process and build your images and issue the up/down/restart command, you can use it from here, so if you didn’t cap init
please do and now we should have config/delpoy.rb
and config/deploy/production.rb
so you’ll need to include capistrano-decompose
to your gemfile gem 'capistrano-decompose, require: false'
and bundle install
, then add it to your capfile
, require 'capistrano/decompose'
, this will load the plugin to capistrano decpolyment flow, now you need to configure the plugin, either in the global deploy.rb
to apply configuration to all environements or add environment specific deployment configuration to config/deploy/production.rb
as follows:
1lock '3.5.0'
2
3set :application, 'application_name'
4set :repo_url, 'git@applicationhost.com/application_repo_name.git'
5set :deploy_to, "/path/to/project/on/server/#{fetch(:application)}"
6set :keep_releases, 1
7set :decompose_restart, [:web]
8set :decompose_web_service, :web
9set :decompose_rake_tasks, ['db:migrate', 'db:seed', 'assets:precompile']
in decompose_rake_tasks
i always seed the database, and in my seed.rb
I make sure that the seeded data is not inserted if it’s already there, this is easier for me to modify and it’ll be executed each deployment, before that if i needed to insert data to the database after the initial seed, I had to create a migration for it, now i reserve migration for structural changes and seed to insert new data as i need.
now if you tried to cap production deploy
you are good to go, your application will be deployed toa the server (make sure you install docker on your server of course), and it will be up on port 3000 as we specified, you can either change port to 80 in docker-compose.yml or add a http server like nginx to redirect traffic to this port for certain host.