# How to install Forgejo or Gitea with Docker Compose, PostgreSQL and HTTPS

> Self-host Forgejo or Gitea with Docker Compose and PostgreSQL behind Caddy HTTPS, with Git over SSH on port 2222, closed sign-ups, backups, restore and updates.

Difficulty: Intermediate\
Tested on: Ubuntu 24.04 LTS, Ubuntu 26.04 LTS, Debian 12, Debian 13

Forgejo and Gitea are lightweight, self-hosted Git forges written in Go. They give you repositories, issues, pull requests, wikis, package registries and built-in CI with Actions on a modest server. The two are closely related: Forgejo started in late 2022 as a fork of Gitea, is stewarded by the non-profit Codeberg e.V., and has been developed as a hard fork since early 2024.

This guide installs either one with Docker Compose and PostgreSQL, following each project's Docker documentation. Caddy provides HTTPS, Git over SSH runs on port 2222, open registration is switched off, and you back up with the projects' `dump` command plus a PostgreSQL dump. Where the commands differ, the guide shows a **Forgejo** and a **Gitea** tab.

## Prerequisites

- A server running **Ubuntu 26.04 LTS**, **Ubuntu 24.04 LTS**, **Debian 13** or **Debian 12** with Docker Engine and the Compose plugin. Follow [Install Docker on Ubuntu](/guides/install-docker-ubuntu) or [Install Docker on Debian](/guides/install-docker-debian); Forgejo's upgrade guide asks for Docker 20.10.6 or later. The commands below assume your user may run `docker` without `sudo`.
- A non-root user with `sudo` rights and SSH key login, as in [Secure a new Linux server](/guides/secure-a-new-linux-server) and [Set up SSH keys](/guides/ssh-keys).
- A domain such as `git.example.com` with an A (and AAAA) record pointing at the server.
- Caddy installed from [Caddy reverse proxy](/guides/caddy-reverse-proxy), or another reverse proxy that forwards WebSocket upgrades.

| Resource | Minimum (official) | Suggested starting point |
|---|---|---|
| CPU | Not published | 2 vCPU |
| Memory | Not published | 2 GB |
| Disk | Not published | 20 GB plus the size of your repositories, LFS objects and packages |

The Docker installation pages of both projects do not publish minimum hardware requirements. The right-hand column is a conservative starting point for a small team, not an official or benchmarked figure. Actions runners, large repositories and package registries need more.

## Step 1 — Choose Forgejo or Gitea

| | Forgejo | Gitea |
|---|---|---|
| Image in the official Docker guide | `codeberg.org/forgejo/forgejo:16` | `docker.gitea.com/gitea:28.1.0` |
| Releases in October 2026 | 16.0.5 (stable), 15.0.9 (LTS) | 28.1.0 |
| Settings through environment variables | `FORGEJO__section__KEY` | `GITEA__section__KEY` |

Forgejo's docs use the major-version tag (`16`), which follows minor and patch releases automatically. Forgejo 16 is supported until 29 October 2026, when the next major release takes over; if you prefer fewer major upgrades, use the LTS tag `15`, supported until 15 July 2027. Gitea's docs pin a full version. Gitea dropped the `1.` prefix with 28.0.0, so the image tag `1` stays on 1.27.x; check the project's releases for the newest tag. Gitea's image is also published as `gitea/gitea` on Docker Hub.

## Step 2 — Create the project folder and the database password

Create a folder in `/opt`, generate a random PostgreSQL password and store it in a `.env` file that only your user can read:

**Forgejo**

```bash
sudo mkdir -p /opt/forgejo
sudo chown $USER:$USER /opt/forgejo
cd /opt/forgejo
echo "POSTGRES_PASSWORD=$(openssl rand -hex 32)" > .env
chmod 600 .env
```
**Gitea**

```bash
sudo mkdir -p /opt/gitea
sudo chown $USER:$USER /opt/gitea
cd /opt/gitea
echo "POSTGRES_PASSWORD=$(openssl rand -hex 32)" > .env
chmod 600 .env
```

