In this article, I will share my experience in automating the entire process of developing a symfony application from scratch, from infrastructure setup to deployment in production. From development to production, the docker-compose will be used to launch the application, and all the continuous integration / deployment procedures will be run via GitLab CI / CD Pipelines in docker containers.
The implication is that you are familiar with docker and docker-compose. If not, or you don’t know how to install it, I prepared instructions on how to prepare the developer’s local environment . In fact, to work on the application, only Docker, VirtualBox and, optionally, Yarn will be required.
I prepared the skeleton application and laid it out on github . Everything written below refers to applications created on the basis of this template and to the infrastructure necessary to run such an application.
To run the application locally, you need to run the following commands:
git clone git@github.com:covex-nn/docker-workflow-symfony.git cd docker-workflow-symfony docker-compose up -d docker-compose exec php phing
The site will be available at http: //docker.local/ , you do not need to add app_dev.php/
to the address. 4 containers will be launched: nginx
, php
, mysql
and phpmyadmin
(the latter is run only in the development environment).
docker.local
needs to be registered in the hosts
. For Linux, the ip-address of the site will be 127.0.0.1
, and under Windows you can find it out as a result of the work of the docker-machine env
(after all, see the instructions ).
composer
in the php
container is configured in such a way that the vendor
folder is inside the container and not on the host, and does not affect the performance in the local developer's environment.
In combat, the system will require three servers: GitLab
— the server for managing the Git and Container Registry repositories, Docker production
— the server for production sites, and Docker
— the server for pre-production and developer test sites.
GitLab and Container Registry installation instructions are available at gitlab.com.
By default, the GitLab Container Registry requires setting SSL certificates. We will use the same certificate for both the Container Registry and the GitLab web interface. You can create an SSL certificate using the LetsEncrypt service.
You can connect an SSL certificate in the /etc/gitlab/gitlab.rb
file. You also need to configure the ability to automatically renew the certificate:
nginx['ssl_certificate'] = "/etc/letsencrypt/live/gitlab.site.ru/fullchain.pem" nginx['ssl_certificate_key'] = "/etc/letsencrypt/live/gitlab.site.ru/privkey.pem" registry_nginx['ssl_certificate'] = "/etc/letsencrypt/live/gitlab.site.ru/fullchain.pem" registry_nginx['ssl_certificate_key'] = "/etc/letsencrypt/live/gitlab.site.ru/privkey.pem" nginx['custom_gitlab_server_config'] = "location ^~ /.well-known { \n allow all;\n alias /var/lib/letsencrypt/.well-known/;\n default_type \"text/plain\";\n try_files $uri =404;\n }\n"
After the change in the gitlab.rb
file, gitlab.rb
need to reload GitLab via gitlab-ctl restart
and configure crontab
to update the certificates:
41 0 * * * /root/certbot-auto renew --no-self-upgrade --webroot -w /var/lib/letsencrypt --renew-hook "service nginx reload"
Docker installation instructions are available on docs.docker.com .
Additionally, you need to create a local network to assign internal IP addresses to the containers:
docker network create graynetwork --gateway 192.168.10.1 --subnet 192.168.10.0/24
In addition to the Docker server, you must install nginx
and certbot-auto
from LetsEncrypt .
Nginx will proxy requests to web servers in Docker containers. Nginx installation instructions are available on the nginx.org website.
Updating future SSL certificates should be configured immediately, as with on the server with GitLab:
41 0 * * * /root/certbot-auto renew --no-self-upgrade --webroot -w /var/lib/letsencrypt --renew-hook "service nginx reload"
You need to complete all the Docker production
installation points Docker production
and additionally install GitLab CI Runner
on the server.
GitLab CI Runner
installation GitLab CI Runner
are available on docs.gitlab.com .
Running GitLab Runner:
gitlab-ci-multi-runner verify --delete printf "concurrent = 10\ncheck_interval = 0\n\n" > /etc/gitlab-runner/config.toml gitlab-ci-multi-runner register -n \ --url https://gitlab-server.ru/ \ --registration-token <token> \ --tag-list "executor-docker,docker-in-docker" \ --executor docker \ --description "docker-dev" \ --docker-image "docker:latest" \ --docker-volumes "/composer/home/cache" \ --docker-volumes "/root/.composer/cache" \ --docker-volumes "/var/run/docker.sock:/var/run/docker.sock"
The <token>
must be copied from the GitLab web interface in the Admin Area --> Runners
section.
Several developers will work on the project, they need to give access so that they do not break anything and do not interfere with each other.
On the Docker production
server Docker production
you need to create a master
user and add it to the docker
group:
adduser master usermod -aG docker master
Next, you need to log in as a new user and create an id_rsa
key without passphrase:
ssh-keygen -t rsa -b 4096 -C "master@docker-server-prod.ru" cat ~/.ssh/id_rsa.pub >> ~/.ssh/authorized_keys
This key will be used for SSH access to the server and for access to the git repository of developers.
master
and add an SSH key to it. This user will be purely technical. In the future, under it will not need to go and perform any operations.On the Docker
server Docker
you need to create a user dev1
Docker
(the name can be any):
adduser dev1 usermod -aG docker dev1
Next, you need to log in as a new user and create an id_rsa key without passphrase:
ssh-keygen -t rsa -b 4096 -C "dev1@docker-server-dev.ru" cat ~/.ssh/id_rsa.pub >> ~/.ssh/authorized_keys chmod 400 ~/.ssh/id_rsa ~/.ssh/id_rsa.pub ~/.ssh/authorized_keys
This key will be used for SSH access to the server, it should not be known to any of the developers.
In GitLab create user dev1
, forbidding him to create their own repositories and groups. You do not need to configure the SSH key - the developer will set it up for himself.
dev1-projects
group and add the master
user with the Master
role to the group. This group will contain all the repositories of this developer.The project will have one main repository and one repository for each developer. The main repository will be the source for production- and staging-sites, the developer repository for the test site of this particular developer. Deploy processes for each of the sites will be the same. The differences will be only in the application configuration and access settings to the server with Docker. Configuration and settings will be stored in GitLab
, in the Settings -- CI/CD Pipelines
section Settings -- CI/CD Pipelines
: mainly the repositories are for production and staging sites, and in the developer repository are for the developer’s test site.
The main project repository can be placed in an arbitrary group.
In the Settings --> Pipelines
section, you need to select git clone
as your Git strategy for pipelines
and add variables:
Variable | Value |
---|---|
COMPOSER_GITHUB_TOKEN | Create a token at https://github.com/settings/tokens |
SSH_PRIVATE_KEY | fill it with the contents of the master user id_rsa file |
NETWORK_NAME_MASTER | graynetwork |
SERVER_NAME_MASTER | site-staging.ru |
NETWORK_IP_MASTER | choose free IP in the graynetwork subnet |
NETWORK_NAME_PRODUCTION | graynetwork |
SERVER_NAME_PRODUCTION | site-production.ru |
NETWORK_IP_PRODUCTION | choose free IP in the graynetwork subnet |
DEPLOY_USER_MASTER | master |
DEPLOY_HOST_MASTER | docker-server-prod.ru |
DEPLOY_DIRECTORY_MASTER | /home/master/site-staging.ru |
DEPLOY_USER_PRODUCTION | master |
DEPLOY_HOST_PRODUCTION | docker-server-prod.ru |
DEPLOY_DIRECTORY_PRODUCTION | /home/master/site-production.ru |
PROJECT_FORKS | <leave empty> |
To deploy the skeleton of an application to staging, you need to upload the master
branch to the repository via git push origin master
.
The developer repository must be in the developer’s project group For the user, dev1
is dev1-projects
. The developer repository is created by creating an administrator from the main repository for Fork . It is important.
In the Settings --> Pipelines
section, you need to select git clone
as your Git strategy for pipelines
, hide the Public pipelines
and add variables:
Variable | Value |
---|---|
COMPOSER_GITHUB_TOKEN | Create a token at https://github.com/settings/tokens |
SSH_PRIVATE_KEY | fill it with the contents of the dev1 user dev1 |
NETWORK_NAME_MASTER | graynetwork |
SERVER_NAME_MASTER | site-dev1.ru |
NETWORK_IP_MASTER | choose free IP in the graynetwork subnet |
DEPLOY_USER_MASTER | dev |
DEPLOY_HOST_MASTER | docker-server-dev.ru |
DEPLOY_DIRECTORY_MASTER | /home/dev1/site-dev1.ru |
PROJECT_FORKS | <leave empty> |
Before deploying to the test site, you need to create a stable
branch that points to the same commit as the master
branch. The stable
branch will correspond to the state of the staging site; only verified and accepted code will be located in this branch.
In the process of work, the developer should, on the one hand, be able to merge commits and rewrite history via git push -f origin master
. On the other hand, it should not be able to displace the stable
branch and create tags in order not to disrupt the rest of the system.
To do this, in the Settings --> Repository
section, you need to unprotect the master
branch and secure the stable
branch and all tags.
To deploy an application to the developer’s test site, you need to run Pipeline for the master
branch. After that, you need to issue the Developer
role to the user dev1
in the Settings --> Members
section.
At the end you need to tune the main repository. You need to add a line with the address of the developer's repository to the PROJECT_FORKS
variable to synchronize the stable
branch in the new repository. And to issue the role Reporter
user dev1
in the main repository.
The last step before starting work is to configure Nginx on servers with Docker. This Nginx will be configured manually, and all HTTP / HTTPS requests to Symfony applications will be proxied to the selected IP address in the internal, previously created, Docker subnet (see the NETWORK_NAME_...
and NETWORK_IP_...
variables).
Sample configuration for the site-dev1.ru
domain. Here 192.168.10.10
is the content of the NETWORK_IP_MASTER
variable from the dev1
developer repository settings.
server { listen 80; # listen 443 ssl; server_name site-dev1.ru; # ssl_certificate /etc/letsencrypt/live/site-dev1.ru/fullchain.pem; # ssl_certificate_key /etc/letsencrypt/live/site-dev1.ru/privkey.pem; # if ($ssl_protocol = "") { # rewrite ^/(.*) https://$server_name/$1 permanent; # } location / { proxy_pass http://192.168.10.10; include proxy_params; } location ~ /.well-known { allow all; alias /var/lib/letsencrypt/.well-known; } }
/root/certbot-auto certonly \ --no-self-upgrade \ --webroot \ -d site-dev1.ru \ -w /var/lib/letsencrypt
To switch the site from HTTP to HTTPS, uncomment the lines in the HTTP domain configuration and reboot Nginx.
nginx -t service nginx reload
At this stage, the developer has access to his own repository. In its repository, it has the role of Developer
and can do almost anything . In the developer's repository, the master
branch corresponds to the state of its test site. Protected- stable
branch - as a staging
site.
Each new task must begin with the creation of a task branch pointing to the same commit as the stable
branch.
git fetch --all --prune git checkout origin/stable git checkout -b feature-qwerty git push origin feature-qwerty
Then, at some stage, when you need to put your changes in to the test site, you can upload the changes to the repository in the master
branch - and the changes will be posted in 2-5 minutes.
The merging of changes from the developer's repository to the main one should occur from the task branch, in the example, this is feature-qwerty
, to the master
branch of the main repository through creating the corresponding Merge Request in the GitLab web interface.
Before accepting a Merge Request, the administrator must make sure that the commits in the developer branch go strictly after the current position of the master
branch of the main repository. This will not happen automatically in GitLab CE, the feature is available only in GitLab EE .
To roll out changes to the working site, you need to create a release-...
tag in the GitLab web interface.
Along with the change in the project code, the developer can add new values ​​to the application parameters. The values ​​of these parameters may vary depending on the environment.
The default configuration is stored in the .env
file in the project root. This file is one for all developers and is part of the repository:
ENV_hwi_facebook_client_id=1234 ENV_hwi_facebook_client_secret=4567
The file is loaded when docker-compose up -d
started, the values ​​get into the container through the environment
block in the php
service description:
services: php: environment: ENV_hwi_facebook_client_id: "${ENV_hwi_facebook_client_id}" ENV_hwi_facebook_client_secret: "${ENV_hwi_facebook_client_secret}"
Inside symfony, these values ​​get through the file app/config/parameters.yml
(it is also part of the application):
parameters: hwi_facebook_client_id: "%env(ENV_hwi_facebook_client_id)%" env(ENV_hwi_facebook_client_id): ~ hwi_facebook_client_secret: "%env(ENV_hwi_facebook_client_secret)" env(ENV_hwi_facebook_client_secret): ~
To implement new papameters, you need to restart docker-compose
:
docker-compose stop docker-compose up -d
Before rolling out the changes to the developer’s test site, the administrator must add the variable values ​​for this site in the Settings --> Pipelines
section. _MASTER
suffix should be added to variable names.
ENV_hwi_facebook_client_id_MASTER ENV_hwi_facebook_client_secret_MASTER
If variables are not created, values ​​for them will be taken from the .env
file.
Before accepting a Merge Request, in the main repository you need to add variables with the _MASTER
suffix, as was done for the developer’s test site.
After accepting the Merge Request and implementing changes to staging
you need to add variables to all other developer repositories.
In the main repository, you need to add variables with the suffix _PRODUCTION
, as was done for staging.
Also for the developer in the development environment, the xdebug
extension is xdebug
, and CSS and Javascript files are managed using the Webpack Encore .
The process of continuous integration / deployment is described in the file .gitlab-ci.yml in the root of the repository, it consists of 4 stages: dependency loading, phpunit testing, building, deployment.
At this stage, an attempt is made to install all the dependencies of the application through the composer
.
deps:php-composer: stage: deps image: covex/php7.1-fpm:1.0 script: - echo '{"github-oauth":{"github.com":"'"$COMPOSER_GITHUB_TOKEN"'"}}' > ./auth.json - composer install --prefer-dist --no-scripts --no-autoloader --no-interaction tags: - executor-docker
The result of this stage will be filling the folder /composer/home/cache
. This folder is saved in the volume
of gitlab-ci-multi-runner
and the composer cache will be available for all subsequent tasks (both in the current pipeline and in subsequent ones).
Before running phpunit
, environment variables are created for the operation of the symfony application. If any values ​​of variables in the testing environment should differ in values ​​in all other environments - you need to create such variables in the settings of the GitLab repository with the _TEST
suffix (for example, ENV_hwi_facebook_client_id_TEST
). Then its value will .env
default from the .env
file.
.template-suffix-vars: &suffix-vars before_script: - cat .env | grep ENV_ > .build-env - sed -i 's/^/export /' .build-env - for name in `env | awk -F= '{if($1 ~ /'"$ENV_SUFFIX"'$/) print $1}'`; do echo 'export '`echo $name|awk -F''"$ENV_SUFFIX"'$' '{print $1}'`'='`printenv $name`'' >> .build-env; done test:phpunit: stage: test image: covex/php7.1-fpm:1.0 <<: *suffix-vars variables: ENV_SUFFIX: "_TEST" script: - eval $(cat .build-env) - echo '{"github-oauth":{"github.com":"'"$COMPOSER_GITHUB_TOKEN"'"}}' > ./auth.json - composer require phpunit/phpunit:* --dev - phpunit dependencies: [] tags: - executor-docker
Here the build for the php project is the creation of docker images for nginx and php containers, and putting the prepared images in GitLab Container Registry.
.template-docker-nginx-image: &docker-nginx-image stage: build image: docker:latest <<: *suffix-vars script: - eval $(cat .build-env) - docker build --tag $CI_NGINX_IMAGE_WITH_TAG --build-arg server_name=$SERVER_NAME --build-arg server_upstream=prod --build-arg app_php=app ./docker/nginx - docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY - docker push $CI_NGINX_IMAGE_WITH_TAG - docker logout $CI_REGISTRY tags: - executor-docker - docker-in-docker .template-docker-app-image: &docker-app-image stage: build image: docker:latest <<: *suffix-vars script: - eval $(cat .build-env) - echo '{"github-oauth":{"github.com":"'"$COMPOSER_GITHUB_TOKEN"'"}}' > ./auth.json - docker build --tag $CI_APP_IMAGE_WITH_TAG . - docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY - docker push $CI_APP_IMAGE_WITH_TAG - docker logout $CI_REGISTRY dependencies: - deps:php-composer tags: - executor-docker - docker-in-docker .template-docker-compose: &docker-compose stage: build image: covex/docker-compose:1.0 <<: *suffix-vars script: - eval $(cat .build-env) - mkdir build - docker-compose -f docker-compose-deploy.yml config > build/docker-compose.yml - sed -i 's/\/builds\/'"$CI_PROJECT_NAMESPACE"'\/'"$CI_PROJECT_NAME"'/\./g' build/docker-compose.yml artifacts: untracked: true name: "$CI_COMMIT_REF_NAME" paths: - build/ tags: - executor-docker dependencies: [] build:docker-nginx-image-master: <<: *docker-nginx-image variables: ENV_SUFFIX: "_MASTER" only: - master except: - tags build:docker-nginx-image-production: <<: *docker-nginx-image variables: ENV_SUFFIX: "_PRODUCTION" only: - /^release-.*$/ except: - branches build:docker-app-image-master: <<: *docker-app-image variables: ENV_SUFFIX: "_MASTER" only: - master except: - tags build:docker-app-image-production: <<: *docker-app-image variables: ENV_SUFFIX: "_PRODUCTION" only: - /^release-.*$/ except: - branches build:docker-compose-master: <<: *docker-compose variables: ENV_SUFFIX: "_MASTER" only: - master except: - tags build:docker-compose-production: <<: *docker-compose variables: ENV_SUFFIX: "_PRODUCTION" only: - /^release-.*$/ except: - branches
Here, the build:docker-app-image-master
task build:docker-app-image-master
creates PHP application images for the staging site (and for the developer’s test site); and the build:docker-app-image-production
task build:docker-app-image-production
is for a production site. For each task, the values ​​of variables from the pipeline settings with the suffix _MASTER
or _PRODUCTION
override the default values ​​from the .env
file. The tasks for building nginx
images are described in a similar way (see the build:docker-nginx-image-master
tasks build:docker-nginx-image-master
and build:docker-nginx-image-production
).
Also at this stage, the docker-compose.yml
file is created, which in the next stage will be copied to the remote server (see build:docker-compose-master
and build:docker-compose-production
tasks). The generated docker-compose.yml
contains all the environment variables needed to run the application. In the services
section all containers will be created only on the basis of ready docker images.
networks: nw_external: external: name: graynetwork nw_internal: {} services: mysql: environment: MYSQL_DATABASE: project MYSQL_PASSWORD: project MYSQL_ROOT_PASSWORD: root MYSQL_USER: project expose: - '3306' image: covex/mysql:5.7 networks: nw_internal: null restart: always volumes: - database:/var/lib/mysql:rw nginx: depends_on: mysql: condition: service_healthy image: gitlab.site.ru:5005/dev1-projects/symfony-workflow2/nginx:master networks: nw_external: ipv4_address: 192.168.10.13 nw_internal: null ports: - 80/tcp restart: always volumes: - assets:/srv/a:ro - assets:/srv/b:ro - assets:/srv/storage:ro php: environment: ENV_database_host: mysql ENV_database_mysql_version: '5.7' ENV_database_name: project ENV_database_password: project ENV_database_port: '3306' ENV_database_user: project ENV_mailer_from: andrey@mindubaev.ru ENV_mailer_host: 127.0.0.1 ENV_mailer_password: 'null' ENV_mailer_transport: smtp ENV_mailer_user: 'null' ENV_secret: ThisTokenIsNotSoSecretChangeIt image: gitlab.site.ru:5005/dev1-projects/symfony-workflow2:master networks: nw_internal: null restart: always volumes: - assets:/srv/a:rw - assets:/srv/b:rw - assets:/srv/storage:rw spare: environment: ENV_database_host: mysql ENV_database_mysql_version: '5.7' ENV_database_name: project ENV_database_password: project ENV_database_port: '3306' ENV_database_user: project ENV_mailer_from: andrey@mindubaev.ru ENV_mailer_host: 127.0.0.1 ENV_mailer_password: 'null' ENV_mailer_transport: smtp ENV_mailer_user: 'null' ENV_secret: ThisTokenIsNotSoSecretChangeIt image: gitlab.site.ru:5005/dev1-projects/symfony-workflow2:master networks: nw_internal: null restart: always volumes: - assets:/srv/a:rw - assets:/srv/b:rw - assets:/srv/storage:rw version: '2.1' volumes: assets: {} database: {}
At this stage, the docker images of the application are ready and uploaded to the Container Registry. It remains to update the application.
There is no phpmyadmin
server on remote servers; in addition to the php
service, absolutely the same spare
service has been added; and in the nginx
configuration, instead of one server, two are upstream
. The use of two identical services allowed achieving almost zero deployment downtime .
.template-secure-copy: &secure-copy stage: deploy image: covex/alpine-git:1.0 before_script: - eval $(ssh-agent -s) - ssh-add <(echo "$SSH_PRIVATE_KEY") script: - eval $(cat .build-env) - ssh -p 22 $DEPLOY_USER@$DEPLOY_HOST 'set -e ; rm -rf '"$DEPLOY_DIRECTORY"'_tmp ; mkdir -p '"$DEPLOY_DIRECTORY"'_tmp' - scp -P 22 -r build/* ''"$DEPLOY_USER"'@'"$DEPLOY_HOST"':'"$DEPLOY_DIRECTORY"'_tmp' - ssh -p 22 $DEPLOY_USER@$DEPLOY_HOST 'set -e ; if [ -d '"$DEPLOY_DIRECTORY"' ]; then rm -rf '"$DEPLOY_DIRECTORY"'; fi ; mv '"$DEPLOY_DIRECTORY"'_tmp '"$DEPLOY_DIRECTORY"' ; cd '"$DEPLOY_DIRECTORY"' ; docker login -u gitlab-ci-token -p '"$CI_JOB_TOKEN"' '"$CI_REGISTRY"' ; docker-compose pull ; docker-compose up -d --no-recreate ; docker-compose up -d --force-recreate --no-deps spare ; docker-compose exec -T spare sh -c "cd /srv && rm -rf b/* && cp -a web/. b/ && rm -rf a/* && cp -a web/. a/" ; docker-compose exec -T spare phing storage-prepare database-deploy ; docker-compose up -d --force-recreate --no-deps php' - ssh -p 22 $DEPLOY_USER@$DEPLOY_HOST 'set -e ; cd '"$DEPLOY_DIRECTORY"' ; echo "[$(date -R)] web-server is down" ; docker-compose stop nginx ; docker-compose up -d nginx ; echo "[$(date -R)] web-server is up"' tags: - executor-docker deploy:secure-copy-master: <<: *secure-copy only: - master except: - tags environment: name: staging dependencies: - build:docker-compose-master deploy:secure-copy-production: <<: *secure-copy only: - /^release-.*$/ except: - branches environment: name: production dependencies: - build:docker-compose-production
The deployment algorithm is as follows:
docker-compose.yml
file generated during the build
docker-compose.yml
spare
containernginx
, we make database migrationphp
nginx
and mysql
(in combat conditions - this is not necessary)During the update of the spare
or php
, nginx
containers after a few seconds the inaccessibility of one of them switches to the next available in upstream
. Those. The application works correctly for 100% HTTP requests, but sometimes with a delay.
HTTP- php
, , — spare
, . Those. . , , .
nginx
mysql
, . , "". 5 , 80-90% deployment downtime.
GitLab Continuous Integration & Deployment
docker-compose
— . - vagrant
. , , , , composer.json
. Development- — , production, Linux + Apache + PHP + MySQL. , , .
— docker swarm
, kubernetes
, . , .
Source: https://habr.com/ru/post/335564/