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
sudoprivileges. - 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/giteaand the service running as thegituser. Adjust the paths if your installation differs. - Gitea reachable over HTTPS at a domain, written as
git.your_domainin 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:
gitea --version
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:
sudo nano /etc/gitea/app.ini
Add or edit the [actions] section:
[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:
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:
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:
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
act_runner version v0.2.13
Generate a default configuration file:
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:
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:
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
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:
sudo nano /etc/systemd/system/act_runner.service
[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:
sudo systemctl daemon-reload
sudo systemctl enable --now act_runner
sudo journalctl -u act_runner -n 10 --no-pager
... 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:
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-Signatureheader 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:
[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:
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:
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
dn: uid=jdoe,ou=users,dc=example,dc=com
uid: jdoe
mail: [email protected]
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:
[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:
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:
sudo install -d -o git -g git -m 0750 /var/backups/gitea
Create the backup script:
sudo nano /usr/local/bin/gitea-backup
#!/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
sudo chmod 0755 /usr/local/bin/gitea-backup
Create a service that runs the script as git:
sudo nano /etc/systemd/system/gitea-backup.service
[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:
sudo nano /etc/systemd/system/gitea-backup.timer
[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:
sudo systemctl daemon-reload
sudo systemctl enable --now gitea-backup.timer
sudo systemctl start gitea-backup.service
sudo ls -lh /var/backups/gitea
-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:
sudo apt install unzip
sudo sh -c 'unzip -l /var/backups/gitea/gitea-dump-*.zip' | head -n 12
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.
ImportantBackups stored on the same server do not survive a disk failure or a deleted VPS. Copy
/var/backups/giteato another location, for example object storage, with a tool such asrcloneorrestic.
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.
