Introduction
I have written before about using Docker and Docker Compose for multi-container orchestration. That covers your laptop. It does not cover what happens when someone else pushes a change and you are not there to watch it build. That is what CI/CD is for, and in this post I wire Docker Compose into GitHub Actions so builds, tests, and deployments happen automatically, reproducibly, and without me babysitting them.
CI/CD and Docker
Continuous Integration and Continuous Deployment
CI/CD stands for Continuous Integration and Continuous Deployment (or Continuous Delivery), and it is a set of practices in software development aimed at automating and streamlining the processes of building, testing, and deploying code changes.
-
Continuous Integration (CI): Developers frequently integrate code changes into a shared repository. With CI, automated build and test processes are triggered whenever code changes are pushed to the repository. This ensures that new code integrates smoothly with the existing codebase and helps catch integration issues early in the development cycle.
-
Continuous Deployment (CD) / Continuous Delivery (CD):
-
Continuous Delivery: After successful integration and testing, the code is automatically prepared and delivered to a staging or pre-production environment. In continuous delivery, the deployment to production is a manual process triggered by the development team when they decide the code is ready for release.
-
Continuous Deployment: In continuous deployment, the entire process is automated, and changes that pass all tests are automatically deployed to the production environment without manual intervention. This results in a faster and more efficient release cycle.
-
CI/CD practices contribute to the following benefits:
-
Early Detection of Issues: Automated testing during the CI phase helps identify bugs and integration problems early in the development process, reducing the chances of defects reaching production.
-
Faster Release Cycles: Automation of build, test, and deployment processes speeds up the delivery of new features and bug fixes, enabling more frequent releases.
-
Consistency: CI/CD ensures consistency in the development, testing, and deployment environments, reducing the “it works on my machine” problem.
-
Risk Reduction: With automated testing and deployment, there is a lower risk of human error during manual processes, leading to more reliable and stable software.
-
Scalability: CI/CD practices are crucial in scalable and agile development, allowing teams to efficiently manage and release software updates.
Tools like Jenkins, Travis CI, GitLab CI/CD, and GitHub Actions are commonly used to implement CI/CD pipelines, integrating seamlessly with version control systems and other development tools.
GitHub Actions with Docker Compose
Using GitHub Actions with Docker Compose involves creating workflows that define the steps to build, test, and deploy your application.
Below is a simple example of a GitHub Actions workflow for a Docker Compose project. This assumes you have a docker-compose.yml file in your repository.
- Create a Workflow File:
- Create a directory named
.github/workflowsinside your GitHub repository. - In that directory, create a YAML file, for example,
docker-compose.yml.
- Create a directory named
- Define the Workflow:
-
Define the workflow steps in the YAML file. Below is a basic example:
name: Docker Compose Build and Test on: push: branches: - main jobs: build: runs-on: ubuntu-latest steps: - name: Checkout Repository uses: actions/checkout@v4 - name: Build and Test run: | docker compose up -d docker compose exec -T your-service-name pytest working-directory: path/to/your/app - name: Stop Docker Compose run: docker compose down working-directory: path/to/your/app -
Customize the workflow according to your project structure. Replace
your-service-nameandpath/to/your/appwith the actual service name and the path to your application.
-
- Commit and Push:
- Commit the workflow file to your repository.
- Push the changes to trigger the GitHub Actions workflow.
This example workflow does the following:
- It runs on each push to the
mainbranch. - It checks out the repository, builds and tests the application using Docker Compose, and then stops the Docker Compose services.
A couple of things worth calling out, since I got both wrong the first time I wrote a workflow like this:
ubuntu-latestrunners already ship with Docker and the Compose v2 plugin installed. You do not need aservices:block running a separatedocker:dindcontainer to get Docker inside your job - that pattern is a GitLab CI idiom, and dragging it into GitHub Actions just gets you a second, unauthenticated Docker daemon listening on port 2375 for no reason. Skip it.- Use
docker compose(a space, no hyphen), notdocker-compose. The standalone Pythondocker-composev1 binary reached end-of-life in mid-2024 and GitHub removed it from the runner images in 2025. If a tutorial you’re reading still uses the hyphenated form, it is out of date. docker compose exec -Tdisables pseudo-TTY allocation, which matters in CI - without-T,execcan hang or fail on a runner that has no TTY attached.
Pin actions/checkout to at least @v4 (it moved to a Node 20 runtime; the old @v2 tag is stuck on a deprecated Node version and increasingly triggers warnings), and check for a newer major version periodically - actions/checkout picked up a security-relevant default change in mid-2026 around fork pull requests, which is exactly the kind of thing you want without having to notice it yourself.
Customize the workflow according to your specific needs, such as adding deployment steps or handling secrets. Additionally, consider versioning your workflows and using the appropriate GitHub Actions for your use case.
The provided example is a starting point; you may need to adjust it based on your project structure and requirements.
GitHub Actions for the Flask App
Below is a simplified example of a GitHub Actions workflow for a “Hello World!” Flask app using Docker Compose. This example assumes a Flask app with a basic structure and a docker-compose.yml file.
- Create a Workflow File:
- Create a directory named
.github/workflowsinside your GitHub repository. - In that directory, create a YAML file, for example,
flask-ci-cd.yml.
- Create a directory named
- Define the Workflow:
-
Define the workflow steps in the YAML file:
name: Flask CI/CD with Docker Compose on: push: branches: - main jobs: build: runs-on: ubuntu-latest steps: - name: Checkout Repository uses: actions/checkout@v4 - name: Build and Test run: | docker compose up -d docker compose exec -T web pytest working-directory: path/to/your/flask-app - name: Stop Docker Compose run: docker compose down working-directory: path/to/your/flask-app -
Customize the workflow according to your Flask app structure. Replace
path/to/your/flask-appwith the path to your Flask application.
-
- Commit and Push:
- Commit the workflow file to your repository.
- Push the changes to trigger the GitHub Actions workflow.
This simplified example assumes your Flask app has a service named web in the docker-compose.yml file, and it runs tests using pytest. Adjust the commands and paths according to your specific setup.
This workflow will execute on each push to the main branch, and it performs the following steps:
- Checks out the repository.
- Builds and tests the Flask app using Docker Compose.
- Stops the Docker Compose services after testing.
Remember to adapt the workflow to your specific project structure, dependencies, and testing procedures.
Environment separation and release gating
A build-and-test workflow is only half the job. Two things turned my early pipelines from “it runs” into something I would trust with production:
- Environment separation. I keep a
docker-compose.ymlfor shared service definitions and adocker-compose.override.yml(or separate-ffiles per environment) for the bits that differ - ports, volumes, debug flags. Compose merges them automatically in local dev, and CI passes explicit-fflags so it never accidentally picks up a dev override. Secrets never live in the compose file itself; they come from GitHub Actions secrets or a.envfile that is.gitignored, injected atdocker compose uptime. - Release gating. Do not let “tests passed” mean “deployed.” Add a required status check on the branch protection rule for the build-and-test job, and put the deploy job behind a GitHub Actions environment with manual approval for production. That gives you continuous delivery (everything is ready to ship) without continuous deployment (everything ships automatically) until you actually want it.
Conclusion
Docker gets your Flask app into a consistent, portable container. Docker Compose coordinates it with whatever else it needs - a database, a cache, a worker. Wire both into GitHub Actions and you get the payoff: every push is built and tested the same way, on someone else’s machine, without you lifting a finger.
Flask is just the example I used here. The same pipeline shape - checkout, docker compose up, run tests, tear down, gate the deploy - works just as well for an API, a database migration, or a machine learning model behind a serving container. Reproducible builds and explicit gates are what make “it works in CI” actually mean something.
References
- Docker Compose CLI reference
- GitHub Actions: actions/checkout
- Using environments for deployment (GitHub Actions)
CI/CD Compose Controls
- Pin base image digests in production.
- Separate build/test/deploy compose profiles.
- Keep secrets out of compose files.
- Add health-check gates before promotion.
- Preserve rollback images for fast recovery.
Stay Ahead in AI, Machine Learning & Python
No hype. Weekly notes on AI tools, Python, and what I'm actually building — plus six free gifts, including the 15-page Fantastic AI: The 2026 Toolkit and a Git Commands & Contribution Workflow Cheatsheet.
You're in
Check your inbox for Set a password to unlock articles if you want gated tutorials. Log in with the same email.