Streamlining CI/CD: How to Configure Gitea Actions for Docker Builds and GHCR Publishing on Your Self-Hosted VPS
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 = trueRestart 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.sockTo 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.
- Log in to your GitHub account and navigate to Settings > Developer Settings > Personal Access Tokens > Tokens (classic).
- Generate a new token with the explicit scopes:
write:packages,read:packages, anddelete:packages(if management is required). Copy this token immediately. - Switch to your Gitea repository, navigate to Settings > Actions > Secrets, and click Add Secret.
- Create a secret named
GHCR_USERNAMEcontaining your exact GitHub username (lowercase). - Create a second secret named
GHCR_TOKENand 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=maxAnatomy 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.iousing 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=ghainstructs 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 Problem | Underlying Cause | Recommended Architectural Solution |
|---|---|---|
| Workflow stuck in Pending status | Runner disconnected or labels mismatch | Ensure gitea-runner is active and verify that the runs-on string matches the exact runner tags in config.yaml. |
| Out of Disk Space Errors | Dangling Docker build caches on VPS host | Implement 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 issues | Verify 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.
