Back to articles
Technology Insight

Streamlining CI/CD: How to Configure Gitea Actions for Docker Builds and GHCR Publishing on Your Self-Hosted VPS

May 29, 2026

Introduction

In the modern software development lifecycle, continuous integration and continuous delivery (CI/CD) have evolved from luxury automation to absolute necessities. While mainstream platforms like GitHub and GitLab offer robust, cloud-hosted CI/CD environments, many enterprise organizations and independent developers prefer the privacy, cost-efficiency, and control of self-hosted solutions. Gitea, a lightweight and ultra-fast DevOps platform, has filled this niche perfectly. With the introduction of Gitea Actions—a built-in CI/CD engine compatible with GitHub Actions syntax—developers can now orchestrate complex deployment pipelines directly from their Virtual Private Servers (VPS).

A common, highly secure architectural pattern involves hosting your source code on a private VPS using Gitea, while leveraging an enterprise-grade, globally distributed registry like the GitHub Container Registry (GHCR) for storing public or private Docker images. This hybrid approach guarantees internal control over your repositories while utilizing GitHub's scalable infrastructure for container distribution. In this deep dive, we will explore the comprehensive, step-by-step process of configuring Gitea Actions on your standalone VPS to automatically build, package, and push Docker images directly to GHCR upon code commits.

---

Prerequisites and System Architecture

Before diving into the configuration files, it is crucial to establish a stable foundational environment. To follow this guide seamlessly, ensure you have administrative control over a VPS running a modern Linux distribution (such as Ubuntu 22.04 LTS or Debian 12) with the following components installed and verified:

  • Gitea Instance: Version 1.19 or higher, which natively supports Gitea Actions.
  • Docker Engine & Compose: Installed on the host VPS to manage containers and execute isolated build steps.
  • GitHub Account: A valid account with permission to write to GitHub Container Registry (ghcr.io) via Personal Access Tokens (PAT).
System Insight: Gitea Actions relies on a separate component called gitea-runner. This runner operates as a lightweight daemon on your VPS, polling your Gitea server for pending jobs, executing them inside isolated Docker containers, and reporting the statuses back to the Gitea UI.
---

Step 1: Enabling Actions and Registering the Gitea Runner

By default, Gitea Actions might be disabled in your primary configuration file. To activate this feature, open your Gitea configuration file, typically located at /etc/gitea/app.ini or within your Docker volume mount, and append the following configuration block:

[actions]
ENABLED = true

Restart your Gitea service to apply the changes. Once activated, you will see an "Actions" tab appear in your repository settings.

Deploying the Gitea Runner

Next, we must deploy the gitea-runner on your VPS using Docker Compose. Create a dedicated directory and define a docker-compose.yml file for the runner:

