Docker Compose CI/CD for Flask with GitHub Actions
I have written before about putting a Flask app in Docker and about making Docker Compose builds fast with BuildKit. All of that covers your laptop. It does not cover what happens when someone else opens a pull request at 11 pm and you are asleep, which, frankly, is when you should be.
That is what CI/CD is for.
Docker Compose is a tool for defining and running multi-container applications from a single YAML file. GitHub Actions is GitHubβs built-in CI/CD platform that runs workflows on hosted virtual machines in response to repository events such as pull requests and pushes. Together, they let you test the same multi-container stack in CI that you run on your laptop and on the server.
In this post, I build a complete pipeline for a small Flask and PostgreSQL app with Docker Compose and GitHub Actions. Every pull request is tested against a real database. Every merge to main publishes the exact image that passed the tests. That image is then deployed, by digest, to a VPS behind an approval gate, with an automatic rollback when the new release refuses to become healthy.
The post is self-contained: every file you need is below, and the Python code and tests run as shown. If you want to follow along, you need a GitHub repository, Docker with the Compose plugin, and, for the deployment half, a Linux server. I use the Vultr VPS I set up in From Localhost to Live.
CI vs continuous delivery vs continuous deployment
The three terms get blended into βCI/CDβ, but they describe different promises:
- Continuous Integration (CI): every change is built and tested automatically before it is merged. The key word is before. A test that runs after the merge is a post-mortem, not a gate.
- Continuous Delivery: every change that passes CI produces a release candidate that could go to production at the press of a button. A human still presses it.
- Continuous Deployment: the button presses itself. Anything that passes goes live.
The pipeline in this post is continuous delivery: the image is built, tested and published automatically, and the production deploy waits in a protected environment. Removing the approval later turns it into continuous deployment without touching anything else.
A word of honesty about the benefits. Automation removes the variation of manual steps: nobody forgets a flag, nobody deploys from their laptop with uncommitted changes. But automation also propagates a bad change faster than any human could. What actually reduces deployment risk is the combination of tests, health checks, approvals and a rollback path. The pipeline is just the conveyor belt; those four are the quality control.
Flask CI/CD pipeline architecture: test, publish, deploy
The pipeline has three jobs:
pull request βββΊ test
push to main βββΊ test βββΊ publish βββΊ deploy (waits for approval)
test builds the image, starts Flask and PostgreSQL with Docker Compose, waits for both to be healthy, and runs integration tests against the running container. On main, it saves the tested image as a workflow artefact.
publish loads that exact image, pushes it to the GitHub Container Registry (GHCR) tagged with the commit SHA, and records its immutable sha256 digest.
deploy copies the Compose files to the server over SSH and runs a deploy script that pulls the digest, waits for health checks, and rolls back if they fail.
What it does not do: database schema migrations beyond a CREATE TABLE IF NOT EXISTS, blue-green or zero-downtime switching, or multi-server rollout. I discuss migrations near the end, because they are the part that makes rollbacks harder than they look.
Flask Docker Compose project structure
π Subscribe to keep reading.
Flask and PostgreSQL notes API with a database health check
π Subscribe to keep reading.
Multi-stage Dockerfile: test and ship the same image
π Subscribe to keep reading.
Docker Compose overlays for local, CI and production
π Subscribe to keep reading.
GitHub Actions workflow for Docker Compose CI/CD
π Subscribe to keep reading.
Publishing to GHCR: build once, deploy by image digest
π Subscribe to keep reading.
Deploying Docker Compose to a Vultr VPS over SSH with rollback
π Subscribe to keep reading.
Deploying to Cloud Run, Amazon ECS or Azure Container Apps with OIDC
π Subscribe to keep reading.
Rolling back Docker deployments safely with database migrations
π Subscribe to keep reading.
Docker Compose secrets vs .env files and least privilege
π Subscribe to keep reading.
Docker Compose CI/CD reproducibility checklist
π Subscribe to keep reading.
Final thoughts on Docker Compose CI/CD
π Subscribe to keep reading.
References
π Subscribe to keep reading.
You've hit a Deep Dive tutorial.
I spend dozens of hours researching, coding, and breaking things to write these guides. This content is free, but reserved for my subscriber community. Drop your email below to unlock this guide (and all past/future deep dives):
Full content temporarily unavailable β refresh in a moment
Already a subscriber? Use the magic link from your last newsletter, or reset your password.
Log in to unlock
New subscribers get an inbox mail: Set a password to unlock articles. The form does not log you in β use the same email afterwards.