## Step 3 — Write the Compose file and start the containers

Create `compose.yaml` in the same folder with `nano compose.yaml`. Replace `git.example.com` with your domain.

**Forgejo**

```yaml
services:
  server:
    image: codeberg.org/forgejo/forgejo:16
    container_name: forgejo
    environment:
      - USER_UID=1000
      - USER_GID=1000
      - FORGEJO__database__DB_TYPE=postgres
      - FORGEJO__database__HOST=db:5432
      - FORGEJO__database__NAME=forgejo
      - FORGEJO__database__USER=forgejo
      - FORGEJO__database__PASSWD=${POSTGRES_PASSWORD}
      - FORGEJO__server__DOMAIN=git.example.com
      - FORGEJO__server__ROOT_URL=https://git.example.com/
      - FORGEJO__server__SSH_PORT=2222
      - FORGEJO__service__DISABLE_REGISTRATION=true
    restart: always
    volumes:
      - ./forgejo:/data
      - /etc/localtime:/etc/localtime:ro
    ports:
      - "127.0.0.1:3000:3000"
      - "2222:22"
    depends_on:
      - db

  db:
    image: postgres:18
    restart: always
    environment:
      - POSTGRES_USER=forgejo
      - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
      - POSTGRES_DB=forgejo
    volumes:
      - ./postgres:/var/lib/postgresql
```
**Gitea**

```yaml
services:
  server:
    image: docker.gitea.com/gitea:28.1.0
    container_name: gitea
    environment:
      - USER_UID=1000
      - USER_GID=1000
      - GITEA__database__DB_TYPE=postgres
      - GITEA__database__HOST=db:5432
      - GITEA__database__NAME=gitea
      - GITEA__database__USER=gitea
      - GITEA__database__PASSWD=${POSTGRES_PASSWORD}
      - GITEA__server__ROOT_URL=https://git.example.com/
      - GITEA__server__SSH_PORT=2222
      - GITEA__service__DISABLE_REGISTRATION=true
    restart: always
    volumes:
      - ./gitea:/data
      - /etc/localtime:/etc/localtime:ro
    ports:
      - "127.0.0.1:3000:3000"
      - "2222:22"
    depends_on:
      - db

  db:
    image: postgres:18
    restart: always
    environment:
      - POSTGRES_USER=gitea
      - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
      - POSTGRES_DB=gitea
    volumes:
      - ./postgres:/var/lib/postgresql
```

What differs from the upstream examples, and why:

- The web port is published on `127.0.0.1:3000` only; Caddy will serve it over HTTPS. Container SSH is published on 2222 for everyone, because Git clients need it.
- `ROOT_URL` is the public HTTPS address. Forgejo also needs `DOMAIN`, from which it derives the SSH host in clone URLs; Gitea 28 no longer reads `DOMAIN` and takes the domain from `ROOT_URL`.
- `SSH_PORT=2222` changes the port shown in clone URLs. The container's SSH server keeps listening on 22 inside the container.
- `DISABLE_REGISTRATION=true` closes self-registration. Gitea 28 already defaults to it; Forgejo does not.
- Both use `postgres:18`, mounted at `/var/lib/postgresql` as Gitea's docs and the PostgreSQL image documentation require for version 18. Forgejo's example still shows `postgres:14`, which reaches end of life on 12 November 2026.
- `USER_UID` and `USER_GID` set the IDs of the `git` user in the container, which owns the data folder.

Start the stack and check it:

```bash
docker compose up -d
docker compose ps
docker compose logs server --tail 20
```

Both containers should be `running`, and the log should show the web server starting without database errors. The firewall rules for ports 80 and 443 come from the Caddy guide; Step 6 adds the Git SSH port.

## Step 4 — Add HTTPS with Caddy

Add a site block for your domain to `/etc/caddy/Caddyfile`:

```caddyfile
git.example.com {
    reverse_proxy 127.0.0.1:3000
}
```

