Self-hosting Git with Gitea on Civo

Learn how to self-host Gitea, a lightweight Git server, on Civo with automatic HTTPS via Let's Encrypt, Caddy reverse proxy, and UFW firewall security in this complete setup tutorial.

6 minutes reading time

Written by

Jubril Oyetunji
Jubril Oyetunji

Technical Writer at Civo

To self-host Git or not, that's been a topic of debate as major source code hosting platforms have repeatedly shown signs of unreliability.

And with supply chain attacks happening almost every other week, it's easy to see why more people are looking for alternatives.

In this post, we'll look at Gitea, a lightweight, self-hosted Git service written in Go that happily runs on a small Linux VM. We'll spin one up on Civo, install Gitea on it, and put Caddy in front so we get automatic HTTPS via Let's Encrypt.

Why self-host Gitea?

A few reasons people end up here:

  • You own your data and your uptime. When <insert big provider> has a bad day, your team doesn't have to.
  • Predictable pricing:  Unlimited private repos, organizations, packages, and Actions runners are all in the box.
  • It's lightweight. Gitea is a single Go binary with a SQLite database
  • The workflow is familiar. Issues, pull requests, Actions, container and package registries, if you can use GitHub, you can use Gitea on day one.

Prerequisites

This tutorial assumes some familiarity with the Linux command line. You'll also need:

  • A Civo account
  • The Civo CLI installed and authenticated (civo apikey save <name> <key>)
  • An SSH key uploaded to Civo (civo sshkey upload <name> --public-key-path ~/.ssh/id_ed25519.pub)
  • A domain name you control, with the ability to add an A record pointing at your VM. If you don't have a domain, we will use nip.io, a free wildcard DNS service, so any IP gets a working hostname that Caddy can fetch a certificate for

Creating a Civo virtual machine

We'll begin by creating a virtual machine using the Civo CLI:

civo instance create \
--hostname=teapot \
--size=g4s.medium \
--diskimage=ubuntu-jammy \
--initialuser=demo \
--sshkey=<your-key-name>

Give it a few seconds to provision, then grab the public IP and the initial user's password. The -o custom -f flags let us pull specific fields straight out:

civo instance show teapot -o custom -f public_ip,initial_password

(If you provisioned with an SSH key, the password field will be empty; that's fine, you'll log in with the key.)

At this point, you should be able to log in to your machine:

ssh demo@<public-ip>

Note: before you reach the Caddy section later on, point your domain's A record at the public IP shown above. Let's Encrypt won't issue a certificate unless your domain resolves to this VM, and DNS can take a few minutes to propagate. If you don't have a domain yet, you can skip this step entirely. We'll use a nip.io hostname built from the VM's IP, which resolves automatically

Self-hosting Gitea on a Civo VM with Caddy

Civo dashboard showing the teapot instance up and running

Installing Gitea

With our VM up, let's install Gitea. We'll grab the binary directly from the Gitea release page and run it as a dedicated system user.

Update the package cache, upgrade installed packages, and install Git:

sudo apt update && sudo apt upgrade -y && sudo apt install git -y

Download the Gitea binary and make it executable. We're pinning to 1.26.2 here, check the Gitea downloads page for the latest stable version:

wget -O gitea https://dl.gitea.com/gitea/1.26.2/gitea-1.26.2-linux-amd64
chmod +x gitea

Add the git system user that will own the Gitea process:

sudo adduser --system --shell /bin/bash --gecos 'Git Version Control' \
--group --disabled-password --home /home/git git

Create the directories Gitea uses for data, logs, and configuration:

sudo mkdir -p /var/lib/gitea/{custom,data,log}
sudo chown -R git:git /var/lib/gitea/
sudo chmod -R 750 /var/lib/gitea/
sudo mkdir /etc/gitea
sudo chown root:git /etc/gitea
sudo chmod 770 /etc/gitea

Move the binary onto $PATH:

sudo cp gitea /usr/local/bin/gitea

Running Gitea as a systemd service

Running Gitea manually works for a quick check, but we want it to come back up automatically after reboots. Ubuntu ships with systemd as its init system, so let's go with that.

Write the unit file:

sudo tee /etc/systemd/system/gitea.service > /dev/null <<'EOF'
[Unit]
Description=Gitea (Git with a cup of tea)
After=syslog.target network.target
[Service]
RestartSec=2s
Type=simple
User=git
Group=git
WorkingDirectory=/var/lib/gitea/
ExecStart=/usr/local/bin/gitea web -c /etc/gitea/app.ini
Restart=always
Environment=USER=git HOME=/home/git GITEA_WORK_DIR=/var/lib/gitea
# Uncomment the two lines below if you have large repos and hit HTTP 500s:
#LimitMEMLOCK=infinity
#LimitNOFILE=65535
[Install]
WantedBy=multi-user.target
EOF

Reload systemd so it picks up the new unit, then enable and start Gitea:

sudo systemctl daemon-reload
sudo systemctl enable --now gitea.service

Check that it came up cleanly:

sudo systemctl status gitea

The output should look something like this:

● gitea.service - Gitea (Git with a cup of tea)
Loaded: loaded (/etc/systemd/system/gitea.service; enabled; vendor preset: enabled)
Active: active (running) since Sat 2026-05-23 19:20:43 UTC; 13h ago
Main PID: 1642 (gitea)
Tasks: 12 (limit: 4420)
Memory: 92.2M
CPU: 1min 47.864s
CGroup: /system.slice/gitea.service
└─1642 /usr/local/bin/gitea web -c /etc/gitea/app.ini

