{"id":69351,"date":"2020-02-18T14:44:06","date_gmt":"2020-02-18T11:44:06","guid":{"rendered":"https:\/\/prohoster.info\/blog\/blog_prohoster\/sozdanie-ci-cd-czepochki-i-avtomatizacziya-raboty-s-docker"},"modified":"2020-03-03T16:15:00","modified_gmt":"2020-03-03T13:15:00","slug":"sozdanie-ci-cd-czepochki-i-avtomatizacziya-raboty-s-docker","status":"publish","type":"post","link":"https:\/\/prohoster.info\/en\/blog\/administrirovanie\/sozdanie-ci-cd-czepochki-i-avtomatizacziya-raboty-s-docker","title":{"rendered":"Creating a CI\/CD chain and automating work with Docker","gt_translate_keys":[{"key":"rendered","format":"text"}]},"content":{"rendered":"<p>I wrote my first websites in the late '90s. Back then, getting them up and running was very simple. There was an Apache server on some shared hosting, and you could log into this server via FTP by typing something like <code>ftp:\/\/ftp.example.com<\/code>. Then you had to enter a username and password and upload files to the server. Those were different times; everything was easier back then than it is now.<\/p>\n<p><noindex><a rel=\"nofollow\" href=\"https:\/\/habr.com\/ru\/company\/ruvds\/blog\/488668\/\"><img decoding=\"async\" alt=\"Creating a CI\/CD chain and automating work with Docker\" src=\"\/wp-content\/uploads\/2020\/02\/0861cdd82770ef4695ffac7a09777c52.jpg\" style=\"display:block;margin: 0 auto;\" \/><\/a><\/noindex><\/p>\n<p>Over the past two decades, everything has changed quite a bit. Websites have become more complex; they need to be built before being released into production. A single server has transformed into multiple servers working behind load balancers, and using version control systems has become commonplace.<\/p>\n<p>For my personal project, I had a special configuration. I knew that I needed the ability to deploy the site to production with just one action: pushing code to the branch <code>master<\/code> on GitHub. Moreover, I also knew that to keep my small web application running, I didn't want to manage a huge Kubernetes cluster, use Docker Swarm technology, or maintain a fleet of servers with pods, agents, and various other complexities. To achieve my goal of maximizing simplicity, I needed to get acquainted with CI\/CD.<\/p>\n<p>If you have a small project (in our case, a Node.js project) and you would like to know how to automate the deployment of this project, ensuring that what is stored in the repository exactly matches what is running in production, then this article may interest you.<br \/>\n<noindex><a rel=\"nofollow\" name=\"habracut\"><\/a><\/noindex><\/p>\n<h2>Prerequisites<\/h2>\n<p>\nIt is expected that the reader of this article has basic knowledge of working with the command line and writing Bash scripts. Additionally, you will need accounts for <noindex><a rel=\"nofollow\" href=\"https:\/\/travis-ci.org\/\">Travis CI<\/a><\/noindex> and <noindex><a rel=\"nofollow\" href=\"https:\/\/hub.docker.com\/\">Docker Hub<\/a><\/noindex>.<\/p>\n<h2>Objectives<\/h2>\n<p>\nI won't say that this article can be unequivocally called a 'tutorial.' It is more of a document where I share what I have learned and describe the process for testing and deploying code in production that I find satisfactory, executed in a single automated pass.<\/p>\n<p>This is what my workflow ultimately looks like.<\/p>\n<p>For code pushed to any branch of the repository except <code>master<\/code>, the following actions are carried out:<\/p>\n<ul>\n<li>The project build is triggered on Travis CI.<\/li>\n<li>All unit, integration, and end-to-end tests are executed.<\/li>\n<\/ul>\n<p>\nOnly for the code that falls into <code>master<\/code>, the following is executed:<\/p>\n<ul>\n<li>Everything mentioned above, plus\u2026<\/li>\n<li>Building a Docker image based on the current code, settings, and environment.<\/li>\n<li>Placing the image on Docker Hub.<\/li>\n<li>Connecting to the production server.<\/li>\n<li>Pulling the image from Docker Hub to the server.<\/li>\n<li>Stopping the current container and starting a new one based on the new image.<\/li>\n<\/ul>\n<p>\nIf you know nothing about Docker, images, and containers\u2014don't worry. I will explain all of this to you.<\/p>\n<h2>What is CI\/CD?<\/h2>\n<p>\nThe abbreviation CI\/CD stands for \"continuous integration\/continuous deployment\".<\/p>\n<h3>\u258dContinuous Integration<\/h3>\n<p>\nContinuous integration is the process in which developers commit to the main code repository of the project (usually to the branch <code>master<\/code>). The quality of the code is ensured through automated testing.<\/p>\n<h3>\u258dContinuous Deployment<\/h3>\n<p>\nContinuous deployment is the frequent automated deployment of code to production. The second part of the CI\/CD abbreviation is sometimes revealed as \"continuous delivery.\" This is generally the same as \"continuous deployment,\" but \"continuous delivery\" implies the need for manual approval of changes before initiating the project's deployment process.<\/p>\n<h2>Getting Started<\/h2>\n<p>\nThe application where I learned all of this is called <noindex><a rel=\"nofollow\" href=\"https:\/\/github.com\/taniarascia\/takenote\">TakeNote<\/a><\/noindex>. This is a web project I am working on, designed for taking notes. Initially, I tried to make a <noindex><a rel=\"nofollow\" href=\"https:\/\/jamstack.org\/\">JAMStack<\/a><\/noindex>-project, or just a frontend application without a server, to take advantage of the standard hosting and deployment features offered by <noindex><a rel=\"nofollow\" href=\"https:\/\/www.netlify.com\/\">Netlify<\/a><\/noindex>. As the complexity of the application grew, I needed to create its backend portion, which meant I needed to formulate my own strategy for automated integration and automated deployment of the project.<\/p>\n<p>In my case, the application consists of an Express server running in a Node.js environment, serving a single-page React application and supporting a secure server-side API. This architecture follows a strategy found in <noindex><a rel=\"nofollow\" href=\"https:\/\/www.taniarascia.com\/full-stack-cookies-localstorage-react-express\/\">this<\/a><\/noindex> full-stack authentication guide.<\/p>\n<p>I consulted with <noindex><a rel=\"nofollow\" href=\"http:\/\/qualitytesting.tech\/\">a friend<\/a><\/noindex>, who is an automation expert, and asked him what I needed to do for everything to work as I needed. He suggested an idea of how the automated workflow should look, as outlined in the Goals section of this article. Setting such goals for myself meant I needed to figure out how to use Docker.<\/p>\n<h2>Docker<\/h2>\n<p>\nDocker is a tool that, thanks to containerization technology, allows for easy distribution of applications, as well as their deployment and execution in the same environment, even if Docker itself is running in various environments. To start, I needed to get the Docker command-line tools (CLI). <noindex><a rel=\"nofollow\" href=\"https:\/\/docs.docker.com\/install\/\">Instructions<\/a><\/noindex> for installing Docker cannot be called very clear and understandable, but from it, you can learn that to take the first step in installation, you need to download Docker Desktop (for Mac or Windows).<\/p>\n<p>Docker Hub is roughly the same as <noindex><a rel=\"nofollow\" href=\"https:\/\/github.com\/\">GitHub<\/a><\/noindex> for git repositories, or a registry <noindex><a rel=\"nofollow\" href=\"https:\/\/www.npmjs.com\/\">npm<\/a><\/noindex> for JavaScript packages. It is an online repository for Docker images. This is where Docker Desktop connects.<\/p>\n<p>So, to start working with Docker, you need to do two things:<\/p>\n<ul>\n<li>Install <noindex><a rel=\"nofollow\" href=\"https:\/\/www.docker.com\/get-started\">Docker Desktop<\/a><\/noindex>.<\/li>\n<li>Sign up at <noindex><a rel=\"nofollow\" href=\"https:\/\/hub.docker.com\/\">Docker Hub<\/a><\/noindex>.<\/li>\n<\/ul>\n<p>\nAfter that, you can check if Docker CLI is working by running the following command to check the Docker version:<\/p>\n<pre><code class=\"plaintext\">docker -v<\/code><\/pre>\n<p>\nNext, log in to Docker Hub by entering your username and password when prompted:<\/p>\n<pre><code class=\"plaintext\">docker login<\/code><\/pre>\n<p>\nTo use Docker, you must understand the concepts of images and containers.<\/p>\n<h3>\u258dImages<\/h3>\n<p>\nAn image is somewhat like a blueprint containing instructions for building a container. It is an immutable snapshot of the file system and application settings. Developers can easily share images.<\/p>\n<pre><code class=\"plaintext\"># \u0412\u044b\u0432\u043e\u0434 \u0441\u0432\u0435\u0434\u0435\u043d\u0438\u0439 \u043e\u0431\u043e \u0432\u0441\u0435\u0445 \u043e\u0431\u0440\u0430\u0437\u0430\u0445\ndocker images<\/code><\/pre>\n<p>\nThis command will output a table with the following header:<\/p>\n<pre><code class=\"plaintext\">REPOSITORY \u00a0 \u00a0 TAG     IMAGE ID \u00a0 \u00a0 CREATED     SIZE\n---<\/code><\/pre>\n<p>\nNext, we will consider some command examples in the same format \u2014 the command comes first with a comment, followed by an example of what it might output.<\/p>\n<h3>\u258dContainers<\/h3>\n<p>\nA container is an executable package that includes everything needed to run an application. With this approach, the application will always run consistently, regardless of the infrastructure: in an isolated environment and in the same environment. This means that instances of the same image are launched in different environments.<\/p>\n<pre><code class=\"plaintext\"># \u041f\u0435\u0440\u0435\u0447\u0438\u0441\u043b\u0435\u043d\u0438\u0435 \u0432\u0441\u0435\u0445 \u043a\u043e\u043d\u0442\u0435\u0439\u043d\u0435\u0440\u043e\u0432\ndocker ps -a\nCONTAINER ID \u00a0 \u00a0 IMAGE     COMMAND \u00a0 \u00a0 CREATED     STATUS     PORTS \u00a0 \u00a0 NAMES\n---<\/code><\/pre>\n<p><\/p>\n<h3>\u258dTags<\/h3>\n<p>\nA tag indicates a specific version of an image.<\/p>\n<h3>\u258dBrief Overview of Docker Commands<\/h3>\n<p>\nHere is an overview of some commonly used Docker commands.<\/p>\n<p>The command<\/p>\n<p>Context<\/p>\n<p>Action<\/p>\n<p><noindex><a rel=\"nofollow\" href=\"https:\/\/docs.docker.com\/engine\/reference\/commandline\/build\/\">docker build<\/a><\/noindex><\/p>\n<p>Type<\/p>\n<p>Building an image from a Dockerfile<\/p>\n<p><noindex><a rel=\"nofollow\" href=\"https:\/\/docs.docker.com\/engine\/reference\/commandline\/tag\/\">docker tag<\/a><\/noindex><\/p>\n<p>Type<\/p>\n<p>Tagging an image<\/p>\n<p><noindex><a rel=\"nofollow\" href=\"https:\/\/docs.docker.com\/engine\/reference\/commandline\/images\/\">docker images<\/a><\/noindex><\/p>\n<p>Type<\/p>\n<p>Listing images<\/p>\n<p><noindex><a rel=\"nofollow\" href=\"https:\/\/docs.docker.com\/engine\/reference\/run\/\">docker run<\/a><\/noindex><\/p>\n<p>Container<\/p>\n<p>Running a container based on an image<\/p>\n<p><noindex><a rel=\"nofollow\" href=\"https:\/\/docs.docker.com\/engine\/reference\/commandline\/push\/\">docker push<\/a><\/noindex><\/p>\n<p>Type<\/p>\n<p>Pushing an image to a registry<\/p>\n<p><noindex><a rel=\"nofollow\" href=\"http:\/\/about:blank\">docker pull<\/a><\/noindex><\/p>\n<p>Type<\/p>\n<p>Pulling an image from a registry<\/p>\n<p><noindex><a rel=\"nofollow\" href=\"https:\/\/docs.docker.com\/engine\/reference\/commandline\/ps\/\">docker ps<\/a><\/noindex><\/p>\n<p>Container<\/p>\n<p>Listing containers<\/p>\n<p><noindex><a rel=\"nofollow\" href=\"https:\/\/docs.docker.com\/engine\/reference\/commandline\/system_prune\/\">docker system prune<\/a><\/noindex><\/p>\n<p>Image\/Container<\/p>\n<p>Removing unused containers and images<\/p>\n<h3>\u258dDockerfile<\/h3>\n<p>\nI know how to locally run an application for production. I have a Webpack configuration designed to build a ready-to-use React application. Next, I have a command that runs a server based on Node.js on port <code>5000<\/code>. It looks like this:<\/p>\n<pre><code class=\"plaintext\">npm i          # installing dependencies\nnpm run build # building the React application\nnpm run start # starting the Node server<\/code><\/pre>\n<p>\nIt is worth noting that I do not have a sample application for this material. However, for experimentation, any simple Node application will suffice.<\/p>\n<p>To make use of the container, you will need to give Docker instructions. This is done through a file called <code>Dockerfile<\/code>, located in the root directory of the project. Initially, this file may seem quite obscure.<\/p>\n<p>But what it contains simply describes, with specific commands, something akin to setting up a working environment. Here are some of these commands:<\/p>\n<ul>\n<li><noindex><a rel=\"nofollow\" href=\"https:\/\/docs.docker.com\/engine\/reference\/builder\/#from\">FROM<\/a><\/noindex> \u2014 This command initiates the file. It specifies the base image upon which the container is built.<\/li>\n<li><noindex><a rel=\"nofollow\" href=\"https:\/\/docs.docker.com\/engine\/reference\/builder\/#copy\">COPY<\/a><\/noindex> \u2014 Copying files from a local source into the container.<\/li>\n<li><noindex><a rel=\"nofollow\" href=\"https:\/\/docs.docker.com\/engine\/reference\/builder\/#workdir\">WORKDIR<\/a><\/noindex> \u2014 Setting the working directory for the following commands.<\/li>\n<li><noindex><a rel=\"nofollow\" href=\"https:\/\/docs.docker.com\/engine\/reference\/builder\/#run\">RUN<\/a><\/noindex> \u2014 Running commands.<\/li>\n<li><noindex><a rel=\"nofollow\" href=\"https:\/\/docs.docker.com\/engine\/reference\/builder\/#expose\">EXPOSE<\/a><\/noindex> \u2014 Setting up the port.<\/li>\n<li><noindex><a rel=\"nofollow\" href=\"https:\/\/docs.docker.com\/engine\/reference\/builder\/#entrypoint\">ENTRYPOINT<\/a><\/noindex> \u2014 Specifying the command to execute.<\/li>\n<\/ul>\n<p>\n<code>Dockerfile<\/code> might look something like this:<\/p>\n<pre><code class=\"plaintext\"># \u0417\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044c \u0431\u0430\u0437\u043e\u0432\u044b\u0439 \u043e\u0431\u0440\u0430\u0437\nFROM node:12-alpine\n\n# \u0421\u043a\u043e\u043f\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0444\u0430\u0439\u043b\u044b \u0438\u0437 \u0442\u0435\u043a\u0443\u0449\u0435\u0439 \u0434\u0438\u0440\u0435\u043a\u0442\u043e\u0440\u0438\u0438 \u0432 \u0434\u0438\u0440\u0435\u043a\u0442\u043e\u0440\u0438\u044e app\/\nCOPY . app\/\n\n# \u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c app\/ \u0432 \u0440\u043e\u043b\u0438 \u0440\u0430\u0431\u043e\u0447\u0435\u0439 \u0434\u0438\u0440\u0435\u043a\u0442\u043e\u0440\u0438\u0438\nWORKDIR app\/\n\n# \u0423\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c \u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0441\u0442\u0438 (\u043a\u043e\u043c\u0430\u043d\u0434\u0430 npm ci \u043f\u043e\u0445\u043e\u0436\u0430 npm i, \u043d\u043e \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u0434\u043b\u044f \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0437\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0445 \u0441\u0431\u043e\u0440\u043e\u043a)\nRUN npm ci --only-production\n\n# \u0421\u043e\u0431\u0440\u0430\u0442\u044c \u043a\u043b\u0438\u0435\u043d\u0442\u0441\u043a\u043e\u0435 React-\u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u0434\u043b\u044f \u043f\u0440\u043e\u0434\u0430\u043a\u0448\u043d\u0430\nRUN npm run build\n\n# \u041f\u0440\u043e\u0441\u043b\u0443\u0448\u0438\u0432\u0430\u0442\u044c \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u044b\u0439 \u043f\u043e\u0440\u0442\nEXPOSE 5000\n\n# \u0417\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u044c Node-\u0441\u0435\u0440\u0432\u0435\u0440\nENTRYPOINT npm run start<\/code><\/pre>\n<p>\nDepending on the selected base image, you may need to install additional dependencies. The fact is that some base images (like Node Alpine Linux) are created to be as compact as possible. As a result, they may lack certain programs that you rely on.<\/p>\n<h3>\u258dBuilding, tagging, and running the container<\/h3>\n<p>\nLocal build and run of the container \u2014 this is, after we have <code>Dockerfile<\/code>, fairly straightforward tasks. Before pushing the image to Docker Hub, it needs to be tested locally.<\/p>\n<h3>\u258dBuild<\/h3>\n<p>\nFirst, you need to build <noindex><a rel=\"nofollow\" href=\"https:\/\/docs.docker.com\/engine\/reference\/commandline\/build\/\">the image<\/a><\/noindex>, specifying a name, and optionally a tag (if no tag is specified, the system will automatically assign a tag to the image <code>latest<\/code>).<\/p>\n<pre><code class=\"plaintext\"># \u0421\u0431\u043e\u0440\u043a\u0430 \u043e\u0431\u0440\u0430\u0437\u0430\ndocker build -t &lt;image&gt;:&lt;tag&gt; .<\/code><\/pre>\n<p>\nAfter executing this command, you can see how Docker builds the image.<\/p>\n<pre><code class=\"plaintext\">Sending build context to Docker daemon \u00a0 2.88MB\nStep 1\/9 : FROM node:12-alpine\n\u00a0---&gt; ...running build steps...\nSuccessfully built 123456789123\nSuccessfully tagged :<\/code><\/pre>\n<p>\nBuilding may take a few minutes \u2014 it all depends on how many dependencies you have. Once the build is complete, you can execute the command <code>docker images<\/code> and take a look at the description of your new image.<\/p>\n<pre><code class=\"plaintext\">REPOSITORY\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 TAG               IMAGE ID\u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 CREATED              SIZE\n \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 latest            123456789123\u00a0 \u00a0 \u00a0 \u00a0 About a minute ago   x.xxGB<\/code><\/pre>\n<p><\/p>\n<h3>\u258dRunning<\/h3>\n<p>\nThe image is created. This means that you can run a container based on it. Since I want to be able to access the application running in the container at <code>localhost:5000<\/code>, I specified in the left part of the pair <code>5000:5000<\/code> in the next command. <code>5000<\/code>On the right side is the container port.<\/p>\n<pre><code class=\"plaintext\"># \u0417\u0430\u043f\u0443\u0441\u043a \u0441 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435\u043c \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u043e\u0433\u043e \u043f\u043e\u0440\u0442\u0430 5000 \u0438 \u043f\u043e\u0440\u0442\u0430 \u043a\u043e\u043d\u0442\u0435\u0439\u043d\u0435\u0440\u0430 5000\ndocker run -p 5000:5000 &lt;image&gt;:&lt;tag&gt;<\/code><\/pre>\n<p>\nNow that the container is created and running, you can use the command <code>docker ps<\/code> to view the details of this container (or you can use the command <code>docker ps -a<\/code>, which shows details about all containers, not just the running ones).<\/p>\n<pre><code class=\"plaintext\">CONTAINER ID        IMAGE               COMMAND                  CREATED              STATUS                   PORTS                    NAMES\n987654321234                     \"\/bin\/sh -c 'npm run\u2026\"   6 seconds ago        Up 6 seconds             0.0.0.0:5000-&gt;5000\/tcp   stoic_darwin<\/code><\/pre>\n<p>\nIf you navigate to <code>localhost:5000<\/code> \u2014 you can see the page of the running application, which looks exactly like the page of the application running in the production environment.<\/p>\n<h3>\u258dAssigning a tag and publishing<\/h3>\n<p>\nTo use one of the created images on a production server, we need to be able to upload this image to Docker Hub. This means that we first need to create a repository for the project on Docker Hub. After this, we will have a location to which we can push the image. The image should be renamed so that its name starts with our username on Docker Hub, followed by the repository name. At the end of the name, any tag may be placed. Below is an example of how to name images according to this scheme.<\/p>\n<p>Now we can build the image, giving it a new name and executing the command <code>docker push<\/code> to push it to the Docker Hub repository.<\/p>\n<pre><code class=\"plaintext\">docker build -t \/: .\ndocker tag \/: \/:latest\ndocker push \/:\n\n# In practice, this might look like:\ndocker build -t user\/app:v1.0.0 .\ndocker tag user\/app:v1.0.0 user\/app:latest\ndocker push user\/app:v1.0.0<\/code><\/pre>\n<p>\nIf everything goes as planned, the image will be available on Docker Hub, and it will be easy to download it to the server or share it with other developers.<\/p>\n<h2>Next Steps<\/h2>\n<p>\nBy this point, we have confirmed that the application, in the form of a Docker container, works locally. We have uploaded the container to Docker Hub. All of this means that we have already made significant progress towards our goal. Now we need to address two more issues:<\/p>\n<ul>\n<li>Setting up a CI tool for testing and deploying the code.<\/li>\n<li>Configuring the production server so that it can download and run our code.<\/li>\n<\/ul>\n<p>\nIn our case, the CI\/CD solution used is <noindex><a rel=\"nofollow\" href=\"https:\/\/travis-ci.org\/\">Travis CI<\/a><\/noindex>. As the server \u2014 <noindex><a rel=\"nofollow\" href=\"https:\/\/www.digitalocean.com\/\">DigitalOcean<\/a><\/noindex>.<\/p>\n<p>It should be noted that another combination of services can also be used here. For example, instead of Travis CI, one could use CircleCI or Github Actions. And instead of DigitalOcean \u2014 AWS or Linode.<\/p>\n<p>We decided to work with Travis CI, and I already have some settings configured in this service. Therefore, I will briefly explain how to prepare it for use.<\/p>\n<h2>Travis CI<\/h2>\n<p>\nTravis CI is a tool for testing and deploying code. I wouldn't like to delve into the specifics of configuring Travis CI, as each project is unique, and that wouldn't bring much benefit. However, I will talk about the basics that will help you get started in case you decide to use Travis CI. Regardless of what you choose \u2014 Travis CI, CircleCI, Jenkins, or something else, similar configuration methods will be applied.<\/p>\n<p>To get started with Travis CI, go to <noindex><a rel=\"nofollow\" href=\"https:\/\/travis-ci.org\/\">the project site<\/a><\/noindex> and create an account. Then, integrate Travis CI with your GitHub account. During the setup process, you'll need to specify the repository you want to automate and enable access to it. (I use GitHub but I'm sure Travis CI can integrate with BitBucket, GitLab, and other similar services as well).<\/p>\n<p>Every time Travis CI starts working, a server is launched to execute the commands specified in the configuration file, including deployment of the corresponding repository branches.<\/p>\n<h3>\u258dJob Lifecycle<\/h3>\n<p>\nThe Travis CI configuration file, called <code>.travis.yml<\/code> and stored in the root directory of the project, supports the concept of events <noindex><a rel=\"nofollow\" href=\"https:\/\/docs.travis-ci.com\/user\/job-lifecycle\/\">lifecycle<\/a><\/noindex> of the job. Here are these events, listed in the order they occur:<\/p>\n<ul>\n<li><code>apt addons<\/code><\/li>\n<li><code>cache components<\/code><\/li>\n<li><code>before_install<\/code><\/li>\n<li><code>install<\/code><\/li>\n<li><code>before_script<\/code><\/li>\n<li><code>script<\/code><\/li>\n<li><code>before_cache<\/code><\/li>\n<li><code>after_success or after_failure<\/code><\/li>\n<li><code>before_deploy<\/code><\/li>\n<li><code>deploy<\/code><\/li>\n<li><code>after_deploy<\/code><\/li>\n<li><code>after_script<\/code><\/li>\n<\/ul>\n<p><\/p>\n<h3>\u258dTesting<\/h3>\n<p>\nIn the configuration file, I am going to set up a local Travis CI server. I chose Node version 12 as the language and instructed the system to install the dependencies necessary for using Docker.<\/p>\n<p>Everything listed in <code>.travis.yml<\/code>, will be executed for all pull requests to all repository branches unless stated otherwise. This is a useful feature since it means we can test all code coming into the repository. It allows us to know whether the code is ready to be merged into the branch <code>master<\/code>, and whether it will disrupt the build process. In this global configuration, I set everything locally, run the Webpack development server in the background (this is part of my workflow), and run the tests.<\/p>\n<p>If you want your repository to display badges with code coverage information, <noindex><a rel=\"nofollow\" href=\"https:\/\/www.taniarascia.com\/display-build-status-and-test-coverage\/\">here<\/a><\/noindex> you can find a brief guide on using Jest, Travis CI, and Coveralls to collect and display this information.<\/p>\n<p>So, here is the contents of the file <code>.travis.yml<\/code>:<\/p>\n<pre><code class=\"plaintext\"># \u0423\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c \u044f\u0437\u044b\u043a\nlanguage: node_js\n\n# \u0423\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c \u0432\u0435\u0440\u0441\u0438\u044e Node.js\nnode_js:\n\u00a0\u00a0- '12'\n\nservices:\n\u00a0\u00a0# \u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u043a\u043e\u043c\u0430\u043d\u0434\u043d\u0443\u044e \u0441\u0442\u0440\u043e\u043a\u0443 Docker\n\u00a0\u00a0- docker\n\ninstall:\n\u00a0\u00a0# \u0423\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c \u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0441\u0442\u0438 \u0434\u043b\u044f \u0442\u0435\u0441\u0442\u043e\u0432\n\u00a0\u00a0- npm ci\n\nbefore_script:\n\u00a0\u00a0# \u0417\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u044c \u0441\u0435\u0440\u0432\u0435\u0440 \u0438 \u043a\u043b\u0438\u0435\u043d\u0442 \u0434\u043b\u044f \u0442\u0435\u0441\u0442\u043e\u0432\n\u00a0\u00a0- npm run dev &amp;\n\nscript:\n\u00a0\u00a0# \u0417\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u044c \u0442\u0435\u0441\u0442\u044b\n\u00a0\u00a0- npm run test<\/code><\/pre>\n<p>\nThis marks the end of the actions performed for all branches of the repository and for pull requests.<\/p>\n<h3>\u258dDeployment<\/h3>\n<p>\nAssuming that all automated tests have completed successfully, we can optionally deploy the code to the production server. Since we want to do this only for code from the branch <code>master<\/code>, we provide the appropriate instructions in the deployment settings. Before you try to use the code we will discuss next in your project, I would like to warn you that you need to have a real script invoked for deployment.<\/p>\n<pre><code class=\"plaintext\">deploy:\n  # Build the Docker container and push it to Docker Hub\n  provider: script\n  script: bash deploy.sh\n  on:\n    branch: master<\/code><\/pre>\n<p>\nThe deployment script solves two tasks:<\/p>\n<ul>\n<li>Building, tagging, and pushing the image to Docker Hub using a CI tool (in our case, Travis CI).<\/li>\n<li>Downloading the image on the server, stopping the old container, and starting the new one (in our case, the server runs on the DigitalOcean platform).<\/li>\n<\/ul>\n<p>\nFirst, you need to set up an automatic process for building, tagging, and pushing the image to Docker Hub. This is very similar to what we have done manually, except that we need a strategy for assigning unique tags to the images and automating the login process. I encountered difficulties with some details of the deployment script, such as the tagging strategy, login, SSH key encoding, and establishing an SSH connection. Fortunately, my boyfriend is very skilled with bash, as well as many other things. He helped me write this script.<\/p>\n<p>So, the first part of the script is uploading the image to Docker Hub. It\u2019s relatively simple to do. The tagging scheme I used involves combining the git hash and the git tag if it exists. This ensures the creation of a unique tag and simplifies the identification of the build it is based on. <code>DOCKER_USERNAME<\/code> and <code>DOCKER_PASSWORD<\/code> \u2014 these are custom environment variables that can be set through the Travis CI interface. Travis CI will automatically handle sensitive data so that it doesn\u2019t fall into the wrong hands.<\/p>\n<p>Here is the first part of the script <code>deploy.sh<\/code>.<\/p>\n<pre><code class=\"plaintext\">#!\/bin\/sh\nset -e # \u041e\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c \u0441\u043a\u0440\u0438\u043f\u0442 \u043f\u0440\u0438 \u043d\u0430\u043b\u0438\u0447\u0438\u0438 \u043e\u0448\u0438\u0431\u043e\u043a\n\nIMAGE=&quot;&lt;username&gt;\/&lt;repository&gt;&quot; \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 # \u041e\u0431\u0440\u0430\u0437 Docker\nGIT_VERSION=$(git describe --always --abbrev --tags --long) # Git-\u0445\u044d\u0448 \u0438 \u0442\u0435\u0433\u0438\n\n# \u0421\u0431\u043e\u0440\u043a\u0430 \u0438 \u0442\u0435\u0433\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u043e\u0431\u0440\u0430\u0437\u0430\ndocker build -t ${IMAGE}:${GIT_VERSION} .\ndocker tag ${IMAGE}:${GIT_VERSION} ${IMAGE}:latest\n\n# \u0412\u0445\u043e\u0434 \u0432 Docker Hub \u0438 \u0432\u044b\u0433\u0440\u0443\u0437\u043a\u0430 \u043e\u0431\u0440\u0430\u0437\u0430\necho &quot;${DOCKER_PASSWORD}&quot; | docker login -u &quot;${DOCKER_USERNAME}&quot; --password-stdin\ndocker push ${IMAGE}:${GIT_VERSION}<\/code><\/pre>\n<p>\nWhat the second part of the script will be completely depends on which host you are using and how the connection to it is organized. In my case, since I am using DigitalOcean, the commands used to connect to the server are <noindex><a rel=\"nofollow\" href=\"https:\/\/github.com\/digitalocean\/doctl\">doctl<\/a><\/noindex>. When working with AWS, the utility <code>aws<\/code>, and so on.<\/p>\n<p>Setting up the server was not particularly difficult. I configured a droplet based on the basic image. It's worth noting that the system I chose requires a one-time manual installation of Docker and a one-time manual start of Docker. For the installation of Docker, I used Ubuntu 18.04, so if you're also using Ubuntu, you can simply follow <noindex><a rel=\"nofollow\" href=\"https:\/\/phoenixnap.com\/kb\/how-to-install-docker-on-ubuntu-18-04\">this<\/a><\/noindex> the simple guide.<\/p>\n<p>I'm not talking about specific commands for the service, as this aspect can vary widely in different cases. I'll just outline a general plan of action performed after connecting via SSH to the server on which the project will be deployed:<\/p>\n<ul>\n<li>You need to find the container that is currently running and stop it.<\/li>\n<li>Then, you need to start a new container in the background.<\/li>\n<li>You will need to set the local server port to <code>80<\/code> \u2014 this will allow accessing the site at an address like <code>example.com<\/code>, without specifying the port, rather than using an address like <code>example.com:5000<\/code>.<\/li>\n<li>And finally, you need to remove all old containers and images.<\/li>\n<\/ul>\n<p>\nHere\u2019s the continuation of the script.<\/p>\n<pre><code class=\"plaintext\"># \u041d\u0430\u0439\u0442\u0438 ID \u0440\u0430\u0431\u043e\u0442\u0430\u044e\u0449\u0435\u0433\u043e \u043a\u043e\u043d\u0442\u0435\u0439\u043d\u0435\u0440\u0430\nCONTAINER_ID=$(docker ps | grep takenote | cut -d&quot; &quot; -f1)\n\n# \u041e\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c \u0441\u0442\u0430\u0440\u044b\u0439 \u043a\u043e\u043d\u0442\u0435\u0439\u043d\u0435\u0440, \u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u044c \u043d\u043e\u0432\u044b\u0439, \u043e\u0447\u0438\u0441\u0442\u0438\u0442\u044c \u0441\u0438\u0441\u0442\u0435\u043c\u0443\ndocker stop ${CONTAINER_ID}\ndocker run --restart unless-stopped -d -p 80:5000 ${IMAGE}:${GIT_VERSION}\ndocker system prune -a -f<\/code><\/pre>\n<p><\/p>\n<h2>Some things to pay attention to<\/h2>\n<p>\nWhen you connect to the server via SSH from Travis CI, you may see a warning that will prevent the installation from continuing, as the system will wait for user input.<\/p>\n<pre><code class=\"plaintext\">The authenticity of host ' ()' can't be established.\nRSA key fingerprint is .\nAre you sure you want to continue connecting (yes\/no)?<\/code><\/pre>\n<p>\nI learned that the string key can be encoded in base64 so that it can be saved in a format that is convenient and secure to work with. At the installation stage, the public key can be decoded and written to the file <code>known_hosts<\/code> to get rid of the aforementioned error.<\/p>\n<pre><code class=\"plaintext\">echo  | base64 # outputs<\/code><\/pre>\n<p>\nIn practice, this command might look like:<\/p>\n<pre><code class=\"plaintext\">echo \"123.45.67.89 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAklOUpkDHrfHY17SbrmTIpNLTGK9Tjom\/BWDSU\nGPl+nafzlHDTYW7hdI4yZ5ew18JH4JW9jbhUFrviQzM7xlELEVf4h9lFX5QVkbPppSwg0cda3\nPbv7kOdJ\/MTyBlWXFCR+HAo3FXRitBqxiX1nKhXpHAZsMciLq8V6RjsNAQwdsdMFvSlVK\/7XA\nt3FaoJoAsncM1Q9x5+3V0Ww68\/eIFmb1zuUFljQJKprrX88XypNDvjYNby6vw\/Pb0rwert\/En\nmZ+AW4OZPnTPI89ZPmVMLuayrD2cE86Z\/il8b+gw3r3+1nKatmIkjn2so1d01QraTlMqVSsbx\nNrRFi9wrf+M7Q== you@example.com\" | base64<\/code><\/pre>\n<p>\nAnd here's what it outputs \u2014 a base64 encoded string:<\/p>\n<pre><code class=\"plaintext\">123.45.67.89 ssh-rsa AABBA3NzaC1yc2EAAABBQW5sUmNrWm9nLjk3eHk1Y3JlQUFBQUJJd0FBQVFFQWtsT1Vwa0RIcmZIWTE3U2JybVRJcE5MVEdLOVRqb20vQldEU1UKR1BsK25hZnpsSERUWVc3aGRJNHlaNWV3MThKSDRKVzlqYmhVRnJ2aVF6TTd4bEVMRVZmNGg5bEZYNVFWa2JQcHBTd2cwY2RhMwpQYnY3a09kSi9NVHlCbFdYRkNSK0hBbzNGWFJpdEJxeGlYMW5LaFhwSEFac01jaUxxOFY2UmpzTkFRd2RzZE1GdlNsVksvN1hBCnQzRmFvSm9Bc25jTTFROXg1KzNWMFd3NjgvZUlGbWIxenVVRmxqUUpLcHJyWDg4WHlwTkR2allOYnk2dncvUGIwcndlcnQvRW4KbVorQVc0T1pQblRQSTg5WlBtVk1MdWF5ckQyY0U4NlovaWw4YitndzNyMysxbkthdG1Ja2puMnNvMWQwMVFyYVRsTXFWU3NieApOclJGaTl3cmYrTTdRPT0geW91QGV4YW1wbGUuY29tCg==<\/code><\/pre>\n<p>\nHere's the command mentioned above<\/p>\n<pre><code class=\"plaintext\">install:\n  - echo  | base64 -d &gt;&gt; $HOME\/.ssh\/known_hosts<\/code><\/pre>\n<p>\nThe same approach can be used with a private key when establishing a connection, as you might need the private key to access the server. When working with the key, you just need to ensure its secure storage in a Travis CI environment variable, and that it's not output anywhere.<\/p>\n<p>Another thing to consider is that you might need to run the entire deployment script as a single line, for example \u2014 using <code>doctl<\/code>. This may require some additional effort.<\/p>\n<pre><code class=\"plaintext\">doctl compute ssh  --ssh-command \"all commands will be here &amp;&amp; here\"<\/code><\/pre>\n<p><\/p>\n<h2>TLS\/SSL and load balancing<\/h2>\n<p>\nAfter I completed everything mentioned above, the last issue I faced was that the server did not have SSL. Since I use a Node.js server, to enable <noindex><a rel=\"nofollow\" href=\"https:\/\/www.digitalocean.com\/community\/tutorials\/how-to-secure-a-containerized-node-js-application-with-nginx-let-s-encrypt-and-docker-compose\">working<\/a><\/noindex> reverse proxy Nginx and Let\u2019s Encrypt, it requires some effort.<\/p>\n<p>I really didn\u2019t want to go through all those SSL configurations manually, so I simply created a load balancer and recorded its details in DNS. In the case of DigitalOcean, for example, creating a self-signed certificate that auto-renews on the load balancer is a simple, free, and quick procedure. This approach also has the added benefit of easily allowing SSL configuration on multiple servers behind the load balancer, enabling the servers themselves to not have to 'worry' about SSL, while still using port <code>80<\/code>. So, configuring SSL on the load balancer is much easier and more convenient than alternative SSL configuration methods.<\/p>\n<p>Now you can close all ports on the server that accept incoming connections \u2014 except for port <code>80<\/code>, which is used for communication with the load balancer, and port <code>22<\/code> for SSH. As a result, attempts to directly access the server through any ports except these two will fail.<\/p>\n<h2>Summary<\/h2>\n<p>\nAfter following all the steps outlined in this material, I was no longer intimidated by the Docker platform or the concepts of automated CI\/CD pipelines. I was able to set up a continuous integration chain, during which code testing occurs before it reaches production, along with automatic code deployment on the server. Everything is still relatively new to me, and I am confident that there are ways to improve my automated workflow and make it more efficient. So if you have any ideas on this matter, please <noindex><a rel=\"nofollow\" href=\"mailto:hello@taniarascia.com\">to me<\/a><\/noindex> let me know. I hope this article has helped you in your endeavors. I like to believe that by reading it, you learned as much as I did while figuring out everything discussed here.<\/p>\n<p><b>P.S.<\/b> In our <noindex><a rel=\"nofollow\" href=\"https:\/\/ruvds.com\/ru-rub\/marketplace#order\">marketplace<\/a><\/noindex> we have an image <noindex><a rel=\"nofollow\" href=\"https:\/\/ruvds.com\/ru-rub\/marketplace\/docker#advantages\">Docker<\/a><\/noindex>, which can be installed with a single click. You can check the operation of containers on <noindex><a rel=\"nofollow\" href=\"https:\/\/ruvds.com\/\">VPS<\/a><\/noindex>. All new clients receive 3 days of free testing.<\/p>\n<p><b>Dear readers!<\/b> Do you use CI\/CD technologies in your projects?<\/p>\n<p><noindex><a rel=\"nofollow\" href=\"https:\/\/ruvds.com\/ru-rub\/#order\"><img decoding=\"async\" alt=\"Creating a CI\/CD chain and automating work with Docker\" src=\"\/wp-content\/uploads\/2020\/02\/3fbfa90ea56a3d9b6b89ba945eb94570.jpg\" style=\"display:block;margin: 0 auto;\" \/><\/a><\/noindex><br \/>\n<br \/>Source: <a content=\"nofollow\" rel=\"nofollow\" href=\"https:\/\/habr.com\/ru\/company\/ruvds\/blog\/488668\/\">habr.com<\/a> <\/p>","protected":false,"gt_translate_keys":[{"key":"rendered","format":"html"}]},"excerpt":{"rendered":"<p>\u042f \u043d\u0430\u043f\u0438\u0441\u0430\u043b\u0430 \u043c\u043e\u0438 \u043f\u0435\u0440\u0432\u044b\u0435 \u0441\u0430\u0439\u0442\u044b \u0432 \u043a\u043e\u043d\u0446\u0435 90-\u0445. \u0422\u043e\u0433\u0434\u0430 \u043f\u0440\u0438\u0432\u043e\u0434\u0438\u0442\u044c \u0438\u0445 \u0432 \u0440\u0430\u0431\u043e\u0447\u0435\u0435 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435 \u0431\u044b\u043b\u043e \u043e\u0447\u0435\u043d\u044c \u043f\u0440\u043e\u0441\u0442\u043e. \u0411\u044b\u043b Apache-\u0441\u0435\u0440\u0432\u0435\u0440 \u043d\u0430 \u043a\u0430\u043a\u043e\u043c-\u043d\u0438\u0431\u0443\u0434\u044c \u043e\u0431\u0449\u0435\u043c \u0445\u043e\u0441\u0442\u0438\u043d\u0433\u0435, \u043d\u0430 \u044d\u0442\u043e\u0442 \u0441\u0435\u0440\u0432\u0435\u0440 \u043c\u043e\u0436\u043d\u043e \u0431\u044b\u043b\u043e \u0432\u043e\u0439\u0442\u0438 \u043f\u043e FTP, \u043d\u0430\u043f\u0438\u0441\u0430\u0432 \u0432 \u0431\u0440\u0430\u0443\u0437\u0435\u0440\u043d\u043e\u0439 \u0441\u0442\u0440\u043e\u043a\u0435 \u043d\u0435\u0447\u0442\u043e \u0432\u0440\u043e\u0434\u0435 ftp:\/\/ftp.example.com. \u041f\u043e\u0442\u043e\u043c \u043d\u0430\u0434\u043e \u0431\u044b\u043b\u043e \u0432\u0432\u0435\u0441\u0442\u0438 \u0438\u043c\u044f \u0438 \u043f\u0430\u0440\u043e\u043b\u044c \u0438 \u0432\u044b\u0433\u0440\u0443\u0437\u0438\u0442\u044c \u0444\u0430\u0439\u043b\u044b \u043d\u0430 \u0441\u0435\u0440\u0432\u0435\u0440. \u0414\u0440\u0443\u0433\u0438\u0435 \u0431\u044b\u043b\u0438 \u0432\u0440\u0435\u043c\u0435\u043d\u0430, \u0432\u0441\u0451 \u0442\u043e\u0433\u0434\u0430 [&hellip;]<\/p>\n","protected":false,"gt_translate_keys":[{"key":"rendered","format":"html"}]},"author":1,"featured_media":69352,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[688],"tags":[],"class_list":["post-69351","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-administrirovanie"],"aioseo_notices":[],"aioseo_head":"\n\t\t<!-- All in One SEO 5.0.1.1 - aioseo.com -->\n\t<meta name=\"description\" content=\"\u042f \u043d\u0430\u043f\u0438\u0441\u0430\u043b\u0430 \u043c\u043e\u0438 \u043f\u0435\u0440\u0432\u044b\u0435 \u0441\u0430\u0439\u0442\u044b \u0432 \u043a\u043e\u043d\u0446\u0435 90-\u0445. \u0422\u043e\u0433\u0434\u0430 \u043f\u0440\u0438\u0432\u043e\u0434\u0438\u0442\u044c \u0438\u0445 \u0432 \u0440\u0430\u0431\u043e\u0447\u0435\u0435 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435 \u0431\u044b\u043b\u043e \u043e\u0447\u0435\u043d\u044c \u043f\u0440\u043e\u0441\u0442\u043e.\" \/>\n\t<meta name=\"robots\" content=\"max-image-preview:large\" \/>\n\t<meta name=\"author\" content=\"Yuri Gagarin\"\/>\n\t<link rel=\"canonical\" href=\"https:\/\/prohoster.info\/en\/blog\/administrirovanie\/sozdanie-ci-cd-czepochki-i-avtomatizacziya-raboty-s-docker\" \/>\n\t<meta name=\"generator\" content=\"All in One SEO (AIOSEO) 5.0.1.1\" \/>\n\t\t<meta property=\"og:locale\" content=\"en_US\" \/>\n\t\t<meta property=\"og:site_name\" content=\"ProHoster | \u041a\u0443\u043f\u0438\u0442\u044c \u043d\u0430\u0434\u0435\u0436\u043d\u044b\u0439 \u0445\u043e\u0441\u0442\u0438\u043d\u0433 \u0434\u043b\u044f \u0441\u0430\u0439\u0442\u043e\u0432 \u0441 \u0437\u0430\u0449\u0438\u0442\u043e\u0439 \u043e\u0442 DDoS, VPS VDS \u0441\u0435\u0440\u0432\u0435\u0440\u044b\" \/>\n\t\t<meta property=\"og:type\" content=\"article\" \/>\n\t\t<meta property=\"og:title\" content=\"\ud83e\udd47\u0421\u043e\u0437\u0434\u0430\u043d\u0438\u0435 CI\/CD-\u0446\u0435\u043f\u043e\u0447\u043a\u0438 \u0438 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0437\u0430\u0446\u0438\u044f \u0440\u0430\u0431\u043e\u0442\u044b \u0441 Docker | ProHoster\" \/>\n\t\t<meta property=\"og:description\" content=\"\u042f \u043d\u0430\u043f\u0438\u0441\u0430\u043b\u0430 \u043c\u043e\u0438 \u043f\u0435\u0440\u0432\u044b\u0435 \u0441\u0430\u0439\u0442\u044b \u0432 \u043a\u043e\u043d\u0446\u0435 90-\u0445. \u0422\u043e\u0433\u0434\u0430 \u043f\u0440\u0438\u0432\u043e\u0434\u0438\u0442\u044c \u0438\u0445 \u0432 \u0440\u0430\u0431\u043e\u0447\u0435\u0435 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435 \u0431\u044b\u043b\u043e \u043e\u0447\u0435\u043d\u044c \u043f\u0440\u043e\u0441\u0442\u043e.\" \/>\n\t\t<meta property=\"og:url\" content=\"https:\/\/prohoster.info\/en\/blog\/administrirovanie\/sozdanie-ci-cd-czepochki-i-avtomatizacziya-raboty-s-docker\" \/>\n\t\t<meta property=\"og:image\" content=\"https:\/\/prohoster.info\/wp-content\/uploads\/2021\/11\/logo-350.jpg\" \/>\n\t\t<meta property=\"og:image:secure_url\" content=\"https:\/\/prohoster.info\/wp-content\/uploads\/2021\/11\/logo-350.jpg\" \/>\n\t\t<meta property=\"og:image:width\" content=\"350\" \/>\n\t\t<meta property=\"og:image:height\" content=\"350\" \/>\n\t\t<meta property=\"article:published_time\" content=\"2020-02-18T11:44:06+00:00\" \/>\n\t\t<meta property=\"article:modified_time\" content=\"2020-03-03T13:15:00+00:00\" \/>\n\t\t<meta property=\"article:publisher\" content=\"https:\/\/www.facebook.com\/prohoster\" \/>\n\t\t<meta property=\"article:author\" content=\"https:\/\/www.facebook.com\/prohoster\" \/>\n\t\t<!-- All in One SEO -->\n\n","aioseo_head_json":{"title":"\ud83e\udd47Creating a CI\/CD chain and automating work with Docker | ProHoster","description":"I wrote my first websites in the late 90s. Back then, getting them up and running was very simple.","canonical_url":"https:\/\/prohoster.info\/en\/blog\/administrirovanie\/sozdanie-ci-cd-czepochki-i-avtomatizacziya-raboty-s-docker","robots":"max-image-preview:large","keywords":"","webmasterTools":{"miscellaneous":""},"schema":null,"og:locale":"en_US","og:site_name":"ProHoster | \u041a\u0443\u043f\u0438\u0442\u044c \u043d\u0430\u0434\u0435\u0436\u043d\u044b\u0439 \u0445\u043e\u0441\u0442\u0438\u043d\u0433 \u0434\u043b\u044f \u0441\u0430\u0439\u0442\u043e\u0432 \u0441 \u0437\u0430\u0449\u0438\u0442\u043e\u0439 \u043e\u0442 DDoS, VPS VDS \u0441\u0435\u0440\u0432\u0435\u0440\u044b","og:type":"article","og:title":"\ud83e\udd47\u0421\u043e\u0437\u0434\u0430\u043d\u0438\u0435 CI\/CD-\u0446\u0435\u043f\u043e\u0447\u043a\u0438 \u0438 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0437\u0430\u0446\u0438\u044f \u0440\u0430\u0431\u043e\u0442\u044b \u0441 Docker | ProHoster","og:description":"\u042f \u043d\u0430\u043f\u0438\u0441\u0430\u043b\u0430 \u043c\u043e\u0438 \u043f\u0435\u0440\u0432\u044b\u0435 \u0441\u0430\u0439\u0442\u044b \u0432 \u043a\u043e\u043d\u0446\u0435 90-\u0445. \u0422\u043e\u0433\u0434\u0430 \u043f\u0440\u0438\u0432\u043e\u0434\u0438\u0442\u044c \u0438\u0445 \u0432 \u0440\u0430\u0431\u043e\u0447\u0435\u0435 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435 \u0431\u044b\u043b\u043e \u043e\u0447\u0435\u043d\u044c \u043f\u0440\u043e\u0441\u0442\u043e.","og:url":"https:\/\/prohoster.info\/en\/blog\/administrirovanie\/sozdanie-ci-cd-czepochki-i-avtomatizacziya-raboty-s-docker","og:image":"https:\/\/prohoster.info\/wp-content\/uploads\/2021\/11\/logo-350.jpg","og:image:secure_url":"https:\/\/prohoster.info\/wp-content\/uploads\/2021\/11\/logo-350.jpg","og:image:width":350,"og:image:height":350,"article:published_time":"2020-02-18T11:44:06+00:00","article:modified_time":"2020-03-03T13:15:00+00:00","article:publisher":"https:\/\/www.facebook.com\/prohoster","article:author":"https:\/\/www.facebook.com\/prohoster"},"aioseo_meta_data":{"post_id":"69351","title":null,"description":null,"keywords":null,"keyphrases":{"focus":[],"additional":[]},"primary_term":null,"canonical_url":null,"og_title":null,"og_description":null,"og_object_type":"default","og_image_type":"default","og_image_url":null,"og_image_width":null,"og_image_height":null,"og_image_custom_url":null,"og_image_custom_fields":null,"og_video":null,"og_custom_url":null,"og_article_section":null,"og_article_tags":null,"twitter_use_og":false,"twitter_card":"default","twitter_image_type":"default","twitter_image_url":null,"twitter_image_custom_url":null,"twitter_image_custom_fields":null,"twitter_title":null,"twitter_description":null,"schema":{"blockGraphs":[],"customGraphs":[],"default":{"data":{"Article":[],"Course":[],"Dataset":[],"FAQPage":[],"Movie":[],"Person":[],"Product":[],"ProductReview":[],"Car":[],"Recipe":[],"Service":[],"SoftwareApplication":[],"WebPage":[]},"graphName":"","isEnabled":true},"graphs":[]},"schema_type":null,"schema_type_options":null,"pillar_content":false,"robots_default":true,"robots_noindex":false,"robots_noarchive":false,"robots_nosnippet":false,"robots_nofollow":false,"robots_noimageindex":false,"robots_noodp":false,"robots_notranslate":false,"robots_max_snippet":null,"robots_max_videopreview":null,"robots_max_imagepreview":"large","priority":null,"frequency":null,"local_seo":null,"seo_analyzer_scan_date":null,"breadcrumb_settings":null,"limit_modified_date":false,"reviewed_by":null,"ai":null,"created":"2021-02-28 19:24:45","updated":"2026-08-11 12:50:24","focus_keyword":null,"additional_keywords":null,"truseo_locale":null},"gt_translate_keys":[{"key":"link","format":"url"}],"_links":{"self":[{"href":"https:\/\/prohoster.info\/en\/wp-json\/wp\/v2\/posts\/69351","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/prohoster.info\/en\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/prohoster.info\/en\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/prohoster.info\/en\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/prohoster.info\/en\/wp-json\/wp\/v2\/comments?post=69351"}],"version-history":[{"count":0,"href":"https:\/\/prohoster.info\/en\/wp-json\/wp\/v2\/posts\/69351\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/prohoster.info\/en\/wp-json\/wp\/v2\/media\/69352"}],"wp:attachment":[{"href":"https:\/\/prohoster.info\/en\/wp-json\/wp\/v2\/media?parent=69351"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/prohoster.info\/en\/wp-json\/wp\/v2\/categories?post=69351"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/prohoster.info\/en\/wp-json\/wp\/v2\/tags?post=69351"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}