Self-host Gitea with Docker, PostgreSQL and HTTPS
Run your own Git server with Gitea and PostgreSQL in Docker Compose, Git over SSH on port 2222, HTTPS from Caddy, no open sign-ups, and backups you can restore.
Tested on Ubuntu 24.04 LTS
Before you start
- A server with Docker Engine and the Compose plugin
- A domain or subdomain for Gitea
- Root or a user with sudo
- An SSH key on your computer
To self-host Gitea, run the official gitea image with a PostgreSQL container in Docker Compose, keep the web port on 127.0.0.1 behind Caddy for HTTPS, and publish Gitea's built-in SSH server on port 2222 for git push over SSH. Configure it through environment variables with sign-ups turned off, and create the admin account from the command line, so no setup page is ever open to the internet.
Gitea is a lightweight, open-source Git service (MIT license) with repositories, pull requests, issues, a package registry and Gitea Actions for CI. A small server runs it for a team.
| What | In this tutorial |
|---|---|
| The domain | git.example.com |
| The server's public IPv4 address | 203.0.113.10 |
| Git over SSH | port 2222 |
| The project directory | /opt/gitea |
| Version | Gitea 28.2.0 (after 1.27, Gitea moved to whole-number major versions) |
Choose how Git over SSH reaches Gitea
The server's own SSH daemon already uses port 22. Gitea's container runs its own SSH server, and there are two ways to reach it:
| Option | Clone URL | Effort |
|---|---|---|
| A second port (this tutorial) | ssh://git@git.example.com:2222/maya/app.git |
Publish one port, open it in the firewall |
| SSH passthrough on port 22 | git@git.example.com:maya/app.git |
A git user on the host whose SSH logins are forwarded into the container; more moving parts, and host SSH changes |
Port 2222 is simpler and keeps the host's SSH untouched. Gitea shows the right clone URL on every repository page, so users copy it rather than type it. HTTPS clones work either way.
Point the domain and open the ports
Add an A record (and AAAA, if the server has IPv6) for git.example.com pointing at the server, then open HTTP, HTTPS and Gitea's SSH port:
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 2222/tcpIf the server sits behind a panel firewall, allow the same ports there.
Write the Compose file
sudo mkdir -p /opt/gitea
cd /opt/gitea
sudo sh -c 'echo "POSTGRES_PASSWORD=$(openssl rand -hex 24)" > .env'
sudo chmod 600 .envCreate /opt/gitea/compose.yaml:
services:
gitea:
image: docker.gitea.com/gitea:28.2.0
restart: unless-stopped
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:?set POSTGRES_PASSWORD in .env}
GITEA__server__DOMAIN: git.example.com
GITEA__server__ROOT_URL: https://git.example.com/
GITEA__server__SSH_DOMAIN: git.example.com
GITEA__server__SSH_PORT: "2222"
GITEA__security__INSTALL_LOCK: "true"
GITEA__service__DISABLE_REGISTRATION: "true"
volumes:
- gitea_data:/data
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
ports:
- "127.0.0.1:3000:3000"
- "2222:22"
depends_on:
db:
condition: service_healthy
db:
image: postgres:18.6-alpine
restart: unless-stopped
environment:
POSTGRES_USER: gitea
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env}
POSTGRES_DB: gitea
volumes:
- db_data:/var/lib/postgresql
healthcheck:
test: ["CMD", "pg_isready", "-U", "gitea", "-d", "gitea"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
volumes:
gitea_data:
db_data:What the settings do:
GITEA__section__KEYvariables are written into Gitea'sapp.iniat start, so the whole configuration lives in this file. Any config cheat sheet setting works this way.ROOT_URL,SSH_DOMAINandSSH_PORTare what Gitea puts in clone URLs and emails.SSH_PORTis the port users connect to (2222), while the container listens on 22.INSTALL_LOCK: "true"skips the web installer. Without it, the first visitor to the site sees a setup page that can change the database and create the admin.DISABLE_REGISTRATION: "true"turns off public sign-ups. You create accounts as admin. Public Gitea servers with open registration attract spam accounts quickly.127.0.0.1:3000:3000keeps the web port off the internet (Docker's published ports bypass ufw); Caddy reaches it on localhost. Port 2222 is published on purpose.- The database has no published port: Gitea reaches it as
dbon the project's network.
Start Gitea and create the admin
sudo docker compose up -d
sudo docker compose psAfter half a minute both containers are up, db as (healthy). Create your admin account with Gitea's CLI, as the git user inside the container:
sudo docker compose exec -u git gitea gitea admin user create --admin --username maya --email maya@example.com --random-passwordIt prints generated random password is '...' and New user 'maya' has been successfully created!. Gitea asks you to change that password at first login.
Check it worked
curl -s http://127.0.0.1:3000/api/v1/versionIt prints {"version":"28.2.0"}.
Put Caddy in front for HTTPS
Install Caddy from its repository:
sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https curl gpg
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 -y caddyReplace /etc/caddy/Caddyfile with:
git.example.com {
reverse_proxy 127.0.0.1:3000
}sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddyCaddy gets a Let's Encrypt certificate and has no upload size limit by default, so large pushes over HTTPS work. If you already run Caddy for other sites, add the block to the existing Caddyfile instead; Caddy as a reverse proxy for several apps shows the layout.
Check it worked
Open https://git.example.com, sign in as maya and set a new password. Then turn on two-factor login under Settings, Security.
Push over SSH
In Gitea, add your public key under Settings, SSH / GPG Keys, create a repository, and push to it from your computer:
git remote add origin ssh://git@git.example.com:2222/maya/hello.git
git push -u origin mainTest the key on its own with:
ssh -p 2222 git@git.example.comGitea answers Hi there, maya! You've successfully authenticated with the SSH key named ... and closes the connection: it doesn't give out a shell.
To skip the port in every URL, add this to ~/.ssh/config on your computer, after which git@git.example.com:maya/hello.git works too:
Host git.example.com
Port 2222
User gitBack it up
gitea dump writes the repositories, attachments, LFS objects, app.ini and a SQL dump of the database into one archive:
cd /opt/gitea
sudo docker compose exec -u git -w /tmp gitea gitea dump --type tar.gz --file /tmp/gitea-dump.tar.gz
sudo docker compose cp gitea:/tmp/gitea-dump.tar.gz /root/gitea-$(date +%F).tar.gz
sudo docker compose exec -u git gitea rm /tmp/gitea-dump.tar.gzAlso take a native PostgreSQL dump, which is the easiest thing to restore into a fresh database:
sudo docker compose exec -T db pg_dump -U gitea -d gitea --format=custom > /root/gitea-db-$(date +%F).dumpKeep compose.yaml and .env with them, and copy everything off the server. Gitea's backup and restore guide describes restoring a dump into a Docker install. Back up your server covers the nightly copy.
Update
Read the release notes, back up, change the tag in compose.yaml (for example 28.2.0 to the next release), then:
cd /opt/gitea
sudo docker compose pull
sudo docker compose up -d
sudo docker compose logs -f giteaGitea migrates its database on start. Don't skip several major versions at once on a production server; step through them.
Troubleshooting
ssh: connect to host git.example.com port 2222: Connection refused or a timeout. Port 2222 isn't open in ufw or in a firewall in front of the server, or the container isn't running. Check sudo ufw status and sudo docker compose ps.
git@git.example.com: Permission denied (publickey). You connected to the server's own SSH on port 22, which doesn't know Gitea's users. Use ssh://git@git.example.com:2222/... or the ~/.ssh/config entry above. If the port is right, the key isn't added to your Gitea account.
Clone URLs show localhost:3000 or the wrong port. ROOT_URL, SSH_DOMAIN or SSH_PORT don't match. Fix them in compose.yaml and run sudo docker compose up -d (a restart keeps the old values).
Gitea logs password authentication failed for user "gitea". The database volume was created with a different password, and PostgreSQL only reads POSTGRES_PASSWORD when the volume is empty. Restore the old password in .env.
The sign-up page says "Registration is disabled". That's the setting doing its job. As admin, add users under Site Administration, User Accounts, or with gitea admin user create.
Next steps
- Watch Gitea from outside with Uptime Kuma.
- Put several apps behind one proxy with Docker Compose and a reverse proxy.
- Read the official Gitea Docker installation guide.