DEV Community

Aman Xavier
Aman Xavier

Posted on

Deploy Django with Docker and GitHub Actions

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"]
Enter fullscreen mode Exit fullscreen mode

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

text

.git
.env
__pycache__/
*.pyc
venv/
node_modules/
media/
Enter fullscreen mode Exit fullscreen mode
  1. 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:
Enter fullscreen mode Exit fullscreen mode

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.

  1. 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.
  1. The workflow

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

{% raw %}

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

{% endraw %}
Enter fullscreen mode Exit fullscreen mode

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.

  1. 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.

How do you handle deploys on your side projects? Still SSH and git pull, or something fancier? Tell me in the comments.

I write more of these at amanxavier.com, mostly about Django, Next.js and building products from India.

Top comments (0)