Deploy a Python Flask App with GitLab CI/CD and Heroku: A Step-by-Step Guide

Deploy a Python Flask App with GitLab CI/CD and Heroku: A Step-by-Step Guide

Shipping a Flask API to a real public URL used to be a Friday-afternoon task. With GitLab CI/CD and Heroku, you can fully automate it: every push to dev deploys to staging, every push to main deploys to production. This guide walks through the full pipeline — Flask setup, Gunicorn, the Procfile, GitLab CI variables, Heroku apps, and environment variables — so you finish with two automatically-deployed environments.

Heads up (2026): Heroku ended its free tier in late 2022, so you’ll need a paid Eco or Basic dyno (~$5/mo). If you’d rather use a free or pay-as-you-go alternative, see the modern alternatives section near the end — the GitLab pipeline pattern stays the same.

The toolchain at a glance

  • VS Code — code editor.
  • Flask — minimal Python web framework.
  • Gunicorn — production WSGI server that runs your Flask app on Heroku.
  • Postman — API testing.
  • GitLab — version control and CI/CD pipelines.
  • Heroku — PaaS that runs your app from a Procfile.

1. Build a minimal Flask API

The example app exposes two endpoints: a ping health check and a salute greeting. It also reads an ENV variable so we can confirm staging vs production after deploy.

import os
from dotenv import load_dotenv
from flask import Flask, jsonify

server = Flask(__name__)

@server.route("/api/ping", methods=["GET"])
def api_ping():
    return jsonify({"message": "pong"})

@server.route("/api/salute/<string:name>", methods=["GET"])
def api_salute(name: str):
    env = os.getenv("ENV")
    return jsonify({"message": f"Hi {name}!", "env": env})

if __name__ == "__main__":
    load_dotenv()
    server.run(host="localhost", port=8000, debug=True)

Local .env:

ENV="Hello from Local"

Run with python app.py and hit http://localhost:8000/api/ping to confirm it works before going further.

2. Add a Procfile and requirements.txt

Heroku reads a Procfile (no extension) at the project root to know how to start your app. We’ll use Gunicorn:

# Procfile
web: gunicorn 'app:server'

The pattern is filename:flask_instance. Since our file is app.py and our Flask instance is server, we use app:server.

Pin your dependencies in requirements.txt so Heroku installs the exact versions:

flask>=3.0
gunicorn>=21.0
python-dotenv>=1.0

Optionally pin Python with a runtime.txt (e.g. python-3.12.3) so Heroku doesn’t surprise-upgrade you.

3. Configure GitLab CI/CD

Create .gitlab-ci.yml at the project root. We’ll deploy dev branches to staging and main to production using dpl (Travis’s deploy tool — works fine in any CI).

stages:
  - deploy

.deploy_template: &deploy_template
  image: ruby:latest
  before_script:
    - apt-get update -qy
    - apt-get install -y ruby-dev
    - gem install dpl

staging:
  <<: *deploy_template
  stage: deploy
  script:
    - dpl --provider=heroku --app=$HEROKU_APP_DEV --api-key=$HEROKU_API_KEY
  only:
    - dev

production:
  <<: *deploy_template
  stage: deploy
  script:
    - dpl --provider=heroku --app=$HEROKU_APP_PROD --api-key=$HEROKU_API_KEY
  only:
    - main
BranchDeploys to
devStaging Heroku app
mainProduction Heroku app

4. Create the Heroku apps

In your Heroku dashboard, create two apps — one for staging, one for production. Suggested naming:

App namePurpose
hhsm-webapp-stagingStaging
hhsm-webappProduction

Set environment variables on each app

For each app, go to Settings → Config Vars → Reveal Config Vars and add:

  • Staging app: ENV=Hello from Staging
  • Production app: ENV=Hello from Production

In real apps these would be database URLs, secret keys, third-party API tokens, etc. Never commit them to git — always inject through Config Vars.

Get the Heroku API key

Go to Account settings → API Key and reveal/copy it. We’ll paste it into GitLab next.

5. Set GitLab CI variables and protect branches

In GitLab → Settings → Repository → Protected branches, protect dev (the same way main is by default). Then in Settings → CI/CD → Variables, add three variables (mark them Masked if available):

  • HEROKU_API_KEY — the key copied above.
  • HEROKU_APP_DEV — e.g. hhsm-webapp-staging.
  • HEROKU_APP_PROD — e.g. hhsm-webapp.

6. Push and verify

Push dev and main. The GitLab pipeline runs automatically; once it shows green, open the app in Heroku and test:

curl https://hhsm-webapp-staging.herokuapp.com/api/ping
# {"message": "pong"}

curl https://hhsm-webapp.herokuapp.com/api/salute/Hector
# {"message": "Hi Hector!", "env": "Hello from Production"}

The env field tells you which app served the request — confirming staging and production are properly isolated.

Modern alternatives to Heroku

Since Heroku removed its free tier, several platforms have become attractive for hobby and small-scale projects. The GitLab CI pattern stays the same — only the deploy command changes:

PlatformFree tierBest for
Fly.ioGenerous (Dockerfile-based)Global edge deploys
RenderFree web services with sleepHeroku-style simplicity
RailwayTrial credit / pay-as-you-goQuick MVPs
AWS App Runner / LightsailPay-as-you-goAWS-native stacks
Heroku Eco~$5/moSticking with Procfile workflow

Final thoughts

Once your CI pipeline is wired up, deployment becomes a non-event: push code, watch the pipeline turn green, refresh the live URL. That feedback loop is what unblocks fast iteration on side projects and small teams alike.

The full repo is on GitLab. If you’re curious about exposing your APIs to a wider developer audience after deploy, see How to get more exposure for your API.

5 2 votes
Article Rating
Subscribe
Notify of
guest
0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments

Pin It on Pinterest

0
Would love your thoughts, please comment.x
()
x