If you are a developer who has had a foot in many projects at the same time, you must have encountered annoying problems with the version or library used in each project. Then, every time you switch back and forth between environments, you have to spend time reinstalling everything. Or more simply, the code on the company computer runs, but when you get home, you open the machine to code the unfinished code and do not understand why it cannot run anymore. That’s when you think about docker. With the slogan “build once, run anywhere” , you will definitely not have a headache any more. Today we will build a Ruby on Rails application in a Docker environment.
Docker compose
Before getting into the main point, we need to define which containers will be created as well as the method of communication between them.
- The first is the container
app
that handles the main logic of the application. - Next is the
db
container which will be the place to store the data. - Finally, the
nginx
container functions as a web server.
Those are also the most basic components of any web application. It is also important to identify these components, because it gives you an overview of the entire system and functionality of each component to optimize your containers accordingly. Below is the content of the file docker-compose.yml
, it is like a guide to build the image as well as the relationship between the containers:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | <span class="token key atrule">version</span> <span class="token punctuation">:</span> <span class="token string">"3.7"</span> <span class="token key atrule">services</span> <span class="token punctuation">:</span> <span class="token key atrule">nginx</span> <span class="token punctuation">:</span> <span class="token key atrule">build</span> <span class="token punctuation">:</span> <span class="token key atrule">context</span> <span class="token punctuation">:</span> docker/nginx <span class="token key atrule">dockerfile</span> <span class="token punctuation">:</span> Dockerfile <span class="token key atrule">args</span> <span class="token punctuation">:</span> <span class="token punctuation">-</span> HOST=$ <span class="token punctuation">{</span> HOST <span class="token punctuation">}</span> <span class="token key atrule">depends_on</span> <span class="token punctuation">:</span> <span class="token punctuation">-</span> app <span class="token key atrule">ports</span> <span class="token punctuation">:</span> <span class="token punctuation">-</span> <span class="token datetime number">80:80</span> <span class="token punctuation">-</span> 443 <span class="token punctuation">:</span> <span class="token number">443</span> <span class="token key atrule">env_file</span> <span class="token punctuation">:</span> <span class="token punctuation">-</span> .env <span class="token key atrule">app</span> <span class="token punctuation">:</span> <span class="token key atrule">depends_on</span> <span class="token punctuation">:</span> <span class="token punctuation">-</span> db <span class="token key atrule">env_file</span> <span class="token punctuation">:</span> <span class="token punctuation">-</span> .env <span class="token key atrule">ports</span> <span class="token punctuation">:</span> <span class="token punctuation">-</span> 3000 <span class="token punctuation">:</span> <span class="token number">3000</span> <span class="token key atrule">build</span> <span class="token punctuation">:</span> <span class="token key atrule">context</span> <span class="token punctuation">:</span> . <span class="token key atrule">dockerfile</span> <span class="token punctuation">:</span> docker/app/Dockerfile <span class="token key atrule">volumes</span> <span class="token punctuation">:</span> <span class="token punctuation">-</span> . <span class="token punctuation">:</span> /app <span class="token key atrule">command</span> <span class="token punctuation">:</span> sh /scripts/command.sh <span class="token key atrule">stdin_open</span> <span class="token punctuation">:</span> <span class="token boolean important">true</span> <span class="token key atrule">tty</span> <span class="token punctuation">:</span> <span class="token boolean important">true</span> <span class="token key atrule">db</span> <span class="token punctuation">:</span> <span class="token key atrule">image</span> <span class="token punctuation">:</span> mysql <span class="token punctuation">:</span> <span class="token number">5.7</span> <span class="token key atrule">restart</span> <span class="token punctuation">:</span> on <span class="token punctuation">-</span> failure <span class="token key atrule">env_file</span> <span class="token punctuation">:</span> <span class="token punctuation">-</span> .env <span class="token key atrule">environment</span> <span class="token punctuation">:</span> <span class="token punctuation">-</span> MYSQL_ROOT_PASSWORD=$ <span class="token punctuation">{</span> DATABASE_ROOT_PASSWORD <span class="token punctuation">}</span> <span class="token punctuation">-</span> MYSQL_DATABASE=$ <span class="token punctuation">{</span> DATABASE_NAME <span class="token punctuation">}</span> <span class="token punctuation">-</span> MYSQL_USER=$ <span class="token punctuation">{</span> DATABASE_USER <span class="token punctuation">}</span> <span class="token punctuation">-</span> MYSQL_PASSWORD=$ <span class="token punctuation">{</span> DATABASE_PASSWORD <span class="token punctuation">}</span> <span class="token key atrule">volumes</span> <span class="token punctuation">:</span> <span class="token punctuation">-</span> mysql_data <span class="token punctuation">:</span> /var/lib/mysql <span class="token key atrule">volumes</span> <span class="token punctuation">:</span> <span class="token key atrule">mysql_data</span> <span class="token punctuation">:</span> |
In the file docker-compose.yml
, in each block we pay attention to the following details:
build
:context
: This is where theDockerfile
will be run.dockerfile
: Specifies the docker file to be used for the build.args
: Added values inDockerfile
depends_on
: Definition to bind containers together, used instead oflinks
.volumes
: Specifies how much space the server will mount into the container.env_file
: The env file location, you can use these env variables anywhere in thedocker-compose.yml
file.environment
: A place to define the environment variables in the container.command
: This is the file sh that will be run when the container is started.
Dockerfile
For convenience of management, we will create folders corresponding to each component, with app
and nginx
their respective folders are docker/app
, docker/nginx
.
Config app
Below is the content docker/app/Dockerfile
where we will specify the components that make up the image app
from which to build the app
container:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | <span class="token keyword">FROM</span> ruby <span class="token punctuation">:</span> 2.6.6 <span class="token punctuation">-</span> slim <span class="token punctuation">-</span> buster <span class="token keyword">WORKDIR</span> /app <span class="token keyword">COPY</span> Gemfile* ./ <span class="token keyword">COPY</span> package.json ./ <span class="token keyword">COPY</span> yarn.lock ./ <span class="token keyword">RUN</span> apt <span class="token punctuation">-</span> get update && apt <span class="token punctuation">-</span> get install build <span class="token punctuation">-</span> essential <span class="token punctuation">-</span> y <span class="token punctuation">-</span> <span class="token punctuation">-</span> no <span class="token punctuation">-</span> install <span class="token punctuation">-</span> recommends gnupg2 curl wget nodejs patch ruby <span class="token punctuation">-</span> dev zlib1g <span class="token punctuation">-</span> dev liblzma <span class="token punctuation">-</span> dev libmariadb <span class="token punctuation">-</span> dev <span class="token keyword">RUN</span> curl <span class="token punctuation">-</span> sS https <span class="token punctuation">:</span> //dl.yarnpkg.com/debian/pubkey.gpg <span class="token punctuation">|</span> apt <span class="token punctuation">-</span> key add <span class="token punctuation">-</span> && echo <span class="token string">"deb https://dl.yarnpkg.com/debian/ stable main"</span> <span class="token punctuation">|</span> tee /etc/apt/sources.list.d/yarn.list && apt <span class="token punctuation">-</span> get update && apt <span class="token punctuation">-</span> get install yarn <span class="token punctuation">-</span> <span class="token punctuation">-</span> no <span class="token punctuation">-</span> install <span class="token punctuation">-</span> recommends <span class="token punctuation">-</span> y <span class="token keyword">RUN</span> gem install bundler <span class="token punctuation">:</span> 2.1.4 <span class="token keyword">RUN</span> bundle install <span class="token keyword">RUN</span> bundle exec rails db <span class="token punctuation">:</span> prepare <span class="token keyword">RUN</span> yarn install <span class="token punctuation">-</span> <span class="token punctuation">-</span> check <span class="token punctuation">-</span> files <span class="token keyword">COPY</span> docker/app/*.sh /scripts/ <span class="token keyword">RUN</span> chmod a+x /scripts/*.sh |
As seen in the docker-compose.yml file’s content, we use the context .
for the image app, this means that the docker/app/Dockerfile
will run as if it were outside the app’s root directory. That’s why we can use the commands:
1 2 3 4 5 6 | <span class="token keyword">COPY</span> Gemfile* ./ <span class="token keyword">COPY</span> package.json ./ <span class="token keyword">COPY</span> yarn.lock ./ |
We also use a docker/app/command.sh
to run the commands before starting the server:
1 2 3 4 5 6 7 8 | <span class="token function">yarn</span> <span class="token function">install</span> --check-files bundle <span class="token function">install</span> bundle <span class="token builtin class-name">exec</span> rails db:prepare db:migrate bundle <span class="token builtin class-name">exec</span> rails s -p <span class="token number">3000</span> -b <span class="token number">0.0</span> .0.0 |
Notice that we previously put this file from the outside into the container and set its permissions with the command:
1 2 3 4 | <span class="token keyword">COPY</span> docker/app/*.sh /scripts/ <span class="token keyword">RUN</span> chmod a+x /scripts/*.sh |
Config Nginx
At this point we can also run our application, but in order for everything to be closer to the new deploy environment, we need to have another container called ngnix
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | <span class="token keyword">FROM</span> nginx <span class="token punctuation">:</span> 1.17.9 <span class="token comment"># set environment variables</span> <span class="token keyword">ARG</span> HOST <span class="token keyword">ARG</span> APP_PATH=/app <span class="token comment"># Install dependencies</span> <span class="token keyword">RUN</span> apt <span class="token punctuation">-</span> get update && apt <span class="token punctuation">-</span> get <span class="token punctuation">-</span> y install vim curl openssl apache2 <span class="token punctuation">-</span> utils <span class="token punctuation">-</span> <span class="token punctuation">-</span> no <span class="token punctuation">-</span> install <span class="token punctuation">-</span> recommends apt <span class="token punctuation">-</span> utils && rm <span class="token punctuation">-</span> r /var/lib/apt/lists/* <span class="token comment"># Set our working directory inside the image</span> <span class="token keyword">WORKDIR</span> $ <span class="token punctuation">{</span> APP_PATH <span class="token punctuation">}</span> <span class="token comment"># Copy Nginx config template</span> <span class="token keyword">COPY</span> nginx.conf /tmp/ <span class="token keyword">RUN</span> envsubst <span class="token string">'${APP_PATH} ${HOST}'</span> < /tmp/nginx.conf <span class="token punctuation">></span> /etc/nginx/nginx.conf <span class="token keyword">EXPOSE</span> 80 <span class="token keyword">CMD</span> <span class="token punctuation">[</span> <span class="token string">"nginx"</span> <span class="token punctuation">,</span> <span class="token string">"-g"</span> <span class="token punctuation">,</span> <span class="token string">"daemon off;"</span> <span class="token punctuation">]</span> |
Above is the content in the file docker/nginx/Dockerfile
, it is the instructions to build the necessary components for the nginx
container. Next, we will prepare the file docker/nginx/nginx.conf
to configure for nginx with the following content:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | <span class="token keyword">user</span> nginx <span class="token punctuation">;</span> <span class="token keyword">worker_processes</span> <span class="token number">1</span> <span class="token punctuation">;</span> <span class="token keyword">error_log</span> <span class="token operator">/</span> var <span class="token operator">/</span> log <span class="token operator">/</span> nginx <span class="token operator">/</span> error <span class="token punctuation">.</span> log warn <span class="token punctuation">;</span> <span class="token keyword">pid</span> <span class="token operator">/</span> var <span class="token operator">/</span> run <span class="token operator">/</span> nginx <span class="token punctuation">.</span> <span class="token keyword">pid</span> <span class="token punctuation">;</span> <span class="token keyword">events</span> <span class="token punctuation">{</span> <span class="token keyword">worker_connections</span> <span class="token number">1024</span> <span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token keyword">http</span> <span class="token punctuation">{</span> <span class="token keyword">root</span> <span class="token variable">$APP_PATH</span> <span class="token operator">/</span> public <span class="token punctuation">;</span> <span class="token keyword">keepalive_timeout</span> <span class="token number">65</span> <span class="token punctuation">;</span> <span class="token keyword">include</span> <span class="token operator">/</span> etc <span class="token operator">/</span> nginx <span class="token operator">/</span> mime <span class="token punctuation">.</span> <span class="token keyword">types</span> <span class="token punctuation">;</span> <span class="token keyword">default_type</span> application <span class="token operator">/</span> octet <span class="token operator">-</span> stream <span class="token punctuation">;</span> <span class="token keyword">access_log</span> <span class="token operator">/</span> var <span class="token operator">/</span> log <span class="token operator">/</span> nginx <span class="token operator">/</span> access <span class="token punctuation">.</span> log main <span class="token punctuation">;</span> <span class="token keyword">error_log</span> <span class="token operator">/</span> var <span class="token operator">/</span> log <span class="token operator">/</span> nginx <span class="token operator">/</span> error <span class="token punctuation">.</span> log warn <span class="token punctuation">;</span> <span class="token keyword">sendfile</span> on <span class="token punctuation">;</span> <span class="token keyword">upstream</span> <span class="token variable">$HOST</span> <span class="token punctuation">{</span> <span class="token keyword">server</span> app <span class="token punctuation">:</span> <span class="token number">3000</span> <span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token keyword">server</span> <span class="token punctuation">{</span> <span class="token keyword">listen</span> <span class="token number">80</span> <span class="token punctuation">;</span> <span class="token keyword">server_name</span> localhost <span class="token number">127.0</span> <span class="token number">.0</span> <span class="token number">.1</span> <span class="token punctuation">;</span> <span class="token keyword">location</span> <span class="token operator">/</span> <span class="token punctuation">{</span> <span class="token keyword">proxy_pass</span> <span class="token keyword">http</span> <span class="token punctuation">:</span> <span class="token operator">/</span> <span class="token operator">/</span> <span class="token variable">$HOST</span> <span class="token punctuation">;</span> <span class="token keyword">proxy_set_header</span> X <span class="token operator">-</span> Forwarded <span class="token operator">-</span> For <span class="token variable">$remote_addr</span> <span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> <span class="token keyword">include</span> <span class="token operator">/</span> etc <span class="token operator">/</span> nginx <span class="token operator">/</span> conf <span class="token punctuation">.</span> d <span class="token operator">/</span> <span class="token operator">*</span> <span class="token punctuation">.</span> conf <span class="token punctuation">;</span> <span class="token punctuation">}</span> |
We copy this file into the / tmp / directory of the container with the command:
1 2 | <span class="token keyword">COPY</span> nginx.conf /tmp/ |
Since Nginx doesn’t support us pass the environment variables into the config file, so to fix this, we used envsubst
to replace the content in the /tmp/nginx.conf
file before including it in /etc/nginx/nginx.conf
:
1 2 | <span class="token keyword">RUN</span> envsubst <span class="token string">'${APP_PATH} ${HOST}'</span> < /tmp/nginx.conf <span class="token punctuation">></span> /etc/nginx/nginx.conf |
Build
Prepare a .env
file to store all the environment variables used in the application, it should look like this:
1 2 3 4 5 6 7 8 9 | HOST=localhost DATABASE_HOST=localhost DATABASE_ROOT_PASSWORD=root DATABASE_NAME=rails_docker DATABASE_PASSWORD=root DATABASE_USER=root |
Everything looks good, now it’s time to enjoy our work, first to build images:
1 2 | docker-compose build |
If all the images have been successfully built, then run the following command:
1 2 | docker-compose up |
Open up your browser and type in the link http: // localhost if the screen shows “Yay! You’re on Rails” then we are considered successful.
Summary
Recently, we went together to build a Rails application using Docker. Above I use the latest versions of Rails, Ruby and Docker. Hopefully the article will be somewhat useful for you to manually configure Docker for your project.