Skip to content

TutorialsGit & DevOps

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.

  • Intermediate
  • 40 min read
  • Updated

Tested on: Ubuntu 24.04 LTS, Ubuntu 26.04 LTS, Debian 12, Debian 13

This guide is not available in your language yet, so it is shown in English.

On this page
  1. Prerequisites
  2. Step 1 — Choose Forgejo or Gitea
  3. Step 2 — Create the project folder and the database password
  4. Step 3 — Write the Compose file and start the containers
  5. Step 4 — Add HTTPS with Caddy
  6. Step 5 — Finish the installer and create the administrator
  7. Step 6 — Open the firewall and test Git over SSH
  8. Back up and restore
  9. Update Forgejo or Gitea
  10. Troubleshooting
  11. The installation page appears again after a restart
  12. Clone URLs show port 22 or SSH asks for a password
  13. pq: password authentication failed for user
  14. The server container restarts with permission errors on /data
  15. Notification counts do not update in Gitea 28
  16. Next steps

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 or Install Docker on 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 and Set up 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, or another reverse proxy that forwards WebSocket upgrades.
ResourceMinimum (official)Suggested starting point
CPUNot published2 vCPU
MemoryNot published2 GB
DiskNot published20 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

ForgejoGitea
Image in the official Docker guidecodeberg.org/forgejo/forgejo:16docker.gitea.com/gitea:28.1.0
Releases in October 202616.0.5 (stable), 15.0.9 (LTS)28.1.0
Settings through environment variablesFORGEJO__section__KEYGITEA__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 and Traefik, 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 [email protected] --random-password

Gitea

Bash
docker exec -u git gitea gitea admin user create --admin --username git-admin --email [email protected] --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.

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 [email protected]
git clone ssh://[email protected]: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://[email protected]:2222/... form of the URL. Test with ssh -T -p 2222 [email protected].

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

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.

Sources

Genera password

Please confirm