--- title: "Gitea Actions CI/CD, LDAP, Mirrors and Backups on Ubuntu" description: "Extend Gitea on Ubuntu 24.04 with an Actions runner for CI/CD, signed webhooks, LDAP login, repository mirroring and automated daily backups with gitea dump." category: "Development Tools" language: en source: https://cubepath.com/docs/development-tools/gitea-advanced-configuration-and-ci-cd updated: 2026-09-25 --- # Gitea Actions CI/CD, LDAP, Mirrors and Backups on Ubuntu Gitea is a lightweight, self-hosted Git service with pull requests, issues, packages and a built-in CI/CD system called Gitea Actions that uses the same workflow syntax as GitHub Actions. A basic installation only hosts repositories; this tutorial covers what a team usually adds next. On Ubuntu 24.04 you will connect an Actions runner and run a first pipeline, configure webhooks, let users log in with LDAP, mirror repositories to and from other Git hosts, and schedule verified daily backups. ## Prerequisites To follow this guide you need: - A server running Ubuntu 24.04 LTS, for example a CubePath VPS, with a non-root user that has `sudo` privileges. - Gitea 1.21 or newer installed from the official binary and running as a systemd service, with the standard layout: binary at `/usr/local/bin/gitea`, configuration at `/etc/gitea/app.ini`, data in `/var/lib/gitea` and the service running as the `git` user. Adjust the paths if your installation differs. - Gitea reachable over HTTPS at a domain, written as `git.your_domain` in this guide. - An administrator account in Gitea. - Docker Engine installed from Docker's official repository on the machine that will run the Actions runner (this server or a separate one). - For the LDAP section: an LDAP or Active Directory server and a read-only bind account. Check the installed version: ```bash gitea --version ``` ```output Gitea version 1.24.x built with GNU Make ... ``` ## Step 1 - Checking the Actions configuration Gitea Actions is enabled by default since Gitea 1.21. Open the configuration file to make it explicit and to choose where actions such as `actions/checkout` are downloaded from: ```bash sudo nano /etc/gitea/app.ini ``` Add or edit the `[actions]` section: ```ini [actions] ENABLED = true DEFAULT_ACTIONS_URL = github ``` `DEFAULT_ACTIONS_URL` accepts `github` (actions referenced as `actions/checkout@v4` are fetched from github.com) or `self` (fetched from your own Gitea instance, useful on networks without internet access). It does not accept an arbitrary URL. Restart Gitea: ```bash sudo systemctl restart gitea ``` In the web interface, **Site Administration > Actions > Runners** should now be available. New repositories have Actions enabled; for existing ones, check **Settings > Repository > Actions** in each repository. ## Step 2 - Installing act_runner Workflows run on `act_runner`, a separate program that connects to Gitea, picks up jobs and executes each one inside a Docker container. Any user in the `docker` group effectively has root on that machine, so for teams where not everyone is trusted, run the runner on a separate server instead of on the Gitea host. Create a system user for the runner with a home directory where it keeps its registration file, and give it access to Docker: ```bash sudo useradd --system --create-home --home-dir /var/lib/act_runner --shell /usr/sbin/nologin act_runner sudo usermod -aG docker act_runner ``` Look up the latest act_runner version on its releases page at `https://gitea.com/gitea/act_runner/releases`, then download that binary. Replace `0.2.13` with the current version, and `amd64` with `arm64` on ARM servers: ```bash RUNNER_VERSION=0.2.13 sudo wget -O /usr/local/bin/act_runner "https://dl.gitea.com/act_runner/${RUNNER_VERSION}/act_runner-${RUNNER_VERSION}-linux-amd64" sudo chmod 0755 /usr/local/bin/act_runner act_runner --version ``` ```output act_runner version v0.2.13 ``` Generate a default configuration file: ```bash sudo mkdir -p /etc/act_runner act_runner generate-config | sudo tee /etc/act_runner/config.yaml > /dev/null sudo nano /etc/act_runner/config.yaml ``` In the `runner:` section, set how many jobs may run in parallel and which labels the runner offers. A label maps the `runs-on` value in a workflow to a Docker image: ```yaml runner: file: .runner capacity: 2 labels: - "ubuntu-latest:docker://gitea/runner-images:ubuntu-latest" ``` Leave the rest of the file at its defaults. `gitea/runner-images` are images maintained by the Gitea project that include Node.js and the common tools GitHub-style actions expect. ## Step 3 - Registering the runner as a service Get a registration token from Gitea: go to **Site Administration > Actions > Runners**, click **Create new Runner** and copy the token. This creates an instance-wide runner available to every repository; organizations and repositories have the same page under their own settings if you want a runner scoped to them. Register from the runner's home directory, so the `.runner` file with its credentials is written there: ```bash cd /var/lib/act_runner sudo -u act_runner act_runner register --no-interactive \ --config /etc/act_runner/config.yaml \ --instance https://git.your_domain \ --token your_registration_token \ --name runner-01 ``` ```output INFO Registering runner, name=runner-01, instance=https://git.your_domain, labels=[ubuntu-latest:docker://gitea/runner-images:ubuntu-latest]. INFO Runner registered successfully. ``` Create a systemd unit so the runner starts at boot: ```bash sudo nano /etc/systemd/system/act_runner.service ``` ```ini [Unit] Description=Gitea Actions runner After=network-online.target docker.service Wants=network-online.target Requires=docker.service [Service] Type=simple User=act_runner WorkingDirectory=/var/lib/act_runner ExecStart=/usr/local/bin/act_runner daemon --config /etc/act_runner/config.yaml Restart=always RestartSec=5 [Install] WantedBy=multi-user.target ``` Start it and check the log: ```bash sudo systemctl daemon-reload sudo systemctl enable --now act_runner sudo journalctl -u act_runner -n 10 --no-pager ``` ```output ... level=info msg="Starting runner daemon" ... level=info msg="runner: runner-01, with version: v0.2.13, with labels: [ubuntu-latest], declare successfully" ``` Back in **Site Administration > Actions > Runners**, the runner is listed with the status **Idle**. ## Step 4 - Running a first workflow Gitea looks for workflow files in `.gitea/workflows/` (and also in `.github/workflows/`). In any repository, create `.gitea/workflows/ci.yaml` with a pipeline that tests a Node.js project on every push and pull request: ```yaml name: CI on: push: branches: [main] pull_request: jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 22 - run: npm ci - run: npm test ``` Commit and push the file. Open the repository's **Actions** tab: the run appears within seconds and each step's log can be expanded. A green check means the runner pulled the image, ran the job and reported back. For credentials such as deploy keys or API tokens, use **Settings > Actions > Secrets** in the repository (or organization) and reference them as `${{ secrets.NAME }}` in the workflow. Secrets are masked in logs and are not passed to workflows triggered from forks. ## Step 5 - Configuring webhooks Webhooks notify other systems, such as a deployment service or a chat channel, when something happens in a repository. Add one in **Repository > Settings > Webhooks > Add Webhook**. Gitea has presets for Slack, Discord, Microsoft Teams, Telegram and Matrix; choose **Gitea** for a generic JSON payload to your own endpoint. For a generic webhook, set: - **Target URL**: the endpoint that receives the POST request. - **Secret**: a long random string. Gitea sends an `X-Gitea-Signature` header containing the hex-encoded HMAC-SHA256 of the request body computed with this secret. Your endpoint should compute the same value and reject requests that do not match. - **Trigger on**: only the events you need, for example **Push events**. Click **Test Delivery** after saving and open the delivery in the list below to see the request, the response code and the response body. By default, Gitea refuses to send webhooks to private and loopback addresses, which protects your internal network from users who can create webhooks. If your receiver runs on a private IP, allow that network explicitly in `app.ini` and restart Gitea: ```ini [webhook] ALLOWED_HOST_LIST = external,10.0.0.0/8 ``` Avoid `*`, which allows any destination, including services on the Gitea server itself. ## Step 6 - Adding LDAP authentication With LDAP, users log in with their directory credentials and Gitea creates their account on first login. Before configuring Gitea, confirm that the server can reach the directory and that the bind account works. Install the LDAP client tools: ```bash sudo apt install ldap-utils ``` Search for a known user, replacing the host, bind DN and search base with your own. The `-W` flag prompts for the bind password: ```bash ldapsearch -x -H ldaps://ldap.your_domain:636 \ -D "cn=gitea,ou=services,dc=example,dc=com" -W \ -b "ou=users,dc=example,dc=com" "(uid=jdoe)" uid mail ``` ```output dn: uid=jdoe,ou=users,dc=example,dc=com uid: jdoe mail: jdoe@example.com ``` If that works, go to **Site Administration > Identity & Access > Authentication Sources > Add Authentication Source** and choose **LDAP (via BindDN)**. The important fields are: | Field | OpenLDAP example | Active Directory example | |-------|------------------|--------------------------| | Security Protocol | LDAPS | LDAPS | | Host / Port | `ldap.your_domain` / `636` | `dc01.corp.example.com` / `636` | | Bind DN | `cn=gitea,ou=services,dc=example,dc=com` | `CN=gitea,OU=Service Accounts,DC=corp,DC=example,DC=com` | | User Search Base | `ou=users,dc=example,dc=com` | `OU=Staff,DC=corp,DC=example,DC=com` | | User Filter | `(&(objectClass=inetOrgPerson)(uid=%[1]s))` | `(&(objectCategory=person)(objectClass=user)(sAMAccountName=%[1]s))` | | Username Attribute | `uid` | `sAMAccountName` | | Email Attribute | `mail` | `mail` | `%[1]s` is replaced by the name the user types at login. Optionally set an **Admin Filter**, such as membership of an LDAP group, to make those users Gitea administrators. Save the source, log out, and log in as a directory user; the account appears under **Site Administration > User Accounts** with the LDAP source as its authentication type. ## Step 7 - Mirroring repositories Gitea can keep a copy of an external repository (pull mirror) or push every change of a local repository to another host (push mirror). The sync schedule for pull mirrors is configured in `app.ini`: ```ini [mirror] ENABLED = true DEFAULT_INTERVAL = 8h MIN_INTERVAL = 10m ``` To create a pull mirror in the web interface, click **+ > New Migration**, choose the source (GitHub, GitLab, Gitea or plain Git), enter the URL and check **This repository will be a mirror**. To script it, create an API token under **Settings > Applications** with write access to repositories, then call the migrate endpoint: ```bash curl -fsS -X POST "https://git.your_domain/api/v1/repos/migrate" \ -H "Authorization: token your_api_token" \ -H "Content-Type: application/json" \ -d '{ "clone_addr": "https://github.com/go-gitea/gitea.git", "repo_owner": "your_user", "repo_name": "gitea-mirror", "mirror": true, "mirror_interval": "8h0m0s", "service": "git" }' ``` The response is the JSON description of the new repository with `"mirror": true`. Its **Settings** page shows the last sync time and a **Synchronize Now** button. A push mirror is configured on an existing repository under **Settings > Repository > Mirror Settings**. Enter the remote URL, a username and an access token for the target host (for example a GitHub fine-grained token with write access to that repository), and enable **Sync when commits are pushed** to push immediately instead of only on the interval. ## Step 8 - Scheduling daily backups `gitea dump` writes the database, repositories, attachments, LFS objects and configuration into a single archive. Run it daily with a systemd timer and delete old archives automatically. Create the backup directory, owned by the `git` user: ```bash sudo install -d -o git -g git -m 0750 /var/backups/gitea ``` Create the backup script: ```bash sudo nano /usr/local/bin/gitea-backup ``` ```bash #!/usr/bin/env bash set -euo pipefail backup_dir="/var/backups/gitea" keep_days=7 cd /var/lib/gitea /usr/local/bin/gitea dump \ --config /etc/gitea/app.ini \ --work-path /var/lib/gitea \ --file "${backup_dir}/gitea-dump-$(date +%F-%H%M).zip" find "${backup_dir}" -name 'gitea-dump-*.zip' -mtime +"${keep_days}" -delete ``` ```bash sudo chmod 0755 /usr/local/bin/gitea-backup ``` Create a service that runs the script as `git`: ```bash sudo nano /etc/systemd/system/gitea-backup.service ``` ```ini [Unit] Description=Gitea backup with gitea dump [Service] Type=oneshot User=git Group=git ExecStart=/usr/local/bin/gitea-backup ``` And a timer that starts it every night at 02:30. `Persistent=true` runs a missed backup after the server was off at that time: ```bash sudo nano /etc/systemd/system/gitea-backup.timer ``` ```ini [Unit] Description=Daily Gitea backup [Timer] OnCalendar=*-*-* 02:30:00 Persistent=true [Install] WantedBy=timers.target ``` Enable the timer and run one backup right away to test it: ```bash sudo systemctl daemon-reload sudo systemctl enable --now gitea-backup.timer sudo systemctl start gitea-backup.service sudo ls -lh /var/backups/gitea ``` ```output -rw------- 1 git git 48M Sep 25 11:02 gitea-dump-2026-09-25-1102.zip ``` List the beginning of the archive to confirm it contains the configuration, the database dump and the repositories. Install `unzip` first if needed: ```bash sudo apt install unzip sudo sh -c 'unzip -l /var/backups/gitea/gitea-dump-*.zip' | head -n 12 ``` ```output Archive: /var/backups/gitea/gitea-dump-2026-09-25-1102.zip Length Date Time Name --------- ---------- ----- ---- 2071 2026-09-25 11:02 app.ini 1843221 2026-09-25 11:02 gitea-db.sql 0 2026-09-25 11:02 repos/ 0 2026-09-25 11:02 data/ ... ``` To restore, stop Gitea, put `app.ini` back in `/etc/gitea/`, the contents of `data` in Gitea's data directory and `repos` in the repository root configured in `app.ini`, import `gitea-db.sql` into an empty database with your database's client, fix ownership with `chown -R git:git`, and finally run `sudo -u git gitea admin regenerate hooks --config /etc/gitea/app.ini` before starting Gitea. Practice this once on a test server so you know the procedure works for your setup. > Important: Backups stored on the same server do not survive a disk failure or a deleted VPS. Copy `/var/backups/gitea` to another location, for example object storage, with a tool such as `rclone` or `restic`. ## Troubleshooting **Workflows stay in "Waiting".** No online runner offers the label in `runs-on`. Compare the workflow's `runs-on` value with the labels shown for the runner in **Site Administration > Actions > Runners**, and check `sudo journalctl -u act_runner -f`. **Jobs fail with `permission denied` on `/var/run/docker.sock`.** The runner user is not in the `docker` group, or the service started before you added it. Run `sudo usermod -aG docker act_runner` and `sudo systemctl restart act_runner`. **Webhook deliveries fail with `webhook can only call allowed HTTP servers`.** The target resolves to a private or loopback address. Add its network to `ALLOWED_HOST_LIST` as shown in Step 5. **LDAP users cannot log in.** Run the same `ldapsearch` command with the user's name in the filter. If it returns nothing, the search base or filter is wrong; if it fails to connect, check the port, the protocol and the CA certificate. Gitea logs the LDAP error in `sudo journalctl -u gitea -n 50`. ## Conclusion Your Gitea server now runs CI pipelines on its own runner, notifies external services through signed webhooks, authenticates users against LDAP, mirrors repositories and creates a backup every night. Next, add a second runner on another server to increase capacity, use Gitea's package registry to publish build artifacts from your workflows, and copy backups off the server on a schedule.