This guide walks you through deploying a production-ready Flask application using Laravel Forge, GitHub Actions, and Supervisor — with automatic database migrations, SSH deployment, and secure environment variable management.
We’ll treat this as a fresh project with new migrations.
1.Provision Your Server
In Laravel Forge:
- Create a new server (Ubuntu 22+ recommended)
- Enable SSH access for your GitHub Actions
- Add a site (e.g., api.yourapp.com)
- Clone your Git repo (use "Deploy Script" toggle OFF for now)
On the server:
ssh forge@your-server-ip
cd /home/forge/api.yourapp.com
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
2.Project Structure & Entry Point
Your main app should have a run.py file:
from app import create_app
app = create_app()
if __name__ == '__main__':
app.run()
Use Flask-Migrate and SQLAlchemy in app/init.py:
from flask_migrate import Migrate
from flask_sqlalchemy import SQLAlchemy
from dotenv import load_dotenv
import os
load_dotenv()
db = SQLAlchemy()
migrate = Migrate()
def create_app():
app = Flask(__name__)
app.config.from_object('config.Config')
db.init_app(app)
migrate.init_app(app, db)
return app
- GitHub Actions Deployment
Create .github/workflows/deploy.yml:
name: Deploy to Forge
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Deploy over SSH
uses: appleboy/[email protected]
with:
host: ${{ secrets.FORGE_HOST }}
username: forge
key: ${{ secrets.FORGE_SSH_KEY }}
script: |
cd /home/forge/api.yourapp.com
git pull origin main
source venv/bin/activate
pip install -r requirements.txt
flask db upgrade
sudo supervisorctl restart flask-app
Important: Add the Forge server's SSH private key (id_rsa) to GitHub Secrets as FORGE_SSH_KEY.
- Managing Environment Variables
On the Forge server:
Add a .env file in your project root (/home/forge/api.yourapp.com/.env):
FLASK_ENV=production
SECRET_KEY=supersecret
DATABASE_URL=mysql+pymysql://user:pass@host:port/db
MAILGUN_DOMAIN=mg.yourapp.com
MAILGUN_API_KEY=your-mailgun-key
In app/init.py:
from dotenv import load_dotenv
load_dotenv(dotenv_path='/home/forge/api.yourapp.com/.env')
Confirm values load with:
flask shell
>>> import os
>>> os.getenv("MAILGUN_DOMAIN")
- Running Migrations on Deploy
When you push code with new migrations:
flask db migrate -m "Add new table"
git add migrations/
git commit -m "Add new table"
git push
GitHub Actions will:
- Pull the latest code
- Install dependencies
- Run flask db upgrade live
- Restart Gunicorn
- Supervisor Configuration
Set up supervisor to run your Flask app:
sudo nano /etc/supervisor/conf.d/flask-app.conf
[program:flask-app]
command=/home/forge/api.yourapp.com/venv/bin/gunicorn run:app
user=forge
directory=/home/forge/api.yourapp.com
autostart=true
autorestart=true
stdout_logfile=/var/log/supervisor/flask-app.log
stderr_logfile=/var/log/supervisor/flask-app.err.log
environment=FLASK_ENV="production"
Then reload:
sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl restart flask-app
You Did It
You now have a full CI/CD pipeline:
GitHub commit → GitHub Actions → SSH to Forge → Pull, Migrate, Restart
New tables are deployed automatically
Environment is secure