> ## Documentation Index
> Fetch the complete documentation index at: https://docs.arct.cloud/llms.txt
> Use this file to discover all available pages before exploring further.

# Self-host Gitea on a VPS

> Deploy Gitea on an Ubuntu VPS with the official rootless container, HTTPS, Git over SSH, backups, and safe upgrades.

Arct Cloud provides the unmanaged Linux VPS for this deployment. Gitea is not preinstalled or managed by Arct Cloud. You are responsible for repositories, user access, secrets, backups, integrations, and updates.

## Choose a Plan

Gitea's [official requirements guidance](https://docs.gitea.com/) states that 2 CPU cores and 1 GB RAM are typically sufficient for small teams and projects. Repository size, Git LFS, Actions, packages, indexing, and concurrent users can increase requirements.

| Profile                           | vCPU |  RAM |       NVMe storage | Guidance                                           |
| --------------------------------- | ---: | ---: | -----------------: | -------------------------------------------------- |
| **Official small-team baseline**  |    2 | 1 GB | Workload-dependent | Typical upstream guidance, not a hard maximum      |
| **Practical Arct starting point** |    2 | 4 GB |      40 GB or more | Small teams with operational and indexing headroom |

Store Actions runners on separate, isolated machines. Runner workloads can execute repository-controlled code and should not share the Gitea application VPS.

## Before You Begin

Prepare the following:

* A fresh Ubuntu 24.04 server
* A dedicated hostname such as `git.example.com`
* An `A` record pointing the hostname to the server's public IPv4 address
* Docker Engine with the Docker Compose plugin, installed from Docker's [official Ubuntu repository](https://docs.docker.com/engine/install/ubuntu/)
* A decision about Git transport: HTTPS only, or HTTPS plus SSH on port `2222`

The guide uses Gitea's official rootless image and SQLite, which upstream supports for a simple small-team deployment. The rootless and standard images use different volume layouts and must not be swapped after installation.

## Install Gitea

