Forgejo Setup
Self-hosted Git server via Docker, exposed through Pangolin/Traefik with SSH cloning support.
Docker Compose
services:
forgejo:
image: codeberg.org/forgejo/forgejo:15
container_name: forgejo
restart: unless-stopped
environment:
- USER_UID=1000
- USER_GID=1000
ports:
- 7003:3000
- 2020:22
volumes:
- ${CONFIG_PATH}/forgejo:/etc/gitea
- ${DATA_PATH}/forgejo:/data
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
$CONFIG_PATH— storesapp.iniand custom templates$DATA_PATH— stores repositories, LFS, SQLite database, logs
First Run: Install Wizard
Navigate to http://yourhost:7003 and configure:
| Field | Value |
|---|---|
| Database type | SQLite3 (fine for personal use) |
| Repository root path | /data/gitea/repositories |
| Server domain | git.yourdomain.com |
| SSH server port | 2020 (external/mapped port, used for clone URLs) |
| HTTP listen port | 3000 (internal container port) |
| Base URL | https://git.yourdomain.com/ |
Expand Administrator account settings and create your admin user before clicking Install. Otherwise the first person to register becomes admin.
The config is saved to /data/gitea/conf/app.ini.
Pangolin / Traefik Exposure
Forgejo is exposed via Pangolin running on a Hetzner VPS. HTTP goes through Pangolin’s normal tunnel. SSH requires additional Traefik TCP routing.
1. Expose port 2020 on the Gerbil container
In /opt/docker/pangolin/docker-compose.yml, add port 2020 to the gerbil service:
gerbil:
...
ports:
- 51820:51820/udp
- 21820:21820/udp
- 443:443
- 80:80
- 2020:2020/tcp # <-- add this
2. Add the entrypoint to Traefik static config
In /opt/docker/pangolin/config/traefik/traefik_config.yml, add under entryPoints:
entryPoints:
web:
address: ":80"
websecure:
address: ":443"
...
forgejo-ssh:
address: ":2020"
3. Add the TCP router to Traefik dynamic config
In /opt/docker/pangolin/config/traefik/dynamic_config.yml, add to the existing tcp section:
tcp:
routers:
forgejo-ssh:
rule: "HostSNI(`*`)"
entryPoints:
- forgejo-ssh
service: forgejo-ssh-service
services:
forgejo-ssh-service:
loadBalancer:
servers:
- address: "192.168.50.203:2020" # homelab LAN IP, reachable via WireGuard tunnel
Note: Traefik runs with
network_mode: service:gerbil, so ports must be declared on thegerbilcontainer, not Traefik directly. The homelab is reachable from the VPS via the WireGuard peer covering192.168.50.0/24.
4. Allow port 2020 through UFW on the VPS
ufw allow 2020/tcp
5. Restart the Pangolin stack
cd /opt/docker/pangolin
docker compose down && docker compose up -d
Use
down && uprather thanrestart— static Traefik config changes (entrypoints) require a full restart to take effect.
Verify SSH
Test SSH connectivity:
ssh -T -p 2020 git@git.yourdomain.com
# Expected: Hi <username>! You've successfully authenticated...
Clone a repo:
git clone ssh://git@git.yourdomain.com:2020/user/repo.git
Troubleshooting
| Symptom | Likely cause |
|---|---|
| HTTPS clone redirects to Pangolin login | Set the Forgejo resource to public in Pangolin — Forgejo handles its own auth |
| SSH hangs | Port not open in UFW or not exposed on Gerbil |
| SSH connection refused | Traefik not listening — check entrypoint in static config, restart with down && up |
| SSH connection reset | Wrong backend address in TCP service, or WireGuard tunnel not routing the port |
manifest unknown on pull | Tag doesn’t exist — use 15 not latest |