Elena' s AI Blog

Docker Compose CI/CD with GitHub Actions: A Reliable Flask Workflow

26 Sep 2026 (updated: 26 Sep 2026) / 53 minutes to read

Elena Daehnhardt

Midjourney, December 2023


TL;DR:
  • - Test pull requests, not just `main`: run the full Compose stack on every PR, wait for real health checks with `up --wait`, and tear down with `if: always()`.
  • - Build once, ship that artefact: the image the tests ran against is saved, pushed to GHCR and deployed by its immutable `sha256` digest, never rebuilt on the server.
  • - Deploy behind a protected `production` environment with an SSH key scoped to one server, and let the deploy script roll back automatically when the new release fails its health check.
  • - Reproducibility is several layers, not one setting: pinned runner OS, SHA-pinned actions, locked Python dependencies, base image digests and Dependabot to keep them all current.

πŸ“š This post is part of the "MLOps & Deployment" series

Series: MLOps & Deployment (Part 7 of 5)

Previous: Part 5 β€” Caching Docker Compose Builds with BuildKit

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)
  1. 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.
  2. publish loads that exact image, pushes it to the GitHub Container Registry (GHCR) tagged with the commit SHA, and records its immutable sha256 digest.
  3. 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):

Already a subscriber? Use the magic link from your last newsletter, or reset your password.

New subscribers get an inbox mail: Set a password to unlock articles. The form does not log you in β€” use the same email afterwards.

desktop bg dark

About Elena

Elena, a PhD in Computer Science, simplifies AI concepts and helps you use machine learning.






Citation
Elena Daehnhardt. (2026) 'Docker Compose CI/CD with GitHub Actions: A Reliable Flask Workflow', daehnhardt.com, 26 September 2026. Available at: https://daehnhardt.com/blog/2026/09/26/docker-compose-ci-cd-github-actions-flask/
All Posts