<Steps>
  <Step title="Deploy and Connect">
    [Deploy an Ubuntu server](/compute/virtual-machines/deploy), then [connect over SSH](/compute/virtual-machines/connect-ssh).
  </Step>

  <Step title="Create Persistent Directories">
    ```bash theme={null}
    sudo install -d -m 0750 -o "$USER" -g "$USER" /opt/gitea
    cd /opt/gitea
    mkdir data config
    sudo chown -R 1000:1000 data config
    ```

    UID and GID `1000` are the rootless image defaults. Incorrect ownership prevents Gitea from writing its configuration or repositories.
  </Step>

  <Step title="Create the Compose File">
    Replace every occurrence of `git.example.com`, then create `/opt/gitea/compose.yaml`:

    ```yaml compose.yaml theme={null}
    name: gitea

    services:
      gitea:
        image: docker.gitea.com/gitea:1-rootless
        restart: unless-stopped
        environment:
          GITEA__database__DB_TYPE: sqlite3
          GITEA__database__PATH: /var/lib/gitea/data/gitea.db
          GITEA__server__DOMAIN: git.example.com
          GITEA__server__ROOT_URL: https://git.example.com/
          GITEA__server__SSH_DOMAIN: git.example.com
          GITEA__server__START_SSH_SERVER: "true"
          GITEA__server__SSH_PORT: "2222"
          GITEA__server__SSH_LISTEN_PORT: "2222"
          GITEA__service__DISABLE_REGISTRATION: "true"
        volumes:
          - ./data:/var/lib/gitea
          - ./config:/etc/gitea
          - /etc/timezone:/etc/timezone:ro
          - /etc/localtime:/etc/localtime:ro
        ports:
          - "127.0.0.1:3000:3000"
          - "2222:2222"

      caddy:
        image: caddy:2-alpine
        restart: unless-stopped
        depends_on:
          - gitea
        ports:
          - "80:80"
          - "443:443"
          - "443:443/udp"
        volumes:
          - ./Caddyfile:/etc/caddy/Caddyfile:ro
          - caddy_data:/data
          - caddy_config:/config

    volumes:
      caddy_data:
      caddy_config:
    ```

    The `1-rootless` tag follows Gitea's stable major channel. Port `3000` is loopback-only for diagnostics; Caddy reaches it over the private Compose network.
  </Step>

  <Step title="Configure HTTPS">
    Generate a temporary setup password hash. The command prompts without echoing the password:

    ```bash theme={null}
    sudo docker run --rm -it caddy:2-alpine caddy hash-password
    ```

    Create `/opt/gitea/Caddyfile` and replace `REPLACE_WITH_HASH` with the command output:

    ```caddy Caddyfile theme={null}
    git.example.com {
        basic_auth {
            setup REPLACE_WITH_HASH
        }
        reverse_proxy gitea:3000
    }
    ```

    Temporary Basic Authentication prevents another visitor from claiming Gitea's installer. Caddy automatically obtains and renews the TLS certificate after DNS resolves and public ports `80` and `443` are reachable.
  </Step>

  <Step title="Start the Stack">
    ```bash theme={null}
    cd /opt/gitea
    sudo docker compose config --quiet
    sudo docker compose pull
    sudo docker compose up -d
    sudo docker compose ps
    ```
  </Step>

  <Step title="Create the Administrator">
    Open `https://git.example.com` immediately and complete the installation wizard. Keep SQLite selected, confirm the site URL is `https://git.example.com/`, confirm the SSH port is `2222`, and create a uniquely named administrator with a generated password.

    Registration is disabled in the Compose environment. After signing in, enable two-factor authentication under your account security settings, add an SSH key, and confirm the installation page is no longer accessible.

    Remove the entire `basic_auth` block from `Caddyfile`, then validate and reload Caddy:

    ```bash theme={null}
    cd /opt/gitea
    sudo docker compose exec caddy caddy validate --config /etc/caddy/Caddyfile
    sudo docker compose exec caddy caddy reload --config /etc/caddy/Caddyfile
    ```

    Confirm a new private-browser session reaches Gitea without the temporary setup prompt.

    <Warning>If the site unexpectedly shows an already-completed installation or an administrator you did not create, stop the stack and rebuild from a clean server.</Warning>
  </Step>

  <Step title="Verify Web and SSH Access">
    ```bash theme={null}
    curl -fsS https://git.example.com/api/healthz
    ssh -T -p 2222 git@git.example.com
    ```

    The health endpoint should return a successful status. After you add your public key in Gitea, the SSH test should identify your account and explain that shell access is not provided.
  </Step>
</Steps>

## Firewall and Port Safety

Allow the VPS administration port and Gitea's separate Git SSH port before enabling UFW:

```bash theme={null}
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 443/udp
sudo ufw allow 2222/tcp
sudo ufw enable
sudo ufw status
```

Host SSH remains on port `22`; Git-over-SSH uses port `2222`. Keep port `3000` private. If every user clones over HTTPS, remove the `2222:2222` mapping and firewall rule and set `GITEA__server__DISABLE_SSH` to `"true"`.

Docker-published ports can bypass some UFW forwarding rules. Publish only the ports you need, use strong SSH keys, and verify exposure from another network.

## Persistent Data and Secrets

`/opt/gitea/data` contains the SQLite database, Git repositories, LFS objects, generated SSH host keys, attachments, and other application data. `/opt/gitea/config` contains `app.ini`, including generated secrets. Losing the secret key can make encrypted data such as two-factor authentication secrets unrecoverable.

Keep both directories together in every backup. Do not edit `app.ini` while Gitea is running when the same setting is managed by a `GITEA__...` environment variable. Custom Git hooks are disabled by default because enabling them permits server-side code execution; leave them disabled unless you fully trust the users granted that privilege.

## Back Up and Restore

Gitea's official guidance requires downtime for a consistent backup because repositories, the database, and files can change together. For this SQLite deployment, create a cold archive:

