Deploy a Scalable Flask App with Docker and GitHub Actions

You’ve probably heard the buzz about “container‑based deployments” and “CI/CD pipelines” and wondered if they’re worth the hype. The truth is, they’re not just buzz – they save you hours of manual work and make your app ready for traffic spikes without a panic‑inducing rewrite. In this post I’ll walk you through a real‑world, step‑by‑step way to get a Python Flask app running in Docker and automatically pushed to production with GitHub Actions. No fluff, just the bits that matter.

Why Docker and CI/CD matter now

When I first moved a hobby project to a cloud VM, I spent an entire weekend fighting “it works on my machine” bugs. Docker solves that by packaging everything – Python, libraries, OS bits – into a single image that runs the same everywhere. GitHub Actions adds the “push‑to‑deploy” part: every time you push code, the pipeline builds a fresh Docker image and ships it to your host. The result? A repeatable, reliable process that scales as your user base grows.

Prerequisites

Before we dive in, make sure you have:

  • A recent version of Docker Desktop (or Docker Engine) installed locally.
  • A GitHub account and the Git command line tool.
  • Basic familiarity with Flask – a simple “Hello, World!” app is enough.
  • Optional but handy: a free container registry like Docker Hub or GitHub Packages.

If any of these sound unfamiliar, pause and get them set up first. Trust me, the later steps will be smoother.

Step 1: Set up a simple Flask app

Create a new folder called flask‑app and add a file named app.py:

from flask import Flask

app = Flask(__name__)

@app.route("/")
def home():
    return "Hello from Flask in Docker!"

if __name__ == "__main__":
    # Use 0.0.0.0 so Docker can expose the port
    app.run(host="0.0.0.0", port=5000)

Next, add a requirements.txt file so Docker knows what to install:

Flask==2.3.2

That’s it – a minimal Flask service that listens on port 5000.

Step 2: Write a Dockerfile

Inside the same folder, create a file called Dockerfile (no extension). This file tells Docker how to build the image.

# Use the official lightweight Python image
FROM python:3.11-slim

# Set a working directory inside the container
WORKDIR /app

# Copy only the requirements first (helps caching)
COPY requirements.txt .

# Install Python dependencies
RUN pip install --no-cache-dir -r requirements.txt

# Copy the rest of the code
COPY . .

# Expose the Flask port
EXPOSE 5000

# Command to run when the container starts
CMD ["python", "app.py"]

A couple of notes:

  • python:3.11-slim is a small base image that keeps the final size low.
  • The WORKDIR line creates a folder /app inside the container and makes it the default location for subsequent commands.
  • By copying requirements.txt before the source code, Docker can reuse the layer when you only change your app logic, speeding up builds.

Step 3: Test locally

Open a terminal, navigate to flask‑app, and run:

docker build -t myflask:dev .
docker run -p 5000:5000 myflask:dev

Visit http://localhost:5000 in your browser. You should see Hello from Flask in Docker! If you do, congratulations – your container works.

Step 4: Create a GitHub repo

Push the folder to a new GitHub repository. Here’s a quick cheat sheet:

git init
git add .
git commit -m "Initial Flask app with Dockerfile"
git branch -M main
git remote add origin https://github.com/youruser/flask-docker-ci.git
git push -u origin main

Replace youruser with your actual GitHub username. Once the code lives on GitHub, we can hook up the automation.

Step 5: Add a GitHub Actions workflow

In the repo, create a directory .github/workflows and add a file docker-ci.yml:

name: Build and Push Docker Image

on:
  push:
    branches: [ main ]

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      # Checkout the repo
      - name: Checkout code
        uses: actions/checkout@v3

      # Log in to Docker Hub (or GitHub Packages)
      - name: Log in to Docker Hub
        uses: docker/login-action@v2
        with:
          username: ${{ secrets.DOCKERHUB_USER }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}

      # Build the Docker image
      - name: Build image
        run: |
          docker build -t ${{ secrets.DOCKERHUB_USER }}/flask-app:${{ github.sha }} .
      
      # Push the image
      - name: Push image
        run: |
          docker push ${{ secrets.DOCKERHUB_USER }}/flask-app:${{ github.sha }}

A few things to unpack:

  • The workflow triggers on every push to main.
  • It checks out the code, logs into Docker Hub using secrets, builds the image, tags it with the commit SHA, and pushes it.
  • You need to add two secrets in your repo settings: DOCKERHUB_USER (your Docker Hub username) and DOCKERHUB_TOKEN (a personal access token with write permissions).

If you prefer GitHub Packages, replace the login step with docker/login-action@v2 using registry: ghcr.io and adjust the image name accordingly.

Commit the workflow file and push. GitHub will spin up a runner, build the image, and push it. You can watch the progress under the “Actions” tab.

Step 6: Deploy to a container service

Now that the image lives in a registry, you need a place to run it. For a quick demo, I like Render because it offers a free Docker service with automatic HTTPS.

  1. Sign up at render.com and click “New Web Service”.
  2. Choose “Docker” as the environment.
  3. In the “Docker Image URL” field, paste docker.io/youruser/flask-app@sha256:<image‑digest> – you can find the digest on Docker Hub after the push.
  4. Set the port to 5000 (the same as in the Dockerfile).
  5. Click “Create Web Service”.

Render pulls the image, starts a container, and gives you a public URL. Visit that URL – you should see the same “Hello from Flask in Docker!” message, now served from the cloud.

If you prefer a more hands‑on approach, spin up a small VM on DigitalOcean, install Docker, and run:

docker pull youruser/flask-app:<sha>
docker run -d -p 80:5000 youruser/flask-app:<sha>

Now traffic hitting port 80 on the VM is forwarded to your Flask app.

Tips for scaling

  • Stateless design – Keep your Flask routes free of in‑memory state. Use a database (PostgreSQL, MySQL) or a cache (Redis) for shared data.
  • Multiple replicas – Services like Render, Fly.io, or Kubernetes let you run several containers behind a load balancer. The Docker image you built works the same for each replica.
  • Health checks – Add a simple /health endpoint that returns 200 OK. Most platforms can ping this to know when a container is ready.
  • Environment variables – Store secrets (DB passwords, API keys) in the platform’s secret manager, not in code. In Flask you can read them with os.getenv.
  • Logging – Docker sends stdout/stderr to the host. Most cloud providers capture these logs automatically, making debugging easier.

By keeping the app stateless and using Docker, you can add more containers whenever traffic spikes, without touching the code again. The GitHub Actions pipeline guarantees every new version is built the same way, so you avoid “it works locally but not in prod” surprises.


Reactions
Do you have any feedback or ideas on how we can improve this page?