Automating CI/CD: How to Configure Gitea Actions on a Private VPS to Build and Push Docker Images to GitHub Container Registry (GHCR)
Introduction to Self-Hosted CI/CD with Gitea Actions
In the modern DevOps landscape, automation is no longer a luxury—it is a core operational necessity. While premium cloud-based CI/CD platforms offer incredible convenience, they often come with restrictive usage limits, data privacy concerns, and scaling costs that can quickly drain a growing enterprise's budget. For organizations seeking maximum control, cost-efficiency, and privacy, hosting a private Git infrastructure using Gitea coupled with Gitea Actions offers a compelling, lightweight alternative to resource-heavy solutions like GitLab.
Gitea Actions brings built-in CI/CD capabilities directly into your self-hosted Git repository, utilizing a workflow syntax that is highly compatible with GitHub Actions. In this comprehensive guide, we will walk through the advanced technical process of configuring Gitea Actions on a private Virtual Private Server (VPS). We will configure the pipeline to automatically compile, package, and securely push Docker images to the GitHub Container Registry (GHCR), establishing a seamless bridge between your private infrastructure and GitHub's global distribution network.
Prerequisites and Infrastructure Requirements
Before initiating the configuration process, ensure your environment meets the following technical specifications:
- A Private VPS: Running a stable Linux distribution such as Ubuntu 22.04 LTS or Debian 12, with at least 2 vCPUs and 4GB of RAM recommended to handle concurrent Docker builds efficiently.
- Gitea Instance: A fully operational Gitea instance (version 1.19 or higher) accessible via a secure HTTPS domain.
- Docker Engine: Installed and running on your VPS, alongside the Docker Compose plugin.
- GitHub Account: A personal or organizational GitHub account with a generated Personal Access Token (PAT) containing
write:packagesandread:packagesscopes to authenticate with GHCR.
Step 1: Enabling Actions on Your Gitea Instance
By default, Gitea Actions may be disabled in your primary configuration file. To enable it, you must modify your app.ini configuration file. Locate this file within your Gitea directory (typically found at /var/lib/gitea/custom/conf/app.ini or managed via Docker volumes).
Open the file using a text editor like Nano or Vim and append the following configuration block:
[actions] ENABLED = true
Save the file and restart your Gitea service to apply the modifications. If you are using Docker Compose to run Gitea, execute the command docker compose restart. Once restarted, a new "Actions" tab will become visible in your Gitea repository settings interface.
Step 2: Deploying and Registering the Gitea Runner on the VPS
Gitea utilizes a separate daemon called act_runner to execute workflow tasks. To ensure maximum isolation and ease of maintenance, we will deploy this runner using Docker Compose on your private VPS.
1. Obtain the Registration Token
Navigate to your Gitea instance. You can register the runner globally (for the entire server) or specifically for a single organization or repository. For a repository-level setup, navigate to Settings > Actions > Runners within your repository and click on Create New Runner. Copy the registration token displayed on the screen; you will need this in the next sub-step.
2. Create the Docker Compose Deployment Configuration
On your VPS, create a dedicated directory for the runner and navigate into it:
mkdir -p ~/gitea-runner && cd ~/gitea-runnerCreate a docker-compose.yml file containing the following multi-container definitions. We will map the host's Docker socket into the runner container so that it can execute Docker builds (known as Docker-in-Docker or execution via host Docker daemon):
version: "3.8"
services:
runner:
image: gitea/act_runner:latest
environment:
- GITEA_INSTANCE_URL=[https://your-gitea-domain.com](https://your-gitea-domain.com)
- GITEA_RUNNER_REGISTRATION_TOKEN=YOUR_COPIED_REGISTRATION_TOKEN
- GITEA_RUNNER_NAME=vps-docker-runner
- GITEA_RUNNER_LABELS=ubuntu-latest:docker://node:18-bullseye,ubuntu-22.04:docker://ubuntu:22.04
volumes:
- ./data:/data
- /var/run/docker.sock:/var/run/docker.sock
restart: alwaysNote: Replace [https://your-gitea-domain.com](https://your-gitea-domain.com) and YOUR_COPIED_REGISTRATION_TOKEN with your actual server parameters.
3. Initialize the Runner Container
Launch the runner in detached mode by executing the following command:
docker compose up -dVerify that the service is running properly by checking the container logs: docker compose logs -f. If successful, the runner will appear with a green "Idle" status icon within your Gitea administrator dashboard or repository settings page.
Step 3: Configuring Secure Repository Secrets
Hardcoding authentication credentials into your CI/CD configuration files represents a major security vulnerability. Gitea Actions allows you to store sensitive data securely using Secrets. These variables are encrypted and exposed to the execution runner only during active workflow execution.
Navigate to your repository's Settings > Actions > Secrets dashboard and add the following two critical variables:
GHCR_USERNAME: Your GitHub account username (or the name of your GitHub Organization) in lowercase format.GHCR_PAT: The GitHub Personal Access Token (Classic) withwrite:packagespermissions that you generated previously.
Step 4: Crafting the CI/CD Workflow Specification File
With the runner online and credentials securely stored, we can now define the automation workflow. Gitea Actions automatically detects workflow files placed within the .gitea/workflows/ directory at the root of your code repository. The workflow syntax utilizes standard YAML structuring.
Create a file named .gitea/workflows/build-push.yml and populate it with the following comprehensive production-grade configuration:
name: Build and Push Docker Image to GHCR
on:
push:
branches:
- main
tags:
- 'v*'
jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: [https://github.com/actions/checkout@v3](https://github.com/actions/checkout@v3)
- name: Set up Docker Buildx
uses: [https://github.com/docker/setup-buildx-action@v2](https://github.com/docker/setup-buildx-action@v2)
- name: Authenticate with GitHub Container Registry
run: |
echo "${{ secrets.GHCR_PAT }}" | docker login ghcr.io -u "${{ secrets.GHCR_USERNAME }}" --password-stdin
- name: Extract Metadata for Docker Tags
id: meta
run: |
REGISTRY=ghcr.io
IMAGE_NAME="${{ secrets.GHCR_USERNAME }}/my-app"
SHA_SHORT=$(echo "${{ gitea.sha }}" | cut -c1-7)
echo "image_path=${REGISTRY}/${IMAGE_NAME}" >> $GITHUB_OUTPUT
echo "tag_sha=${SHA_SHORT}" >> $GITHUB_OUTPUT
- name: Build and Push Docker Image
uses: [https://github.com/docker/build-push-action@v4](https://github.com/docker/build-push-action@v4)
with:
context: .
file: ./Dockerfile
push: true
tags: |
${{ steps.meta.outputs.image_path }}:latest
${{ steps.meta.outputs.image_path }}:${{ steps.meta.outputs.tag_sha }}Detailed Breakdown of the Workflow Stages
- Trigger Mechanisms (
on:): The automation pipeline initiates whenever a developer pushes new code commits to themainbranch, or pushes a release tag matching the version pattern (e.g.,v1.0.0). - Code Checkout: It pulls the code repository to the runner context using the actions ecosystem. Note that Gitea can fetch community actions directly via their full GitHub URLs.
- Registry Authentication: The runner executes a secure
docker logincommand againstghcr.iousing the encrypted secrets stored safely in the database. - Metadata Tagging: The script dynamically generates clean semantic versioning tags using a combination of the
latestkeyword and the short Git commit SHA hash, ensuring full traceability between deployed containers and source commits. - BuildX Processing: The
docker/build-push-actionhandles complex multi-stage Docker builds, layer caching optimization, and securely pushes the final artifact directly out to the GitHub Container Registry.
Step 5: Testing and Verifying the Deployment Pipeline
To validate your new setup, ensure your repository contains a basic, valid Dockerfile. Commit your changes and push the newly created .gitea/workflows/build-push.yml file directly to your primary tracking branch:
git add .gitea/workflows/build-push.yml
git commit -m "ci: implement automated build to ghcr pipeline"
git push origin mainImmediately head over to the Actions tab inside your Gitea web user interface. You will see an active execution instance initialized under your commit message. Click into the job panel to observe live console logs as your host VPS pulls build dependencies, runs compilation tasks, and establishes connection handshakes with the external GitHub registry networks.
Once the workflow run completes with a successful green status indicator, log into your GitHub profile and navigate to your Packages dashboard. Your new container image will be visible there, fully cataloged, timestamped, and immediately ready for target deployment configurations via docker pull ghcr.io/username/my-app:latest.
Conclusion and Best Practices
By establishing this cross-platform CI/CD integration, you unlock an elite standard of infrastructure performance. You preserve complete data governance over your operational source code repository via your self-hosted Gitea VPS, while safely delegating bulky container image storage and rapid edge CDN distribution workloads to GitHub's global registry infrastructure.
As you scale this setup across production environments, remember to implement standard platform upkeep policies: regularly purge dangling container builder cache segments on the VPS hosting the runner (docker system prune -f), keep your act_runner core images updated to match upstream development improvements, and apply the principle of least privilege to your GitHub Personal Access Tokens to guarantee absolute digital workspace security.