```bash theme={null}
cd /opt/gitea
backup_stamp="$(date -u +%Y%m%dT%H%M%SZ)"
sudo install -d -m 0700 /var/backups/gitea
sudo docker compose stop caddy gitea
sudo tar -C /opt/gitea -czf "/var/backups/gitea/gitea-${backup_stamp}.tar.gz" data config compose.yaml Caddyfile
sudo docker image inspect docker.gitea.com/gitea:1-rootless caddy:2-alpine --format '{{.RepoTags}} {{.RepoDigests}}' | sudo tee "/var/backups/gitea/images-${backup_stamp}.txt" >/dev/null
sudo docker compose start gitea caddy
sudo ls -lh /var/backups/gitea
```

Copy the archive and matching image manifest to encrypted storage outside the VPS. To test a restore, extract the archive into an empty `/opt/gitea` on a replacement server, restore ownership with `sudo chown -R 1000:1000 data config`, replace the floating image tags with the immutable digests recorded in the manifest, and verify repositories over both HTTPS and SSH before switching DNS.

For PostgreSQL or MySQL deployments, follow Gitea's [official backup and restore guide](https://docs.gitea.com/administration/backup-and-restore/) and include a native database dump in addition to repositories and configuration.

## Update and Roll Back Safely

Read the release notes and create an off-server cold backup first. Record the current image digest, then update within the stable major channel:

```bash theme={null}
cd /opt/gitea
sudo docker image inspect docker.gitea.com/gitea:1-rootless --format '{{index .RepoDigests 0}}'
sudo docker compose pull
sudo docker compose up -d
sudo docker compose ps
sudo docker compose logs --tail=100 gitea
```

Verify login, repository browsing, clone, fetch, push, webhooks, and LFS if enabled. Never switch between rootless and standard images by changing only the image tag; their layouts are incompatible. If a migration fails, restore the matching pre-update `data` and `config` archive and use the recorded compatible image digest rather than attempting a database-only downgrade.

## Troubleshooting

| Symptom                                                 | Check                                                                                                                |
| ------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| Gitea reports a permission error                        | Confirm `/opt/gitea/data` and `/opt/gitea/config` are owned by UID/GID `1000:1000`.                                  |
| Clone URLs show the wrong host or port                  | Recheck `ROOT_URL`, `SSH_DOMAIN`, `SSH_PORT`, and `SSH_LISTEN_PORT`, then recreate the container.                    |
| HTTPS works but SSH clone fails                         | Confirm port `2222/tcp` is open, the public key is added to the correct account, and the clone URL includes `:2222`. |
| Caddy cannot issue a certificate                        | Verify DNS, remove a stale `AAAA` record, check ports `80` and `443`, and inspect Caddy logs.                        |
| The web UI returns an error after an update             | Check Gitea logs, free disk space, and release-specific migration notes before restoring the pre-update backup.      |
| Pushes fail after a migration from another install type | Regenerate repository hooks as documented by Gitea after the restore.                                                |

## Official Resources

<CardGroup cols={2}>
  <Card title="Gitea Rootless Docker Install" icon="docker" href="https://docs.gitea.com/installation/install-with-docker-rootless">
    Official image layout, Compose examples, ports, and upgrade workflow.
  </Card>

  <Card title="Gitea Documentation" icon="book-open" href="https://docs.gitea.com/">
    Current installation, administration, security, and configuration reference.
  </Card>

  <Card title="Gitea Repository" icon="github" href="https://github.com/go-gitea/gitea">
    Upstream source, security policy, issues, and development history.
  </Card>

  <Card title="Gitea Releases" icon="clock-rotate-left" href="https://github.com/go-gitea/gitea/releases">
    Stable release notes, checksums, and upgrade information.
  </Card>
</CardGroup>

<Note>Gitea is developed by the Gitea project and its community. Arct Cloud is an independent infrastructure provider and is not affiliated with, sponsored by, or endorsed by Gitea.</Note>
