Rails + Docker + Gitlab = ❤
Publié il y a presque 3 ans~4 min
A web projects lifecycle doesn't end with its development! It still needs to be published: it's the deployment phase. However, this necessary step very often involves a number of complications... Fortunately, we now have a solid tool: the CI/CD is the strongest example, such as the one from Gitlab.
Based on a Ruby On Rails dockerized project, we are going to set up the different elements necessary for our CI/CD: Compose files specific to each environment, and of course a
.gitlab-ci.yml
file, to orchestrate everything!
Prerequisites
- Docker and Docker Compose
- A dockerized Ruby On Rails project (cf. this article)
- A Gitlab account 😲
- A web server (DigitalOcean, ...) including Docker
A demonstration project has been created for this article, so do not hesitate to refer to it for more details (scripts, ...)!
Docker Compose(s)
Starting with a simple docker-compose.yml
file, we will decline for all environments (development, test and production). Here's an example, taken from the demonstration project:
1version: '3.0'2services:3 db:4 image: postgres:11-alpine5 ports:6 - 5433:54327 environment:8 POSTGRES_PASSWORD: postgres910 webpacker:11 image: railsondocker_development12 command: ["./docker/start_webpack_dev.sh"]13 environment:14 - NODE_ENV=development15 - RAILS_ENV=development16 - WEBPACKER_DEV_SERVER_HOST=0.0.0.017 volumes:18 - .:/railsondocker:cached19 ports:20 - 3035:30352122 app:23 image: railsondocker_development24 build:25 context: .26 args:27 - PRECOMPILEASSETS=NO28 environment:29 - RAILS_ENV=development30 links:31 - db32 - webpacker33 ports:34 - 3000:300035 volumes:36 - .:/railsondocker:cached
Here it will globally be a question of adjusting the variables (RAILS_ENV
, ...), so nothing very complicated.
Gitlab configuration
The starting point of the Gitlab CI/CD is a .gitlab-ci.yml
file (placed in the project root), which will orchestrate the different scripts via a Runner (a task executor made available for free by Gitlab, or customized as needed).
Here we'll define three stages: test, release and deploy. Each step may be specific to an environment or a branch, and above all may condition the execution of the next one. Thanks to this there will be no more (or almost!) "broken" production! 👍
Repository settings
First of all, in order to deploy our project on an external server, we must allow the Gitlab CI/CD to work on it. Here is the procedure, laborious but ultimately quite simple:
- From the server, generate an SSH key (via the
ssh-keygen -t rsa
command). Make sure that the public key is authorized for this server, and saved in theathorized_keys
folder (or equivalent). - In the Gitlab repository, declare a
PRODUCTION_SERVER_PRIVATE_KEY
environment variable corresponding to the private key (in Settings > Integration and Continuous Delivery) - Still in the environment variables section of the Gitlab repository, declare a
PRODUCTION_SERVER_IP
variable, with the IP address of the concerned server.
That's it, the server and your Gitlab repository are now connected! 🥂 Note that the operation will have to be repeated for each additional environment (staging, ...).
The Gitlab file
The bases of a .gitlab-ci.yml
file are the stages defining the sequences of scripts, the variables (which can be environment variables from the repository) and of course, the scripts themselves. Again, the file structure is pretty clear:
1image: docker2services:3 - docker:dind45cache:6 paths:7 - node_modules89variables:10 DOCKER_HOST: tcp://docker:2375/11 DOCKER_DRIVER: overlay212 CONTAINER_STABLE_IMAGE: $CI_REGISTRY_IMAGE:stable1314stages:15 - test16 - release17 - deploy1819before_script:20 - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY21 - apk add --no-cache py-pip python-dev libffi-dev openssl-dev gcc libc-dev make22 - pip install docker-compose23 - docker-compose --version2425# ...
Our file starts here with the reference image declaration (Docker here). A few settings (cache, variables, addition of dependencies, ...) are added directly from the official documentation. We are now ready to start configuring our CI/CD!
Test
For the test step we declare a simple script, relying on a specific Compose file. Concretely, it's all about running the tests (Rspec, ...) then purging the Docker images, containers and volumes resulting from this step:
1test:2 stage: test3 script:4 - docker-compose -f docker-compose.test.yml build --pull5 - docker-compose -f docker-compose.test.yml run --rm app sh -c "./docker/wait_for_services.sh && bundle exec rake db:create spec"6 after_script:7 - docker-compose -f docker-compose.test.yml run --rm app rm -rf tmp/8 - docker-compose -f docker-compose.test.yml down9 - docker volume rm `docker volume ls -qf dangling=true`
Relase
This is where the most important thing happens: we build the Docker container for our application, before pushing it to the server (if all goes well).
1release_stable:2 stage: release3 only:4 - production5 script:6 - docker-compose -f docker-compose.production.yml build --pull7 - docker tag railsondocker_production $CONTAINER_STABLE_IMAGE8 - docker push $CONTAINER_STABLE_IMAGE
One line here is particularly important: docker push $CONTAINER_STABLE_IMAGE
. After building the container, we save it in a container registry linked to our Gitlab repository. We will then only have to use the container of this registry to deploy our project on the server.
At this stage, only the application container interests us. For this reason there is no database container in the docker-compose.production.yml
file. This is to avoid overwriting the database with each deployment! 😉
Deploy
The Docker container of our application is now available from the the Gitlab repository's registry. To deploy it, we will simply copy a Compose file specific to the deployment on the server (by authenticating us using the SSH key created previously). This will orchestrate the assembly of the different containers (application from the registry, database), and will be executed via the following script:
1deploy_stable:2 stage: deploy3 only:4 - production5 environment: production6 before_script:7 - mkdir -p ~/.ssh8 - echo "$PRODUCTION_SERVER_PRIVATE_KEY" | tr -d '\r' > ~/.ssh/id_rsa9 - chmod 600 ~/.ssh/id_rsa10 - which ssh-agent || (apk add openssh-client)11 - eval $(ssh-agent -s)12 - ssh-add ~/.ssh/id_rsa13 - ssh-keyscan -H $PRODUCTION_SERVER_IP >> ~/.ssh/known_hosts14 script:15 - scp -rp ./docker-deploy.production.yml root@${PRODUCTION_SERVER_IP}:~/16 - ssh root@$PRODUCTION_SERVER_IP "docker login -u ${CI_REGISTRY_USER} -p ${CI_REGISTRY_PASSWORD} ${CI_REGISTRY};17 docker pull $CONTAINER_STABLE_IMAGE;18 docker-compose -f docker-deploy.production.yml stop;19 docker-compose -f docker-deploy.production.yml rm app --force;20 docker-compose -f docker-deploy.production.yml up -d"
🎉
Et voilà ! As often the configuration is the most delicate part for this type of project, but the game is worth the candle: once this is done we get a project with a perfectly functional CI/CD, which we can develop in according to our needs (addition of a staging environment, ...). 🙂