My CI pipeline failed on the first push.
Not because of a big mistake.
Because of a layer conflict in my Dockerfile that
only showed up in a clean environment.
Locally — everything worked.
CI — red on the first run.
That one failure taught me more about Docker
than a week of tutorials.
Here's exactly what I built and what happened.
The Project
A Flask + PostgreSQL app running in Docker Compose.
It tracks page visits and stores the count
in a real PostgreSQL database.
Browser → Flask Container → PostgreSQL → pgdata Volume
Multi-container setup. Persistent storage.
Environment variables via .env.
Health checks and restart policies.
Every decision is production-style —
even though it's a learning project.
Why I Added CI
Before CI, my workflow looked like this:
Make a change
↓
docker compose down
↓
docker compose build
↓
docker compose up -d
↓
Open browser, check manually
↓
Repeat
This works for one or two changes.
But when you're pushing regularly —
manually checking every time is not sustainable.
I wanted the system to tell me if something broke.
Not discover it later.
What I Built
.github/workflows/docker-build.yml
name: Docker Build CI
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Setup Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Build Docker image
run: docker build -t flask-app:test .
- name: Verify image exists
run: docker images | grep flask-app
- name: Run container test
run: |
docker run --rm flask-app:test python -c "
import flask
import psycopg2
print('Flask:', flask.__version__)
print('All imports OK')
"
- name: Check image size
run: |
SIZE=$(docker image inspect flask-app:test \
--format='{{.Size}}')
echo "Image size: $SIZE bytes"
echo "Build verified"
What Each Step Does
Checkout — GitHub's server downloads my code
Docker Buildx setup — Prepares build tools on the runner
Build image — Runs my Dockerfile from scratch,
clean environment every time
Verify image — Confirms image was actually created
Container test — Runs the container,
checks Flask and psycopg2 import correctly
Size check — Tracks image size on every build
The First Run Failed
Pushed the code. Opened Actions tab. Red.
The error was in my Dockerfile.
I had this:
RUN apt-get update && apt-get install -y curl \
&& rm -rf /var/lib/apt/lists/*
The layer order was causing a conflict
in the clean CI environment that
didn't show up on my machine.
Reordered the layers. Pushed again. Green.
This is the whole point of CI.
Your local machine has cached layers,
existing images, leftover containers.
CI starts completely fresh every time.
It catches what you miss.
The Health Endpoint
I also added a /health route to Flask:
@app.route("/health")
def health():
try:
conn = get_connection()
conn.close()
return {"status": "healthy", "database": "connected"}, 200
except Exception as e:
return {"status": "unhealthy", "error": str(e)}, 500
A running container doesn't mean a working app.
This endpoint checks actual database connectivity.
200 = Flask is up and DB is reachable.
500 = Something broke inside.
Real monitoring tools and load balancers
use exactly this kind of endpoint.
Architecture

GitHub Actions CI:
Code Push → Build → Test → Validate
↓
Docker Hub (image stored)
↓
AWS EC2 (deployed)
↓
Flask Container ←→ PostgreSQL Container
↓
pgdata Volume
Project Versioning
v1.0 — Flask + PostgreSQL + Docker + EC2 deploy
v1.1 — CI pipeline + /health endpoint added
Tagging versions made me treat this
like a real product — not just practice code.
What Actually Changed
Before:
Push code → manually test → hope for the best.
After:
Push code → pipeline runs in ~2 minutes → green or red.
No guessing. No "I think it should work."
The pipeline knows.
Three Things CI Taught Me Practically
1. Clean environment matters
Your machine lies to you.
Cached layers, existing images, leftover state —
none of that exists in CI.
If it works locally but fails in CI,
the CI is right.
2. The pipeline is documentation
Anyone reading the workflow file
understands exactly how the project builds.
No README needed for that part.
3. Start CI early
Adding it to an existing project is harder
than starting with it.
Next project — pipeline goes in on day one.
Links
GitHub: https://github.com/tj2905
Docker Hub: docker pull tanayjain29/flask-devops-app:v1.0
Learning by building real things.
Turning ideas into working projects.
Sharing everything on GitHub.
Top comments (0)