This is a minimal Forgejo setup for a VPS using Docker Compose, Traefik, Let's Encrypt, and optional Forgejo Actions runner support.
The compose stacks are stored under /opt/stacks so they can also be managed later with Dockge.
Replace every your-domain.com placeholder before deploying.
| Hostname | Purpose | DNS proxy mode |
|---|---|---|
forgejo.your-domain.com |
Forgejo web UI | Proxied or DNS only |
gitssh.your-domain.com |
Git over SSH on port 2222 |
DNS only |
dockge.your-domain.com |
Optional Dockge UI | Proxied or DNS only |
For Cloudflare, keep gitssh.your-domain.com as DNS only. The orange-cloud proxy does not proxy normal SSH traffic unless you use a separate TCP proxy product such as Cloudflare Spectrum.
Create DNS records that point to your VPS public IP address:
A forgejo <VPS_PUBLIC_IPV4>
A gitssh <VPS_PUBLIC_IPV4>
A dockge <VPS_PUBLIC_IPV4> # optional, if you use Dockge
Add matching AAAA records only if the VPS really has public IPv6 configured and your firewall allows it.
Open HTTP, HTTPS, and the Forgejo SSH port:
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 2222/tcp
sudo ufw reloadBoth stacks use the same external Docker network:
docker network create proxyIf the network already exists, Docker will print an error. That is fine.
Use Dockge-compatible stack folders:
/opt/stacks/
├── reverse-proxy/
│ ├── compose.yaml
│ └── letsencrypt/
│ └── acme.json
└── forgejo/
├── compose.yaml
├── .env
└── data/
Dockge uses /opt/stacks as its stack directory in the common setup. If Dockge is configured with DOCKGE_STACKS_DIR=/opt/stacks, each direct subfolder that contains a compose.yaml can be shown as a stack.
You can still deploy everything with the Docker CLI first. Dockge can pick up the same stack folders later. If Dockge is already running while you add the files, refresh the Dockge UI or restart Dockge if the new stacks do not appear immediately.
Dockge will not automatically start newly copied stacks just because the files exist. Start them with docker compose up -d or from the Dockge UI.
Create the reverse proxy stack directory:
mkdir -p /opt/stacks/reverse-proxy
cd /opt/stacks/reverse-proxy
# copy reverse-proxy/compose.yaml here
mkdir -p letsencrypt
touch letsencrypt/acme.json
chmod 600 letsencrypt/acme.json
docker compose up -dThis gist uses Traefik's built-in Let's Encrypt ACME support. That is usually simpler than running Certbot separately.
If you already use Certbot on the VPS, choose one certificate manager only. Do not let Certbot and Traefik ACME manage the same hostnames at the same time. With Certbot, you would normally mount the renewed certificate files into Traefik and configure them through Traefik's file provider instead of enabling the certresolver=le labels.
Create the Forgejo stack directory:
mkdir -p /opt/stacks/forgejo
cd /opt/stacks/forgejo
# copy forgejo/compose.yaml and forgejo/.env.example here
cp .env.example .env
sed -i "s/change-me-generate-a-long-random-secret/$(openssl rand -hex 32)/" .env
docker compose up -dThen open:
https://forgejo.your-domain.com
During the first Forgejo setup, verify that the external URL is:
https://forgejo.your-domain.com/
This compose enables Forgejo's built-in SSH server and publishes it on host port 2222.
Clone URLs should look like this:
git clone ssh://git@gitssh.your-domain.com:2222/USER/REPO.gitIf SSH does not work, check:
sudo ufw status numbered
ss -tulpen | grep 2222
docker logs forgejo --tail=100The runner service is included but needs to be registered once before it can work.
In Forgejo, create a runner registration token, then run something like:
cd /opt/stacks/forgejo
docker compose run --rm runner forgejo-runner register \
--no-interactive \
--instance http://server:3000 \
--name docker-runner \
--token <RUNNER_REGISTRATION_TOKEN> \
--labels docker:docker://docker:latest
docker compose up -d runnerThe runner mounts /var/run/docker.sock, which is convenient but powerful. Treat runner jobs as trusted code or isolate runners on a separate machine.
Before exposing the VPS to the internet, verify that only the required ports are publicly reachable.
This deployment only needs these public ports:
22/tcp SSH access to the VPS
80/tcp HTTP for Traefik and Let's Encrypt HTTP challenge
443/tcp HTTPS through Traefik
2222/tcp Forgejo Git SSH, only if Git over SSH is enabled
Do not expose application UI ports directly on the VPS IP address.
Dockge should be reachable only through the configured HTTPS domain, for example:
https://dockge.your-domain.com
Do not expose Dockge directly on the VPS IP:
http://<vps-ip>:5001
When Dockge is connected to the same Docker network as Traefik, it does not need a public ports: mapping.
Avoid this:
ports:
- "5001:5001"Prefer Traefik routing to Dockge's internal container port:
labels:
- traefik.enable=true
- traefik.http.routers.dockge.rule=Host(`dockge.your-domain.com`)
- traefik.http.routers.dockge.entrypoints=websecure
- traefik.http.routers.dockge.tls.certresolver=le
- traefik.http.services.dockge.loadbalancer.server.port=5001Forgejo's web UI should be reachable through Traefik only:
https://forgejo.your-domain.com
Do not expose the Forgejo web UI directly on the VPS IP:
http://<vps-ip>:3000
Avoid this when using Traefik:
ports:
- "3000:3000"
- "2222:22"Prefer exposing only Git SSH, if needed:
ports:
- "2222:22"Run:
docker ps --format "table {{.Names}}\t{{.Ports}}"A safe result should look similar to this:
forgejo 3000/tcp, 0.0.0.0:2222->22/tcp, [::]:2222->22/tcp
dockge 5001/tcp
traefik 0.0.0.0:80->80/tcp, [::]:80->80/tcp, 0.0.0.0:443->443/tcp, [::]:443->443/tcp
Avoid public bindings like:
0.0.0.0:3000->3000/tcp
0.0.0.0:5001->5001/tcp
[::]:3000->3000/tcp
[::]:5001->5001/tcp
Run:
sudo ufw status numberedExpected public rules:
22/tcp
80/tcp
443/tcp
2222/tcp
Remove rules for direct application ports such as:
3000/tcp
5001/tcp
Delete UFW rules by number:
sudo ufw delete <rule-number>Delete higher numbered rules first because UFW renumbers rules after each deletion.
Run this from your local machine, not from the VPS:
nmap -Pn -p 22,80,443,2222,3000,5001 <vps-ip-address>Expected result:
22/tcp open
80/tcp open
443/tcp open
2222/tcp open, only if Forgejo Git SSH is used
3000/tcp closed or filtered
5001/tcp closed or filtered
- Forgejo web UI is reachable only through the HTTPS domain.
- Dockge is reachable only through the HTTPS domain, if installed.
- Port
3000is not publicly exposed. - Port
5001is not publicly exposed. - Only
22,80,443, and optionally2222are publicly reachable. -
docker psshows no unwanted0.0.0.0:<port>bindings. -
ufwhas no allow rules for direct application ports. - External
nmapconfirms direct application ports are closed or filtered.
Dockge works well for managing these Compose stacks on a VPS because it gives you a simple web UI to start, stop, inspect, and edit Docker Compose stacks without logging in over SSH for every change.
Dockge setup guide: https://ramnode.com/guides/dockge
Deploy the reverse proxy stack first, then add Forgejo as a separate stack that joins the existing proxy network. If Dockge is configured with /opt/stacks, it can pick up both /opt/stacks/reverse-proxy/compose.yaml and /opt/stacks/forgejo/compose.yaml later.
If Dockge is exposed publicly, put it behind HTTPS, use a strong password, and avoid leaving the raw Dockge port open to the internet.