```bash
sudo systemctl reload caddy
curl -I https://git.example.com
```

Caddy obtains a certificate and `curl` prints the response headers of the installation page over HTTPS. Caddy forwards WebSocket connections automatically, which Gitea 28 needs for live notification counts. For Nginx or Traefik, see [Nginx with Certbot](/guides/nginx-reverse-proxy-certbot) and [Traefik](/guides/traefik-reverse-proxy), and make sure WebSocket upgrade headers are forwarded.

## Step 5 — Finish the installer and create the administrator

Open `https://git.example.com` right away: until you finish, anyone can see the installation page. The database fields are already filled in from the environment variables (PostgreSQL, host `db:5432`, user and database name). Check that the base URL is `https://git.example.com/` and the SSH port is 2222. Then expand the **administrator account** section, choose a username that is not `admin`, enter your email address and a long password, and select **Install**.

Because registration is disabled, this is the moment to create the administrator. If you closed the page without one, create it on the command line; the command prints a random password:

**Forgejo**

```bash
docker exec -u git forgejo forgejo admin user create --admin --username git-admin --email admin@example.com --random-password
```
**Gitea**

```bash
docker exec -u git gitea gitea admin user create --admin --username git-admin --email admin@example.com --random-password
```

Sign in and open **Site Administration**. It shows the version and lets you create accounts for your team. If the whole instance should be private, also add `REQUIRE_SIGNIN_VIEW=true` in the `service` section (for example `FORGEJO__service__REQUIRE_SIGNIN_VIEW=true`) and run `docker compose up -d` again.

Both apps email account activation, password resets and notifications once the `mailer` section is set, for example by adding `FORGEJO__mailer__ENABLED=true`, `FORGEJO__mailer__PROTOCOL=smtp+starttls`, `FORGEJO__mailer__SMTP_ADDR=smtp.example.com`, `FORGEJO__mailer__SMTP_PORT=587` and the `FROM`, `USER` and `PASSWD` keys in the same form to the `environment` list (prefix `GITEA__` for Gitea) and running `docker compose up -d`.

> **Note**
>
> Outbound port 25 is closed by default on HyperDC VPS. For services bought for a term of 3 months or longer, it is opened on request: [open a support ticket](/guides/support-tickets). Until then, send mail through an SMTP relay on port 587.

## Step 6 — Open the firewall and test Git over SSH

Allow SSH for the server, the web ports for Caddy, and port 2222 for Git:

```bash
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 2222/tcp
sudo ufw enable
sudo ufw status verbose
```

Port 2222 is published by Docker, which bypasses ufw anyway; the rule documents your intent. Port 3000 stays unreachable from outside because it is bound to `127.0.0.1`.

Add your public SSH key in your user settings under **SSH / GPG keys**, then test from your computer and clone a repository:

```bash
ssh -T -p 2222 git@git.example.com
git clone ssh://git@git.example.com:2222/your-user/your-repo.git
```

The first command prints a short greeting confirming that you authenticated. Both projects also document SSH passthrough, which lets Git use the host's port 22; it needs extra configuration on the host.

## Back up and restore

State lives in three places: the PostgreSQL database, the data folder (`./forgejo` or `./gitea`, with repositories, attachments, LFS objects and `app.ini`), and your `compose.yaml` and `.env`. Gitea's backup documentation says the instance must be shut down for a fully consistent backup; Forgejo recommends a point-in-time snapshot or a shutdown. The routine below therefore dumps the database, stops the `server` container for a moment while it archives the files, and starts it again:

**Forgejo**

```bash
sudo mkdir -p /opt/backups && sudo chown $USER:$USER /opt/backups
cd /opt/forgejo
docker compose exec -T db pg_dump -U forgejo forgejo > /opt/backups/forgejo-db-$(date +%F).sql
docker compose stop server
sudo tar czf /opt/backups/forgejo-files-$(date +%F).tar.gz compose.yaml .env forgejo
docker compose start server
```
**Gitea**

