Continuous Deployment of a Python Flask Application with Docker and Semaphore CI

DevOpsDevOps News

YOUR COSMETIC CARE STARTS HERE

Find the Best Cosmetic Hospitals

Trusted • Curated • Easy

Looking for the right place for a cosmetic procedure? Explore top cosmetic hospitals in one place and choose with confidence.

“Small steps lead to big changes — today is a perfect day to begin.”

Explore Cosmetic Hospitals Compare hospitals, services & options quickly.

✓ Shortlist providers • ✓ Review options • ✓ Take the next step with confidence

This tutorial walks through building a simple Python Flask application, containerizing it with Docker, and wiring up a full CI/CD pipeline using Semaphore that automatically tests and deploys the app to Heroku on every push.

Attribution: This article is based on a tutorial originally published on semaphoreci.com.

Why Combine CI/CD, Docker, and Flask?

Continuous integration and deployment solve several real pain points for development teams:

  • Developers focus on writing features rather than manually shepherding builds to production.
  • Every commit is verified against automated tests before it reaches users.
  • Git history gives you an audit trail and a clear rollback path.
  • The classic “it works on my machine” problem disappears when every environment runs the same container image.

Docker reinforces these benefits by packaging the application and its runtime dependencies into a single, portable image. Key advantages include:

  • Identical runtime environments for every developer on the team.
  • No bleed between OS-level packages and application dependencies.
  • Improved portability — the same image runs locally, in CI, and in production.
  • Lower overhead compared to full virtual machines, because only the required runtime is bundled, not an entire OS.

Prerequisites

Make sure the following are available before you start:

  • Python 2.7 or 3.x (the Dockerfile example below targets Python 2.7; update the base image tag for Python 3).
  • Docker and Docker Compose installed locally.
  • A GitHub or Bitbucket repository to host the project.
  • A Semaphore account (free tier is sufficient for this tutorial).
  • A Heroku account for deployment.

Project Structure

The finished application uses the following directory layout:

app/
├── templates/
│   └── index.html
├── tests/
│   └── test_endpoints.py
├── app.py
├── Dockerfile
├── docker-compose.yml
└── requirements.txt

app.py contains all routing logic and renders HTML templates stored in the templates/ directory. Tests live in tests/ and are isolated from the main application code.

Setting Up the Python Environment

Create and activate a virtual environment, then install dependencies. Using a virtual environment keeps the project’s packages separate from system-level Python packages.

$ virtualenv flask-env
$ . flask-env/bin/activate
$ pip install -r requirements.txt

A typical requirements.txt for this project includes Flask, PyMongo, and a test runner such as nose:

Flask
pymongo
nose

Writing Application Tests

Test-Driven Development (TDD) encourages you to define expected behaviour before writing the implementation. It also replaces repetitive manual testing with an automated suite that runs on every build.

The test class uses setUp and tearDown methods so each test runs against a fresh state. setUp creates a Flask test client, and tearDown clears the database collection so data from one test cannot influence another.

# tests/test_endpoints.py

from app import app, db
from flask import url_for
import unittest


class FlaskTodosTest(unittest.TestCase):

    def setUp(self):
        """Set up test application client."""
        self.app = app.test_client()
        self.app.testing = True

    def tearDown(self):
        """Clear DB after running tests."""
        db.todos.remove({})

    def test_home_status_code(self):
        """Assert that the homepage returns HTTP 200."""
        result = self.app.get('/')
        self.assertEqual(result.status_code, 200)

    def test_todo_creation(self):
        """Assert that a POST to /new redirects with HTTP 302."""
        response = self.app.post(
            '/new',
            data=dict(name="First todo", description="Test todo")
        )
        self.assertEqual(response.status_code, 302)


if __name__ == '__main__':
    unittest.main()

Run the test suite locally with:

$ nosetests -v

Building the Flask Application

The application connects to a MongoDB instance (hosted on mLab in the original tutorial, though any MongoDB URI works) and exposes two routes:

  • GET / — Fetches all todo items from the database and renders the index.html template.
  • POST /new — Accepts form data, inserts a new todo document, then redirects back to /.
# app.py

import os
from flask import Flask, redirect, url_for, request, render_template
from pymongo import MongoClient

app = Flask(__name__)

# Replace with your actual MongoDB connection string.
client = MongoClient(
    "mongodb://username:password@database_url:port_number/db_name")
db = client['db_name']


