Data encrypted under one secret is unreadable under another, so the secrets must move with the data instead of being regenerated by the image.
gitea-mirror
Self-hosted Gitea backed by PostgreSQL, with gitea-mirror mirroring GitHub repositories into it.
Services
| Service | Image | Internal port | Domain |
|---|---|---|---|
db |
postgres:16-alpine |
5432 | none |
gitea |
gitea/gitea:28 |
3000 | GITEA_ROOT_URL |
gitea-mirror |
ghcr.io/raylabshq/gitea-mirror:latest |
4321 | GITEA_MIRROR_URL |
In Coolify, give gitea and gitea-mirror each a domain on their internal
port, matching the two URL variables.
gitea waits for db to pass its health check. gitea checks
/api/healthz; the gitea-mirror image ships its own health check.
Variables
| Variable | Feeds | Notes |
|---|---|---|
POSTGRES_PASSWORD |
db, gitea |
Defaults to gitea. |
GITEA_ROOT_URL |
Gitea server.ROOT_URL |
Public URL, with trailing slash. Gitea builds clone URLs and redirects from it. |
BETTER_AUTH_SECRET |
gitea-mirror | Signs sessions and encrypts its login keys. Generate with openssl rand -base64 32. |
ENCRYPTION_SECRET |
gitea-mirror | Encrypts the stored GitHub and Gitea tokens. Generate with openssl rand -base64 48. |
GITEA_MIRROR_URL |
BETTER_AUTH_URL, PUBLIC_BETTER_AUTH_URL, BETTER_AUTH_TRUSTED_ORIGINS |
Public URL of the mirror UI, no trailing slash. |
Postgres sets the password only when it first initialises db-data. Changing
POSTGRES_PASSWORD later breaks Gitea's connection until the role is altered
to match:
docker compose exec db psql -U gitea -c "ALTER USER gitea PASSWORD '<new>';"
Behind a reverse proxy, gitea-mirror rejects sign-in with "invalid origin" unless all three Better Auth variables hold the external URL, so one variable feeds them all.
Both secrets are set explicitly rather than left to the image, which would
otherwise generate its own into gitea-mirror-data. Data encrypted under one
secret is unreadable under another, so moving the data to a new deployment
means carrying the secrets with it. Never change either on an existing
install.
Choices
- HTTPS only. The proxy routes HTTP, not SSH, so Gitea's SSH server is disabled and the UI offers HTTPS clone URLs only.
gitea/gitea:28. Gitea publishes major tags; the major pin takes updates without a surprise major upgrade.gitea-mirror:latestwithpull_policy: always: upstream publishes no major tag, so every redeploy takes the newest release.postgres:16-alpinestays on 16: a new Postgres major cannot read the existing data directory without a dump and restore.
Usage
Complete Gitea's first-run setup at GITEA_ROOT_URL, create an access token,
then configure mirroring at GITEA_MIRROR_URL. In gitea-mirror, set the Gitea
URL to http://gitea:3000 so it talks to Gitea over the internal network.
Storage
| Volume | Holds |
|---|---|
db-data |
PostgreSQL data |
gitea-data |
Repositories, Gitea config and state |
gitea-mirror-data |
Mirror job database and generated secrets |