```bash
sudo mkdir -p /opt/backups && sudo chown $USER:$USER /opt/backups
cd /opt/gitea
docker compose exec -T db pg_dump -U gitea gitea > /opt/backups/gitea-db-$(date +%F).sql
docker compose stop server
sudo tar czf /opt/backups/gitea-files-$(date +%F).tar.gz compose.yaml .env gitea
docker compose start server
```

Both projects also ship a `dump` command that packs the configuration, the data folder (`data/`), the repositories (`repos/`) and an SQL file into one zip. It is handy before upgrades and for moving to a different setup. Forgejo's upgrade guide advises against relying on the SQL file inside the zip, because loading it into a new database has known problems, so keep the `pg_dump` file as well. Both images keep their configuration at `/data/gitea/conf/app.ini`:

**Forgejo**

```bash
docker exec -u git -w /tmp forgejo forgejo dump -c /data/gitea/conf/app.ini -f /tmp/forgejo-dump.zip
docker cp forgejo:/tmp/forgejo-dump.zip /opt/backups/forgejo-dump-$(date +%F).zip
docker exec -u git forgejo rm /tmp/forgejo-dump.zip
```
**Gitea**

```bash
docker exec -u git -w /tmp gitea gitea dump -c /data/gitea/conf/app.ini -f /tmp/gitea-dump.zip
docker cp gitea:/tmp/gitea-dump.zip /opt/backups/gitea-dump-$(date +%F).zip
docker exec -u git gitea rm /tmp/gitea-dump.zip
```

Copy every backup off the server.

**Restore.** On a new server with Docker, create the empty project folder owned by your user, unpack the file archive into it, start only the database, load the SQL dump once PostgreSQL accepts connections, then start the application with the same image tag as before:

**Forgejo**

```bash
sudo mkdir -p /opt/forgejo && sudo chown $USER:$USER /opt/forgejo
sudo tar xzf /opt/backups/forgejo-files-2026-10-09.tar.gz -C /opt/forgejo
cd /opt/forgejo
docker compose up -d db
docker compose exec db pg_isready -U forgejo
docker compose exec -T db psql -U forgejo -d forgejo < /opt/backups/forgejo-db-2026-10-09.sql
docker compose up -d server
```
**Gitea**

```bash
sudo mkdir -p /opt/gitea && sudo chown $USER:$USER /opt/gitea
sudo tar xzf /opt/backups/gitea-files-2026-10-09.tar.gz -C /opt/gitea
cd /opt/gitea
docker compose up -d db
docker compose exec db pg_isready -U gitea
docker compose exec -T db psql -U gitea -d gitea < /opt/backups/gitea-db-2026-10-09.sql
docker compose up -d server
```

Repeat `pg_isready` until it reports that the server is accepting connections before you run `psql`. To restore from a `dump` zip instead, follow the restore procedure in Gitea's backup documentation, which also applies to Forgejo's Docker image: unpack the archive inside the container, copy `data/` into `/data/gitea` and `repos/` into `/data/git/repositories`, put the configuration back at `/data/gitea/conf/app.ini`, run `chown -R git:git /data`, load the database dump, and finish with `admin regenerate hooks`. Afterwards, sign in and open a few repositories, issues and attachments.

## Update Forgejo or Gitea

Take a full backup first and read the release notes. Patch and minor releases arrive by pulling the same tag again; major versions need you to change the tag in `compose.yaml`.

**Forgejo**

```bash
cd /opt/forgejo
docker exec -u git forgejo forgejo manager flush-queues
docker compose pull
docker compose up -d
docker exec -u git forgejo forgejo doctor check --all --log-file /tmp/doctor.log
```
**Gitea**

```bash
cd /opt/gitea
docker compose pull
docker compose up -d
docker compose logs server --tail 50
```