version: '3.8'
services:
  runner:
    image: gitea/act_runner:latest
    environment:
      - CONFIG_FILE=/config.yaml
      - GITEA_INSTANCE_URL=[https://your-gitea-domain.com](https://your-gitea-domain.com)
      - GITEA_RUNNER_REGISTRATION_TOKEN=YOUR_REGISTRATION_TOKEN
      - GITEA_RUNNER_NAME=vps-docker-runner
      - GITEA_RUNNER_LABELS=ubuntu-latest:docker://node:18-bullseye,ubuntu-22.04:docker://node:18-bullseye
    volumes:
      - ./config.yaml:/config.yaml
      - ./data:/data
      - /var/run/docker.sock:/var/run/docker.sock

To obtain the GITEA_RUNNER_REGISTRATION_TOKEN, navigate to your Gitea instance, go to Site Administration > Actions > Runners, and click Create New Runner. Copy the generated token into your environment file. Notice that we mount /var/run/docker.sock into the runner container; this enables a critical technique known as Docker-outside-of-Docker (DooD), allowing the runner to spin up sibling containers on the host VPS to compile and build your application images.

---

Step 2: Securing GitHub Container Registry Credentials

To safely push the generated Docker images to ghcr.io from an isolated VPS, your pipeline needs authentication credentials. Storing plain-text passwords in source control is a severe security risk. Instead, we use Gitea Secrets.

  1. Log in to your GitHub account and navigate to Settings > Developer Settings > Personal Access Tokens > Tokens (classic).
  2. Generate a new token with the explicit scopes: write:packages, read:packages, and delete:packages (if management is required). Copy this token immediately.
  3. Switch to your Gitea repository, navigate to Settings > Actions > Secrets, and click Add Secret.
  4. Create a secret named GHCR_USERNAME containing your exact GitHub username (lowercase).
  5. Create a second secret named GHCR_TOKEN and paste the classic Personal Access Token you copied from GitHub.

These environment variables are now dynamically injected into the runtime environment of your CI/CD runner at execution time, completely hidden from unauthorized eyes and omitted from logs.

---

Step 3: Creating the Gitea Actions Workflow

Gitea Actions utilizes the identical YAML syntax pioneered by GitHub Actions. To define a workflow pipeline, create a directory structure at the root of your project repository named .gitea/workflows/. Inside this folder, create a file named build-and-push.yml.

Let us analyze a production-ready workflow template designed explicitly for checking out code, building a specialized multi-stage Dockerfile, authenticating against GHCR, and distributing the final container image:

name: Build and Deploy to GHCR

on:
  push:
    branches:
      - main
      - master

jobs:
  publish-docker-image:
    runs-on: ubuntu-latest
    steps:
      - name: Check out the repository
        uses: actions/checkout@v3

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v2

      - name: Log in to GitHub Container Registry
        uses: docker/login-action@v2
        with:
          registry: ghcr.io
          username: ${{ secrets.GHCR_USERNAME }}
          password: ${{ secrets.GHCR_TOKEN }}

      - name: Extract metadata (tags, labels) for Docker
        id: meta
        uses: docker/metadata-action@v4
        with:
          images: ghcr.io/${{ secrets.GHCR_USERNAME }}/my-app-image
          tags: |
            type=ref,event=branch
            type=semver,pattern={{version}}
            type=sha,format=short

      - name: Build and Push Docker Image
        uses: docker/build-push-action@v4
        with:
          context: .
          file: ./Dockerfile
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

Anatomy of the Pipeline Steps

Let's dissect the vital mechanisms within this file to understand why it ensures an efficient build ecosystem:

  • Triggers (on.push.branches): This optimization guarantees that resources on your VPS are conserved, executing the computationally expensive build steps only when code is safely merged into production-ready stable branches.
  • Docker Login (docker/login-action): Connects natively to ghcr.io using the encrypted secrets configured in Step 2.
  • Metadata Extraction (docker/metadata-action): Programmatically tags your Docker images. It tracks whether the build came from a branch version or a specific Git commit hash (SHA), ensuring complete traceability between container deployments and underlying source modifications.
  • Caching (cache-from / cache-to): Leveraging type=gha instructs the system to reuse compilation layers from previous builds. This reduces network overhead and minimizes VPS CPU utilization, reducing subsequent build times from minutes to seconds.
---

Step 4: Execution, Verification, and Best Practices

With your configuration finalized, commit the .gitea/workflows/build-and-push.yml file along with your application code and push it to your self-hosted Gitea remote instance. Navigate to the Actions tab of your web UI. You will observe your workflow initializing instantly, provisioning the isolated container on your VPS host, cloning the code, executing the Docker layers, and transmitting payload chunks directly to the GitHub global repository network.

Troubleshooting and Optimization Tips

When engineering advanced pipelines on self-hosted architecture, developers frequently encounter edge-case bottlenecks. Consider implementing these industry-standard mitigation strategies:

Common ProblemUnderlying CauseRecommended Architectural Solution
Workflow stuck in Pending statusRunner disconnected or labels mismatchEnsure gitea-runner is active and verify that the runs-on string matches the exact runner tags in config.yaml.
Out of Disk Space ErrorsDangling Docker build caches on VPS hostImplement a periodic cron job on the host system running docker system prune -f --volumes to free up storage space.
Authentication Denied (403)Incorrect PAT permissions or naming issuesVerify that your GitHub PAT has explicit write:packages access, and confirm your repository name uses entirely lowercase characters.

Furthermore, ensure that your VPS has adequate resource allocations. While the Gitea daemon itself requires minimal hardware overhead (often performing flawlessly with less than 1GB of RAM), running simultaneous local multi-stage Docker builds can spike CPU usage and exhaust memory rapidly. Limiting parallel builds or setting resource boundaries on your gitea-runner container is recommended for smaller, budget-friendly VPS setups.

---

Conclusion

By connecting your self-hosted Gitea Actions environment to the GitHub Container Registry, you achieve an optimal, modern hybrid architecture. You maintain sovereign ownership and complete confidentiality over your core Git infrastructure and proprietary source code assets on your personal VPS, while leveraging GitHub's globally distributed, highly reliable container content delivery networks to distribute your production-ready runtime dependencies.

This framework is infinitely scalable; as your applications grow, you can seamlessly add secondary runners to distribute compilation workloads across multiple physical nodes without outgrowing your Git platform. Implementing this unified, standard-based automated delivery framework lays a rock-solid foundation for continuous enterprise-grade modern application delivery.

Streamlining CI/CD: How to Configure Gitea Actions for Docker Builds and GHCR Publishing on Your Self-Hosted VPS | DPTCloud