Aman Xavier

Tutorials · 3 min read · 13 October 2026

Deploy Django with Docker and GitHub Actions

Push to main and your Django app rebuilds and redeploys itself. A Dockerfile, a compose file and one GitHub Actions workflow.

By the end of this, every push to main builds a fresh Docker image of your Django app, pushes it to GitHub's container registry, and restarts it on your server with migrations applied. No more SSH-ing in to git pull by hand.

What you need

  • A Django project with a requirements.txt that includes gunicorn.
  • A Linux server (any VPS) with Docker and the Docker Compose plugin installed.
  • A GitHub repository for the project.

1. The Dockerfile

Put this in the project root. Change config.wsgi to your project's WSGI module.

dockerfile
FROM python:3.12-slim

ENV PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1

WORKDIR /app

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

COPY . .

# Collect static files into the image (needs STATIC_ROOT set in settings)
RUN python manage.py collectstatic --noinput

EXPOSE 8000
CMD ["gunicorn", "config.wsgi:application", "--bind", "0.0.0.0:8000", "--workers", "3"]

Add a .dockerignore so you don't copy junk into the image:

text
.git
.env
__pycache__/
*.pyc
venv/
node_modules/
media/

2. The compose file on the server

On your server, create a folder such as /srv/myapp with this docker-compose.yml and a .env file holding your secrets. Replace the image name with your own, in lowercase.

yaml
services:
  web:
    image: ghcr.io/yourname/myapp:latest
    env_file: .env
    ports:
      - "8000:8000"
    depends_on:
      - db
    restart: unless-stopped

  db:
    image: postgres:16
    environment:
      POSTGRES_DB: myapp
      POSTGRES_USER: myapp
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    volumes:
      - pgdata:/var/lib/postgresql/data
    restart: unless-stopped

volumes:
  pgdata:

Put a reverse proxy such as Nginx or Caddy in front of port 8000 for HTTPS. Caddy is the quickest, because it gets certificates for you automatically.

3. Repository secrets

In your GitHub repo, go to Settings, then Secrets and variables, then Actions, and add:

  • SERVER_HOST: your server's IP or domain.
  • SERVER_USER: the user that runs Docker on the server.
  • SERVER_SSH_KEY: a private SSH key whose public half is in that user's ~/.ssh/authorized_keys.

4. The workflow

Save this as .github/workflows/deploy.yml.

yaml
name: Deploy

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
    steps:
      - uses: actions/checkout@v4

      - uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - uses: docker/build-push-action@v6
        with:
          context: .
          push: true
          tags: ghcr.io/yourname/myapp:latest

      - uses: appleboy/ssh-action@v1
        with:
          host: ${{ secrets.SERVER_HOST }}
          username: ${{ secrets.SERVER_USER }}
          key: ${{ secrets.SERVER_SSH_KEY }}
          script: |
            cd /srv/myapp
            docker compose pull web
            docker compose up -d web
            docker compose exec -T web python manage.py migrate --noinput

Image names on GitHub's registry must be lowercase. If your GitHub username has capitals, type the lowercase name into tags yourself rather than building it from github.repository.

5. Let the server pull the image

If the package is private, log the server in to the registry once, using a GitHub personal access token with the read:packages scope:

bash
echo YOUR_TOKEN | docker login ghcr.io -u yourname --password-stdin

Push and watch it deploy

Commit everything and push to main. Open the Actions tab on GitHub and you'll see the build, the push and the deploy run in order. From now on, merging to main is your deploy button.

Common problems

  • collectstatic fails during the build: set STATIC_ROOT in settings, and make sure settings don't crash when secrets are missing at build time.
  • Permission denied on SSH: the public key isn't in authorized_keys for SERVER_USER, or the key secret has extra spaces.
  • Old version still running: check that the tags in the workflow and the image in compose are exactly the same name.

Work with me

Two ways to work together

If you run a business in Tricity

XavierStack builds websites, apps and booking systems for local businesses. That's the better door for you.

Go to xavierstack.com

If you're outside India

I take a few remote projects at a time. Django, Laravel, React, Next.js, and deployment, built end to end by the person who'll actually maintain it. I've shipped two live marketplaces, so I know what breaks once real users show up.

Projects start at $150. Also open to remote full-stack roles.

Tell me what you're building