For Forgejo, flushing the queues before the update is part of the official procedure, and `forgejo doctor check --all` afterwards should report no problems. Forgejo follows semantic versioning, so only a change of the first number (for example `16` to `17`) can contain breaking changes and needs manual checks; the upgrade guide calls a backup a requirement for those. Forgejo's guide also recommends testing typical tasks in the web interface right after an upgrade, while a rollback to the backup is still painless.

For Gitea, edit the image tag to the new version, then pull and restart. The 28.0.0 release notes list breaking changes: `[server] DOMAIN` is no longer read, self-registration is off unless you enable it, Actions runs are purged after 400 days by default, and live notifications need WebSockets through the proxy. Gitea migrates the database on first start, so going back means restoring the backup.

Do not change the PostgreSQL major version (`postgres:18`) by editing the tag: a new major version needs a dump and restore.

## Troubleshooting

### The installation page appears again after a restart

The container cannot find its saved `app.ini`. Check that the volume line is `./forgejo:/data` (or `./gitea:/data`), that you start Compose from the project folder, and that `ls ./forgejo/gitea/conf/` (or `./gitea/gitea/conf/`) lists `app.ini`.

### Clone URLs show port 22 or SSH asks for a password

`SSH_PORT=2222` is missing, or your key is not added to your account. Fix the environment variable, run `docker compose up -d`, add the key in your settings, and use the `ssh://git@git.example.com:2222/...` form of the URL. Test with `ssh -T -p 2222 git@git.example.com`.

### pq: password authentication failed for user

PostgreSQL sets the password only when it initialises an empty data folder. If you changed `POSTGRES_PASSWORD` in `.env` afterwards, put the old value back, or change the password inside PostgreSQL with `ALTER USER` and keep `.env` in sync.

### The server container restarts with permission errors on /data

The data folder is owned by a different user than `USER_UID` and `USER_GID`. Forgejo's docs note that the container may not start in that case. Fix it with `sudo chown -R 1000:1000 /opt/forgejo/forgejo` (or the Gitea path), matching the IDs in `compose.yaml`.

### Notification counts do not update in Gitea 28

Gitea 28 moved live notifications to WebSockets. Caddy forwards them automatically; with Nginx, add the `Upgrade` and `Connection` headers to the location block. Otherwise Gitea falls back to polling.

## Next steps

- Prefer an all-in-one DevOps platform with its own CI/CD? See [Install GitLab CE](/guides/install-gitlab-ce).
- Deploy apps straight from your repositories with [Coolify](/guides/install-coolify).
- Review servers for code hosting on the [Gitea hosting](/gitea-hosting) page.
- Read the official [Forgejo documentation](https://forgejo.org/docs/latest/) and [Gitea documentation](https://docs.gitea.com/).

## Frequently asked questions

### Should I choose Forgejo or Gitea?

Both install the same way with Docker and PostgreSQL. Forgejo is developed under the non-profit Codeberg e.V. and offers long-term support releases. Gitea moved to version 28 in September 2026. Choose the project whose governance and release cycle suit you.

### Can I migrate from Gitea to Forgejo?

Forgejo documents a path from Gitea in its upgrade guide, which starts by moving to Forgejo v10.0.x before any newer version. Recent Gitea releases have diverged from Forgejo, so read that guide and test on a copy before you migrate.

### Why does this guide use SSH port 2222?

The server’s own SSH daemon already uses port 22. Publishing the container’s SSH server on 2222 keeps the two apart, and SSH_PORT=2222 makes the web interface show matching clone URLs. Both projects also document SSH passthrough if you prefer port 22.

### How do I stop strangers from creating accounts?

Set DISABLE_REGISTRATION=true in the service section, as the compose files in this guide do through environment variables. Gitea 28 disables self-registration by default; Forgejo allows it by default. Create users yourself in Site Administration.

### Can I use SQLite instead of PostgreSQL?

Yes. Both projects support SQLite and their Docker guides show it as the simplest option. This guide uses PostgreSQL, a common choice for teams; moving from one database to another later requires a migration.

---

Source: <https://hyperdc.com/guides/tutorials/install-gitea-forgejo>\
Updated: 2026-10-09
