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.0Optionally 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| Branch | Deploys to |
|---|---|
dev | Staging Heroku app |
main | Production Heroku app |
4. Create the Heroku apps
In your Heroku dashboard, create two apps — one for staging, one for production. Suggested naming:
| App name | Purpose |
|---|---|
hhsm-webapp-staging | Staging |
hhsm-webapp | Production |
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:
| Platform | Free tier | Best for |
|---|---|---|
| Fly.io | Generous (Dockerfile-based) | Global edge deploys |
| Render | Free web services with sleep | Heroku-style simplicity |
| Railway | Trial credit / pay-as-you-go | Quick MVPs |
| AWS App Runner / Lightsail | Pay-as-you-go | AWS-native stacks |
| Heroku Eco | ~$5/mo | Sticking 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.