Deployment
CI/CD pipelines

Test and Deploy Django With Gitea or Forgejo Actions

Use Gitea Actions or Forgejo Actions, GitHub Actions-compatible CI built into self-hosted forges, for Django: register act_runner, write workflows with a PostgreSQL service, push images to the built-in container registry, and deploy over SSH with secrets.

Problem statement

Your code lives on a self-hosted Gitea or Forgejo instance, and you want CI/CD without adding a separate CI server. Both forges include an Actions feature that reads workflow files in GitHub Actions syntax and runs them on act_runner (Forgejo: forgejo-runner) agents you host. Most Django workflows written for GitHub run with small changes, but runners, action sources, services, and secrets need setting up.

Django on Gitea ActionsWorkflow YAML.gitea/workflowsact_runneryour host, DockerJobstest with PostgresRegistry + deploybuilt-in packagesGitHub-style workflows run on runners you host, next to your forge.
  1. Enable Actions in the forge's configuration and in the repository settings.
  2. Install act_runner (or forgejo-runner) on a host with Docker and register it with a token from the admin or repository settings.
  3. Put workflows in .gitea/workflows/ (or .forgejo/workflows/), using GitHub syntax with services: postgres.
  4. Push images to the forge's built-in container registry with a token stored as a secret.
  5. Deploy over SSH from a job limited to main, with the SSH key and known hosts as secrets.

Step-by-step solution

1. Enable Actions and register a runner

In Gitea's app.ini:

[actions]
ENABLED = true

Then on a runner host:

./act_runner register --instance https://git.example.com --token <registration-token> --name runner-1 \
  --labels ubuntu-latest:docker://node:20-bookworm
./act_runner daemon

Labels map runs-on values to container images. Run the runner as a service and give it access to Docker. Keep runners for public repositories separate from those that deploy production.

2. Write the workflow

# .gitea/workflows/ci.yaml
name: ci
on:
  push:
    branches: [main]
  pull_request:

jobs:
  test:
    runs-on: ubuntu-latest
    container: python:3.12-slim
    services:
      postgres:
        image: postgres:17
        env:
          POSTGRES_PASSWORD: postgres
          POSTGRES_DB: shop
    env:
      DATABASE_URL: postgres://postgres:postgres@postgres:5432/shop
      DJANGO_SETTINGS_MODULE: config.settings.test
    steps:
      - uses: actions/checkout@v4
      - run: pip install -r requirements.txt
      - run: python manage.py test --noinput

When the job runs in a container, services are reachable by their names (postgres). Actions referenced with uses: are fetched from GitHub by default (configurable with DEFAULT_ACTIONS_URL), or from your own mirror.

what carries overGitea / ForgejoGitHubWorkflow syntaxmostly compatiblereferenceWorkflow folder.gitea or .forgejo.githubRunnersself-hosted onlyhosted or selfMarketplace actionsfetched or mirrorednativeOIDC to clouds!limitedbuilt inMost Django workflows port with small changesdjango-deployment.com
Without OIDC, deploy credentials are stored as repository secrets.

3. Build and push to the built-in registry

  build:
    needs: test
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: docker/login-action@v3
        with:
          registry: git.example.com
          username: ${{ secrets.REGISTRY_USER }}
          password: ${{ secrets.REGISTRY_TOKEN }}
      - uses: docker/build-push-action@v6
        with:
          push: true
          tags: git.example.com/acme/shop:${{ github.sha }}

Building images inside job containers needs Docker access on the runner (socket mount or Docker-in-Docker). Configure that in the runner's config file deliberately; mounting the socket gives jobs control of the host's Docker.

4. Deploy over SSH

  deploy:
    needs: build
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    steps:
      - run: |
          mkdir -p ~/.ssh
          echo "${{ secrets.DEPLOY_KEY }}" > ~/.ssh/id_ed25519 && chmod 600 ~/.ssh/id_ed25519
          echo "${{ secrets.DEPLOY_KNOWN_HOSTS }}" > ~/.ssh/known_hosts
          ssh deploy@app.example.com "/srv/shop/bin/deploy ${{ github.sha }}"

The server-side script pulls the new image, runs migrations, and restarts services.

5. Secrets and runner security

Store secrets at the repository, organization, or instance level. Runners execute arbitrary workflow code; do not run untrusted pull request workflows on runners that hold deploy access or Docker socket access. Use separate runner labels for trusted deploy jobs.

setup order1Enable Actionsapp.ini and repo2Register act_runnerlabels to images3Add test workflowpostgres service4Add build and deploy jobsmain branch only5Add secrets, test a runregistry, SSHThe forge, registry, and CI all live on infrastructure you controldjango-deployment.com
Mirror commonly used actions locally if the runners have no internet access.

Explanation

Gitea and Forgejo Actions implement the GitHub Actions workflow format and delegate execution to runners based on nektos/act. Each job runs in a container chosen by the runner label, with service containers on a shared network. Compatibility is high for typical CI tasks such as checkout, setup, tests, Docker builds, and SSH deploys, while some GitHub-specific features, such as hosted runners and native cloud OIDC, are limited or absent.

Edge cases / notes

  • Caching. actions/cache works if the runner's cache server is configured; otherwise dependencies install fresh each run.
  • Action availability. Some marketplace actions assume GitHub APIs; prefer plain shell steps for critical deploy logic.
  • Forgejo differences. Forgejo Actions is closely related but evolving independently; check its documentation for runner and syntax details.

Next steps

FAQ

Can I reuse my GitHub Actions workflows on Gitea?

Mostly, yes. Move them to .gitea/workflows/ and check actions that depend on GitHub APIs.

Do I need my own runners?

Yes. Gitea and Forgejo Actions run only on runners you register.

Where should images be stored?

The forge's built-in container registry works well and keeps images next to the code.

Is it safe to run pull requests from strangers?

Not on runners with deploy secrets or Docker socket access. Use separate, isolated runners.

Keep going