Deployment
CI/CD pipelines

Test and Deploy Django With Woodpecker CI

Set up Woodpecker CI, a lightweight self-hosted CI engine, for Django: write .woodpecker.yaml pipelines with a PostgreSQL service, run tests and linting, build and push images with the buildx plugin, deploy over SSH on main, and manage secrets.

Problem statement

You host your own Git server (Gitea, Forgejo, or GitLab) or want CI that runs entirely on your hardware with minimal resources. Woodpecker CI is a community fork of Drone: a small server plus agents that run each pipeline step in a container. A Django pipeline needs a PostgreSQL service for tests, an image build, a deploy step limited to the main branch, and secrets stored in Woodpecker rather than in the repository.

Django on Woodpecker.woodpecker.yamlsteps + servicesTest stepPostgres serviceBuild stepbuildx plugin pushDeploy stepmain only, SSHEvery step runs in its own container; services run beside them.
  1. Run the Woodpecker server and at least one agent, connected to your Git forge with OAuth.
  2. Add .woodpecker.yaml with a services entry for PostgreSQL and a test step using your Python image.
  3. Build and push the image with the woodpeckerci/plugin-docker-buildx plugin, tagged with ${CI_COMMIT_SHA}.
  4. Add a deploy step with when: branch: main, event: push that runs your deploy script over SSH.
  5. Store registry and SSH credentials as Woodpecker secrets, exposed with from_secret.

Step-by-step solution

1. Install Woodpecker

Run the server and agent with Docker Compose, pointing the server at your forge (Gitea, Forgejo, GitHub, GitLab, or Bitbucket) with OAuth credentials. Agents connect to the server with a shared secret and run steps using the Docker backend (or Kubernetes). Activate the repository in the Woodpecker UI.

2. Write the pipeline

# .woodpecker.yaml
when:
  - event: [push, pull_request]

services:
  - name: db
    image: postgres:17
    environment:
      POSTGRES_PASSWORD: postgres
      POSTGRES_DB: shop

steps:
  - name: test
    image: python:3.12-slim
    environment:
      DATABASE_URL: postgres://postgres:postgres@db:5432/shop
      DJANGO_SETTINGS_MODULE: config.settings.test
    commands:
      - pip install -r requirements.txt
      - for i in $(seq 1 30); do python -c "import os, psycopg; psycopg.connect(os.environ['DATABASE_URL']).close()" && break; sleep 1; done
      - python manage.py check --deploy --fail-level WARNING
      - python manage.py test --noinput

Services are reachable by name (db). The loop retries a real connection for up to 30 seconds, because the service container may still be starting when the step begins.

step and triggerImage or pluginRuns whenservice: dbpostgres:17every pipelinetestpython:3.12-slimpush and PRbuildplugin-docker-buildxpush to maindeployalpine + sshpush to mainPull requests run tests only; main also builds and deploysdjango-deployment.com
Secrets are never exposed to pull requests from forks.

3. Build and push the image

  - name: build
    image: woodpeckerci/plugin-docker-buildx
    settings:
      registry: git.example.com
      repo: git.example.com/acme/shop
      tags: [ "${CI_COMMIT_SHA}" ]
      cache_from: git.example.com/acme/shop:buildcache
      username: { from_secret: registry_user }
      password: { from_secret: registry_password }
    when:
      - branch: main
        event: push

Gitea and Forgejo include a container registry, so images can live next to the code.

4. Deploy on main

  - name: deploy
    image: alpine:3.20
    environment:
      SSH_KEY: { from_secret: deploy_ssh_key }
      KNOWN_HOSTS: { from_secret: deploy_known_hosts }
    commands:
      - apk add --no-cache openssh-client
      - mkdir -p ~/.ssh && echo "$SSH_KEY" > ~/.ssh/id_ed25519 && chmod 600 ~/.ssh/id_ed25519
      - echo "$KNOWN_HOSTS" > ~/.ssh/known_hosts
      - ssh deploy@app.example.com "/srv/shop/bin/deploy ${CI_COMMIT_SHA}"
    when:
      - branch: main
        event: push

The server-side deploy script pulls the image, runs migrations, and restarts services. Storing known_hosts as a secret keeps host key verification on.

5. Secrets and safety

Create secrets in the repository settings and restrict them to specific events (push, not pull request) and images where possible. Woodpecker does not expose secrets to pull requests from forks by default. Use separate SSH keys per environment, limited to running the deploy command on the server through authorized_keys options.

pipeline flow1Postgres service startsname: db2Tests and check --deployfail fast3Image built and pushedtag = commit SHA4SSH deploy script runsmigrate, restart5Health check passespipeline greenA failing step stops everything after itdjango-deployment.com
Have the deploy script exit non-zero if the health check fails so the pipeline shows it.

Explanation

Woodpecker runs each step as a container on an agent, sharing a workspace volume with the cloned repository. Services run alongside steps on the same network for the duration of the pipeline. Conditions (when) select which steps run for each event and branch, and secrets are injected only into the steps that reference them. The small footprint suits self-hosted setups alongside a Gitea or Forgejo server.

Edge cases / notes

  • Caching pip. Steps start from fresh containers; mount a cache volume (if your agent allows it) or build a test image with dependencies preinstalled.
  • Multiple workflows. A .woodpecker/ directory with several YAML files splits test, build, and deploy into separate workflows with dependencies.
  • Matrix builds. matrix can test several Python or Django versions in parallel.

Next steps

FAQ

Is Woodpecker the same as Drone?

It is a community fork of Drone with its own development since 2019.

How do I provide PostgreSQL for tests?

Add it under services; steps reach it by its service name.

How do I deploy only from main?

Add when: branch: main, event: push to the build and deploy steps.

Are secrets safe in pull requests?

By default, secrets are not passed to pull requests from forks; restrict them to push events anyway.

Keep going