MLOPS Tutorial- Automating Workflow Of CI/CD for Dockerized Flask App Using Github Action
Based on Krish Naik's video on YouTube. If you like this content, support the original creators by watching, liking and subscribing to their content.
A two-job GitHub Actions workflow enforces CI quality by running pytest before any Docker image is built or published.
Briefing
A complete CI/CD workflow for a Dockerized Flask app is built using GitHub Actions, with automated unit testing, Docker image creation, and publishing to Docker Hub. The core payoff is that every push to the main branch triggers a pipeline that validates the code with pytest, builds a fresh container image from a Dockerfile, and then pushes that image to Docker Hub using credentials stored as GitHub secrets—eliminating manual steps.
The setup starts with a minimal Flask application (an endpoint returning “Hello World”) and a matching pytest suite. Unit tests are organized so pytest automatically discovers them: test files are named starting with “test_”, and the example test imports the Flask `app` object and asserts both the HTTP status code (200) and the response body. Dependencies are captured in a `requirements.txt`, and a `.gitignore` prevents local virtual environment folders (created via `cond create -p vnv python=3.10` and activated with `cond activate vnv`) from being committed.
Next comes containerization. A Dockerfile uses a slim Python base image (`python:3.9-slim`), sets a working directory, copies the app code into the container, installs Flask (and relies on the container build to include what’s needed for running tests and the app), exposes port 5000, and starts the service with `python app.py`. This Dockerfile is essential because the CI job later needs to build the image inside a Linux runner.
The GitHub Actions pipeline is defined in a YAML workflow (named `cicd` in the example) under `.github/workflows/`. It triggers on pushes and pull requests targeting the `main` branch. Two jobs structure the automation: **build and test** runs on `ubuntu-latest`, checks out the repository, sets up Python 3.9, installs dependencies (including pytest), and executes `pytest` to validate the unit tests. A second job, **build and publish**, depends on the first job via `needs: build and test`, then sets up Docker Buildx, logs into Docker Hub, builds the Docker image, and pushes it.
Docker Hub authentication is handled securely. The workflow reads `DOCKER_USERNAME` and `DOCKER_PASSWORD` from GitHub Secrets. The password is generated on Docker Hub as a Personal Access Token with read/write/delete permissions, then stored in GitHub under repository secrets. During the push step, the image tag follows the Docker Hub naming convention: `<docker-username>/<image-name>:latest` (the example uses `krishn 06/sl flask test app:latest`, preserving the naming pattern used in the transcript).
A practical troubleshooting moment occurs when the Docker build fails with “failed to solve… no such file or directory” because the YAML didn’t specify the Dockerfile path. Adding `file: ./Dockerfile` (or the correct Dockerfile location) fixes the build. After successful runs, the published image can be pulled locally and executed with `docker run -p 5000:5000 <image>:latest`, returning “Hello World” from the container.
Finally, an optional third job is demonstrated to build a Docker image standalone (without publishing) to confirm Docker build correctness. The overall result is a developer workflow where code changes automatically trigger testing and container publishing, and broken Docker builds prevent bad images from reaching Docker Hub.
Cornell Notes
The workflow automates CI/CD for a Dockerized Flask app using GitHub Actions. On every push or pull request to the `main` branch, a **build and test** job runs on `ubuntu-latest`, sets up Python 3.9, installs dependencies, and executes `pytest` to validate the Flask endpoint behavior. A dependent **build and publish** job then builds a Docker image from the Dockerfile and pushes it to Docker Hub, using `DOCKER_USERNAME` and `DOCKER_PASSWORD` stored as GitHub Secrets (with the password coming from a Docker Hub Personal Access Token). The pipeline ensures only code that passes tests produces and publishes a new container image. A common failure—Dockerfile not found in the build step—is resolved by specifying the Dockerfile path in the YAML.
How does the pipeline ensure code quality before publishing a Docker image?
What file and naming conventions make pytest automatically find unit tests in this setup?
What does the Dockerfile do, and why is it central to the CI/CD pipeline?
How are Docker Hub credentials handled securely in GitHub Actions?
Why did the Docker build fail once, and what fixed it?
How can someone verify the published image actually works after CI/CD runs?
Review Questions
- In the GitHub Actions workflow, what mechanism prevents the Docker image from being published when unit tests fail?
- Which pytest naming conventions are required for the unit tests to be discovered and executed by the CI job?
- What YAML parameter must be set to avoid Docker build errors when the Dockerfile path isn’t automatically detected?
Key Points
- 1
A two-job GitHub Actions workflow enforces CI quality by running pytest before any Docker image is built or published.
- 2
pytest discovery relies on test file naming (files starting with `test_`) and test functions that assert expected Flask responses.
- 3
The Dockerfile containerizes the Flask app using `python:3.9-slim`, exposes port 5000, and launches with `python app.py`.
- 4
Docker Hub publishing uses GitHub Secrets (`DOCKER_USERNAME`, `DOCKER_PASSWORD`) populated from a Docker Hub Personal Access Token.
- 5
The publish job depends on the test job via `needs`, preventing broken builds from reaching Docker Hub.
- 6
Docker build failures like “Dockerfile not found” are fixed by explicitly specifying the Dockerfile path in the YAML (e.g., `file: ./Dockerfile`).
- 7
Verification is done by pulling the pushed image and running it locally with port mapping to confirm the endpoint returns “Hello World.”