Skip to main content

Automated Deployments for Python Apps with GitHub Actions

Automated deployments transform your CI/CD pipeline from testing to live users. When tests pass on main, your workflow automatically deploys the latest code to staging or production, eliminating manual steps, reducing human error, and accelerating time-to-value.

This guide covers deploying Python web applications via SSH, containerized deployments with Docker, and cloud-native deployments to platforms like Heroku, AWS, and Google Cloud. You'll learn how to validate deployments, configure staging/production environments, and safely roll back on failure.

Deploying via SSH

SSH deployments pull the latest code from your repository and restart your application. This works for Python web apps running on VPS or dedicated servers.

First, add your server's private SSH key to GitHub Actions secrets as DEPLOY_KEY:

name: Deploy Python Web App

on:
push:
branches:
- main

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install and test
run: |
pip install -r requirements.txt
pytest tests/

deploy:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Deploy via SSH
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.DEPLOY_HOST }}
username: ${{ secrets.DEPLOY_USER }}
key: ${{ secrets.DEPLOY_KEY }}
script: |
cd /var/www/myapp
git pull origin main
pip install -r requirements.txt
systemctl restart myapp

This workflow:

  1. Runs tests on push to main
  2. If tests pass, deploys by pulling code and restarting the service
  3. Uses the appleboy/ssh-action action to execute commands remotely

The server needs systemctl to manage the service. For other server types (gunicorn, FastAPI), adjust the restart command.

Docker-Based Deployments

Docker containers package your Python app with all dependencies. Build and push a Docker image to a container registry, then pull and run it on your deployment platform.

Create a Dockerfile:

FROM python:3.11-slim

WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .
CMD ["python", "-m", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

Then deploy in your workflow:

- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: |
ghcr.io/${{ github.repository }}:latest
ghcr.io/${{ github.repository }}:${{ github.sha }}
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Deploy to server
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.DEPLOY_HOST }}
username: ${{ secrets.DEPLOY_USER }}
key: ${{ secrets.DEPLOY_KEY }}
script: |
docker pull ghcr.io/myuser/myapp:latest
docker stop myapp || true
docker run -d --name myapp -p 8000:8000 \
ghcr.io/myuser/myapp:latest

This builds a Docker image, pushes it to GitHub Container Registry (GHCR), then pulls and runs it on your server.

Deploying to Heroku

Heroku is a PaaS (Platform as a Service) that handles infrastructure for you. Deploying is as simple as pushing to a Heroku-managed git remote:

- name: Deploy to Heroku
uses: akhileshns/heroku-[email protected]
with:
heroku_api_key: ${{ secrets.HEROKU_API_KEY }}
heroku_app_name: ${{ secrets.HEROKU_APP_NAME }}
heroku_email: ${{ secrets.HEROKU_EMAIL }}

Create a Heroku account, create an app, and generate an API key. Add the key as a GitHub secret. Heroku automatically runs pip install -r requirements.txt and starts your app according to Procfile:

web: python -m gunicorn main:app

Deploying to AWS

AWS Lambda runs Python functions serverlessly (pay per invocation, no servers to manage). Deploy using serverless framework:

- name: Deploy to AWS Lambda
uses: serverless/github-action@v3
with:
args: deploy
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_REGION: us-east-1

Configure serverless.yml:

service: myapp

provider:
name: aws
runtime: python3.11
region: us-east-1

functions:
api:
handler: main.handler
events:
- http:
path: api/{proxy+}
method: ANY

This packages your Python app and deploys to Lambda, creating an HTTPS endpoint automatically.

Health Checks and Rollback

Always verify deployments succeeded:

- name: Health check
run: |
sleep 10 # Wait for deployment
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" https://myapp.example.com)
if [ "$HTTP_CODE" != "200" ]; then
echo "Health check failed (HTTP $HTTP_CODE)"
exit 1
fi

If health checks fail, trigger a rollback:

- name: Rollback on failure
if: failure()
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.DEPLOY_HOST }}
username: ${{ secrets.DEPLOY_USER }}
key: ${{ secrets.DEPLOY_KEY }}
script: |
cd /var/www/myapp
git checkout main~1
systemctl restart myapp

This reverts to the previous commit if deployment fails.

Environment-Specific Deployments

Use conditional logic to deploy to staging on PR, production on main:

deploy-staging:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- name: Deploy to staging
run: |
curl -X POST https://staging-server.example.com/deploy

deploy-production:
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
runs-on: ubuntu-latest
steps:
- name: Deploy to production
run: |
curl -X POST https://production-server.example.com/deploy

This pattern ensures pull requests deploy to staging for testing before merging to main, which triggers production deployment.

Key Takeaways

  • Use SSH deployments for simple VPS; pull code and restart the service.
  • Docker deployments are reproducible; build once, run anywhere.
  • Heroku simplifies Python app deployment; create an app and configure Procfile.
  • AWS Lambda runs serverless Python functions; pay only for execution time.
  • Always include health checks to verify deployment success.
  • Implement rollback mechanisms to revert failed deployments automatically.
  • Use conditional logic to deploy staging on PR, production on main.

Frequently Asked Questions

What happens if my deployment fails mid-way?

If a step fails, the workflow stops and alerts you (red X on the commit). Implement health checks and rollback logic to automatically revert on failure. For critical systems, require manual approval before production deploys.

How do I deploy without downtime?

Use blue-green deployments: run two identical environments (blue and green), switch traffic from one to the other. Only applicable to load-balanced systems. For simple apps, accept brief downtime during restart.

Can I deploy to multiple servers in parallel?

Yes, use a matrix to deploy to multiple servers simultaneously:

deploy:
strategy:
matrix:
server: [server1, server2, server3]
steps:
- uses: appleboy/ssh-action@v1
with:
host: ${{ matrix.server }}

Should I deploy on every push or only on tags?

Deploy main branch to staging continuously; production on version tags. This gives you staging testing before production release.

How do I handle database migrations in deployments?

Run migrations before restarting:

- name: Run migrations
run: python manage.py migrate
- name: Restart service
run: systemctl restart myapp

Test migrations thoroughly; a failed migration can break production.

Further Reading