DEV Community

Tanay Jain
Tanay Jain

Posted on

My CI Pipeline Failed on the First Push. Here's What I Learned.

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"
Enter fullscreen mode Exit fullscreen mode

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/*
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

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)