@app.route('/')
def todo():
    _items = db.todos.find()
    items = [item for item in _items]
    return render_template('index.html', items=items)


@app.route('/new', methods=['POST'])
def new():
    item_doc = {
        'name': request.form['name'],
        'description': request.form['description']
    }
    db.todos.insert_one(item_doc)
    return redirect(url_for('todo'))


if __name__ == "__main__":
    app.run(host='0.0.0.0', debug=True)Code language: PHP (php)

Start the development server with python app.py and open http://127.0.0.1:5000 in your browser. Flask defaults to port 5000. To override the port, pass it explicitly:

app.run(host='0.0.0.0', port=8080, debug=True)Code language: PHP (php)

Security note: Do not commit real database credentials to version control. Use environment variables or a secrets manager and read them with os.environ.get().

Dockerizing the Application

Writing the Dockerfile

The Dockerfile defines the container image for the application:

FROM python:2.7
ADD . /todo
WORKDIR /todo
EXPOSE 5000
RUN pip install -r requirements.txt
ENTRYPOINT ["python", "app.py"]Code language: JavaScript (javascript)

What each instruction does:

  • FROM python:2.7 — Starts from an official Python 2.7 base image. Change to python:3.11-slim (or another 3.x tag) for modern projects.
  • ADD . /todo — Copies the project files into a /todo directory inside the container.
  • WORKDIR /todo — Sets /todo as the working directory for subsequent commands.
  • EXPOSE 5000 — Documents that the container listens on port 5000.
  • RUN pip install -r requirements.txt — Installs Python dependencies inside the container.
  • ENTRYPOINT — Runs python app.py when the container starts.

Writing the Docker Compose File

Docker Compose simplifies building and running the container with a single command by codifying the configuration in docker-compose.yml:

web:
  build: .
  container_name: flock
  ports:
    - "5000:5000"
  volumes:
    - .:/todoCode language: JavaScript (javascript)
  • build: . — Builds the image from the Dockerfile in the current directory.
  • ports — Maps container port 5000 to host port 5000.
  • volumes — Mounts the project directory into the container, so code changes are reflected without rebuilding the image during development.

Build and run the container:

$ docker-compose build
$ docker-compose up

Docker pulls the base image, installs dependencies, and starts the application. It is then accessible at http://127.0.0.1:5000.

Setting Up the CI/CD Pipeline with Semaphore

With the application tested and containerized locally, the next step is to automate the build, test, and deployment cycle using Semaphore CI.

Creating a New Semaphore Project

Log in to your Semaphore account, click Create new, and select Project from the dropdown. On the next screen, choose whether your repository is hosted on GitHub or Bitbucket.

Semaphore: choose repository host

Search for and select your project repository:

Semaphore: select project repository

Select the branch to build (typically main or master):

Semaphore: choose branch

Platform Detection and Build Configuration

Semaphore analyses the repository and detects the language and platform automatically.

Semaphore: analysing repository
Semaphore: detecting platform

When a Dockerfile is present, Semaphore recommends the Docker platform. You then configure the commands Semaphore should run — typically docker-compose build followed by the test command. Semaphore will refuse to proceed to deployment if any test fails.

Semaphore: running tests

Deploying to Heroku

Once the build and tests pass, click Set Up Deployment and choose Heroku as the target platform.

Semaphore: tests passed
Semaphore: configure deployment

After entering your Heroku API key and target application name, Semaphore completes the first deployment:

Semaphore: Heroku deployment complete

Enable automatic deployment so that every subsequent push to the configured branch triggers a build and, if tests pass, an automatic deployment. The first deployment is always triggered manually.

Semaphore: automatic deployment configured

The application is now live on Heroku:

Flask todo application running on Heroku

Conclusion

This tutorial covered the full journey from a local Flask application to an automated deployment pipeline:

  • Built a simple Flask todo application backed by MongoDB.
  • Wrote unit tests using Python’s built-in unittest module and the Flask test client.
  • Containerized the application with a Dockerfile and docker-compose.yml.
  • Connected the repository to Semaphore to automate builds and testing.
  • Configured automatic deployment to Heroku, triggered on every successful build.

The result is a pipeline where pushing code is the only manual step. Semaphore handles the rest: building the Docker image, running tests, and shipping to production only when everything passes.

0 0 votes
Article Rating
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Oldest
Newest Most Voted
0
Would love your thoughts, please comment.x