CICD Patterns with GitHub Actions and Docker
Tags: cicd, docker
Published: 2021-12-04 04:28:48

*By: [Peter Solymos](https://hosting.analythium.io/author/peter/)*

**Start with easy integrations and add more serious testing to your CICD pipeline as your application matures.**

CICD is a shorthand to refer to [continuous integration and continuous delivery/deployment](https://en.wikipedia.org/wiki/CI/CD). CICD includes automated building, testing and deployment of applications. It connects development and operation activities and teams responsible for these stages in application life cycles.

CICD often builds on version control (i.e. GitHub, GitLab, Bitbucket), and hosted CICD services ([GitHub actions](https://github.com/features/actions), [GitLab pipelines](https://docs.gitlab.com/ee/ci/pipelines/), [Bitbucket pipelines](https://bitbucket.org/product/features/pipelines), and many others like Travis, CircleCI, etc.). The workflow looks like this:

1.  Commit your changes to the git remote
2.  Changes on the remote trigger the CICD pipeline
3.  If the steps in the pipeline are successful a new delivery or deployment is initiated

The difference between continuous delivery and continuous deployment is that delivery usually means that all the artifacts are delivered that are required for the deployment, but deployment happens in a separate step. Continuous deployment means that a new version is deployed without manual intervention. Think like your Shiny app is included in a Docker image (delivered) vs. the new image is deployed as a running container.

However, when it comes to Docker-based delivery and deployments, there are multiple ways how CICD can be set up. In this post, I review some of the common patterns I have seen in Docker-related CICD workflows.


## Why should you care?

Why is this whole CICD thing important? On one side, it just makes your life easier by automating some of the steps that would otherwise require you to run tests, copy files, push/pull images, etc. Making sure that no human error occurred along the way. By removing the human element and relying on great tooling, your chances of making silly mistakes are hugely eliminated.

On the other side, platform-as-a-service (PaaS) offerings often include some form of integration with major hosted git provides, such as GitHub and GitLab (this latter can be self-hosted too). Therefore, it is a good idea to take advantage of these integrations to reap the benefits of automation.

<figure>
<img src="https://hosting.analythium.io/content/images/2021/11/1-PaaS-1.png" sizes="(min-width: 720px) 720px" srcset="https://hosting.analythium.io/content/images/size/w600/2021/11/1-PaaS-1.png 600w, https://hosting.analythium.io/content/images/size/w1000/2021/11/1-PaaS-1.png 1000w, https://hosting.analythium.io/content/images/2021/11/1-PaaS-1.png 1366w" width="1366" height="768" alt="Four CICD patterns to work with Docker and PaaS" /><figcaption aria-hidden="true">Four CICD patterns to work with Docker and PaaS</figcaption>
</figure>

## Non-git-based delivery and deployment

By non-git based I mean that the source code management and the delivery/deployment ideas of your activities are independent. You use git to manage your codebase (versioning). From the same code base, you make sure that the right artifacts(files, images) are built, tested, delivered, and deployed.

### Local image build

In this most basic scenario, you version your codebase. You build Docker images locally. You push the images to a Docker registry. When you deploy the image, you trigger the pull yourself (via command line or user interface) which then initiates a new deployment.

<figure>
<img src="https://hosting.analythium.io/content/images/2021/10/2-PaaS-2.png" class="kg-image" sizes="(min-width: 720px) 720px" srcset="https://hosting.analythium.io/content/images/size/w600/2021/10/2-PaaS-2.png 600w, https://hosting.analythium.io/content/images/size/w1000/2021/10/2-PaaS-2.png 1000w, https://hosting.analythium.io/content/images/2021/10/2-PaaS-2.png 1366w" width="1366" height="768" alt="Local build with independent source code management" /><figcaption aria-hidden="true">Local build with independent source code management</figcaption>
</figure>

This is not a very common pattern in production. It is most likely that you start with something like this when you are just experimenting.

An example would be how to deploy Docker container images to virtual machines or Kubernetes clusters using OpenFaaS. In this case, the `faas-cli` is used to build images locally, push these into a registry, then instruct the servers to pull the updated images and deploy them.

<figure>
<img src="https://www.openfaas.com/touch-icon.png" class="kg-bookmark-icon" />
</figure>

### PaaS-based image build

In this scenario, the version control is still independent of the deployment process in the sense that GitHub won't instruct your PaaS provider about what to do. It is your responsibility. Once you push a new version of your app, PaaS takes care of the build and deployment processes.

<figure>
<img src="https://hosting.analythium.io/content/images/2021/10/1-PaaS-1-1.png" class="kg-image" sizes="(min-width: 720px) 720px" srcset="https://hosting.analythium.io/content/images/size/w600/2021/10/1-PaaS-1-1.png 600w, https://hosting.analythium.io/content/images/size/w1000/2021/10/1-PaaS-1-1.png 1000w, https://hosting.analythium.io/content/images/2021/10/1-PaaS-1-1.png 1366w" width="1366" height="768" alt="Build and deployment taken care of by the PaaS" /><figcaption aria-hidden="true">Build and deployment taken care of by the PaaS</figcaption>
</figure>

An example of this scenario is [deploying apps to Heroku from the command line interface](https://hosting.analythium.io/deploying-shiny-apps-to-heroku-with-docker-from-the-command-line/) (CLI). The twist here is Heroku CLI sets up a new remote on Heroku's servers. So in a way, git instructs the PaaS about the deployment. But you also keep your other remote (GitHub, etc.) to version your code. When you push a new git commit (the event), the Heroku remote will kick off the Docker build process and the deployment of the new version for you.

## Enter GitOps

> GitOps is an operational framework that takes DevOps best practices used for application development such as version control, collaboration, compliance, and CI/CD, and applies them to infrastructure automation. – [GitLab Docs](https://about.gitlab.com/topics/gitops/)

This means that the remote git platform is responsible for all downstream events. Secure communication between different service provides (GitHub and your PaaS) is set up using access tokens (revocable "secrets").

The advantage of this setup is that a lot more steps can be automated within the same workflow, like testing etc. This will make the process more robust.

### Git-triggered and PaaS-based image build

This setup is very similar to the previous, PaaS-based image build. The difference is that the build process is triggered by the code hosting platform, GitHub for example. Pushing the new commit, often to a certain pre-configured branch, will result in a webhook alerting the PaaS provider. The PaaS servers then launch the deployment process.

<figure>
<img src="https://hosting.analythium.io/content/images/2021/10/3-PaaS-3.png" class="kg-image" sizes="(min-width: 720px) 720px" srcset="https://hosting.analythium.io/content/images/size/w600/2021/10/3-PaaS-3.png 600w, https://hosting.analythium.io/content/images/size/w1000/2021/10/3-PaaS-3.png 1000w, https://hosting.analythium.io/content/images/2021/10/3-PaaS-3.png 1366w" width="1366" height="768" alt="Git commit triggers the build process in the PaaS" /><figcaption aria-hidden="true">Git commit triggers the build process in the PaaS</figcaption>
</figure>

GitHub will only deliver the news to the PaaS, but no GitHub actions are involved here. Examples for this scenario include the [GitHub integration with the DigitalOcean App Platform](https://hosting.analythium.io/auto-deploy-shiny-app-changes-to-the-digitalocean-app-platform/). Setup is usually super simple, but the ease of setup is at the expense of full control over how exactly the process is taking place. If you need more control then see the next option.

### Git-triggered and GitHub-based image build

The last scenario circles back to the 1st option, but instead of everything happening locally, the whole build/deployment process is happening on remote servers. The git push event kicks off the GitHub action (or similar) CICD process. The Docker image build happens inside the [GitHub actions runners](https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners).

<figure>
<img src="https://hosting.analythium.io/content/images/2021/10/4-PaaS-4.png" class="kg-image" sizes="(min-width: 720px) 720px" srcset="https://hosting.analythium.io/content/images/size/w600/2021/10/4-PaaS-4.png 600w, https://hosting.analythium.io/content/images/size/w1000/2021/10/4-PaaS-4.png 1000w, https://hosting.analythium.io/content/images/2021/10/4-PaaS-4.png 1366w" width="1366" height="768" alt="Build and deploy with GitHub actions" /><figcaption aria-hidden="true">Build and deploy with GitHub actions</figcaption>
</figure>

This setup and the required GitHub actions steps give you the most control of how you want things built and delivered. For example, you can specify if you want to [cache the Docker image layers](https://hosting.analythium.io/how-to-use-github-actions-with-the-digitalocean-app-platform-for-shiny-apps/#3-docker-build-and-push-with-cache). This is also ideal to make sure new deployment only happens if all steps are successful. E.g. testing your code might find errors in unit tests. You don't want to release a new version until the issues are dealt with.

Here are a few examples:

-   [Heroku deployment with GitGub actions](https://hosting.analythium.io/deploying-shiny-apps-to-heroku-with-docker-and-github-actions/)
-   [DigitalOcean app platform with image caching example](https://hosting.analythium.io/how-to-use-github-actions-with-the-digitalocean-app-platform-for-shiny-apps/)
-   [CICD with the Fly.io](https://fly.io/docs/app-guides/continuous-deployment-with-github-actions/) platform with a [Shiny example workflow](https://github.com/analythium/fly-shiny/blob/main/.github/workflows/main.yml)
-   Functions and microservices with [OpenFaaS and GitHub actions](https://www.openfaas.com/blog/openfaas-functions-with-github-actions/)

When there is a command-line tool to interact with the PaaS, you can most likely set up a GitHub actions workflow with that.

## Conclusions

CICD practices are important for a robust and automated developer experience. Knowing that the workflows are in order gives you peace of mind. Deploying containerized applications adds a few extra steps to your workflows. As a result, there are multiple ways of approaching it.

Some workflows are simpler, others are more complex. You can start simple and add complexity as your needs evolve. Try easy integrations when prototyping, add more serious testing and GitHub actions as your application matures. Once you get the hang of it, CICD becomes second nature. Tooling is getting better every day.