The two lines to look for are Loaded: ... enabled (it'll come back after a reboot) and Active: active (running) (it's up right now). Gitea is now listening on port 3000.

Completing the install in the browser

Gitea's first run is a one-page web installer. Open http://<your-vm-ip>:3000 in your browser, you'll land on the install form.

Self-hosting Gitea on a Civo VM with Caddy

Gitea one-time install screen on first boot.

Choose SQLite3 as the database type. SQLite keeps the application lightweight and is a great fit for a self-hosted instance.

If you outgrow it later, you can migrate to PostgreSQL or MySQL. Leave the remaining pre-filled fields as they are and submit the form.

Once the installation completes, create the first user by visiting http://<your-vm-ip>:3000/user/sign_up.

Note: the first account to register automatically becomes the admin of your Gitea instance.

Self-hosting Gitea on a Civo VM with Caddy

Registering the first user, this account becomes the instance admin.

Submit the form, and you'll be dropped into a freshly initialized dashboard, ready for repositories.

Self-hosting Gitea on a Civo VM with Caddy

A freshly-initialized Gitea dashboard, ready for its first repository.

Putting Caddy in front for automatic HTTPS

Right now, Gitea is exposed on port 3000 over plain HTTP. We'll put Caddy in front so we get HTTPS via Let's Encrypt without any certbot dance. Caddy handles certificate issuance and renewal on its own, as long as our hostname resolves to this VM.

⚠️ If you do not have a domain: nip.io is a free wildcard DNS service. Any hostname of the form <anything>.<your-ip>.nip.io resolves to <your-ip>. So if your VM's public IP is 74.220.21.5, the hostname gitea.74.220.21.5.nip.io will Just Work, and Let's Encrypt issues real certificates for it happily.

Pick a hostname and stash it in a shell variable so every command below stays copy-paste-able. Run one of these on the VM:

# If you have a domain:
HOST=gitea.example.com
# If you don't, build a nip.io hostname from this VM's public IP:
HOST=gitea.$(curl -s ifconfig.me).nip.io
echo "Using HOST=$HOST"

Confirm DNS is pointing the right way. From your local machine:

dig +short "$HOST"

You should see the public IP of your teapot VM. If you don't (and you're using your own domain), wait a few minutes for DNS propagation before continuing. Let's Encrypt's first request will fail otherwise. With nip.io, this resolves instantly with no DNS configuration on your end.

Install Caddy from the official Cloudsmith repository:

sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https curl
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' \
| sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' \
| sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo apt update && sudo apt install caddy -y

Write the Caddyfile, expanding $HOST into it:

sudo tee /etc/caddy/Caddyfile > /dev/null <<EOF
${HOST} {
reverse_proxy 127.0.0.1:3000
}
EOF

That's the entire reverse proxy configuration. Caddy will request, install, and renew a Let's Encrypt certificate automatically the first time the site is hit. Reload Caddy to pick up the new config:

sudo systemctl reload caddy

We also need to tell Gitea about its new public URL so links it generates (clone URLs, webhook URLs, password reset emails) use HTTPS. Patch /etc/gitea/app.ini in place, again using $HOST:

sudo sed -i \
-e "s|^ROOT_URL\s*=.*|ROOT_URL = https://${HOST}/|" \
-e "s|^DOMAIN\s*=.*|DOMAIN = ${HOST}|" \
/etc/gitea/app.ini

Restart Gitea to apply the change:

sudo systemctl restart gitea

Heading over to gitea.<yourinstanceip>.nip.io, you should be greeted with the following screen

Self-hosting Gitea on a Civo VM with Caddy

Gitea is reachable on the custom domain with a valid certificate

Locking down port 3000 with UFW

Caddy is doing the public-facing work now, so there's no reason to leave port 3000 open to the internet. Let's tighten things up with UFW.

sudo apt install ufw -y
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable

We're allowing SSH on 22, HTTP on 80 (Caddy uses it for the ACME challenge and to redirect visitors to HTTPS), and HTTPS on 443. Everything else, including 3000, is now refused from outside the VM, so direct connections to Gitea have to go through Caddy.

Confirm the firewall is active:

sudo ufw status

Closing thoughts

We've now got a self-hosted Gitea instance running on a Civo VM, fronted by Caddy with auto-renewing TLS and locked down behind a UFW firewall. From here, you might:

  • Push your first repository to it, create a repo in the Gitea UI, then git remote add origin git@gitea.example.com:<you>/<repo>.git from your local machine
  • Set up Gitea Actions runners to run CI on the same VM
  • Mirror an existing GitHub repo into Gitea for a quick backup
  • Take regular snapshots of /var/lib/gitea/ and your SQLite database so you can recover from a bad day

If you'd like to keep an eye on the VM itself, our Monitoring a Linux VM with Prometheus and Grafana tutorial is a natural next step. 

Jubril Oyetunji
Jubril Oyetunji

Technical Writer at Civo

Jubril Oyetunji is a DevOps engineer and technical writer with a strong focus on cloud-native technologies and open-source tools. His work centers on creating practical tutorials that help developers better understand platforms such as Kubernetes, NGINX, Rust, and Go.

As a contract technical writer, Jubril authored an extensive library of technical guides covering cloud-native infrastructure and modern development workflows. Many of his tutorials achieved strong search rankings, helping developers around the world learn and adopt emerging technologies.

View author profile