Step-by-step instructions for Electric Eel (24.10+): finding the DB container and network, creating the read-only user, deploying with docker compose, reverse proxying, and locating the Gitea custom dir. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
gitea-heatmap-sidecar
A tiny HTTP service that exposes daily contribution counts for an allowlisted Gitea user — including private repo activity — for rendering a GitHub-style heatmap on a public profile page.
Stock Gitea (and Forgejo) intentionally do not expose private contribution
counts to anonymous viewers. This is a sidecar workaround: read directly from
Gitea's action table with a read-only DB user, return per-day counts as
JSON, and let a custom profile template render the squares client-side.
What it shows / what it doesn't
- ✅ Daily counts (the green squares)
- ✅ Hover tooltips with the date, count, and repo names
- ❌ Commit messages, branches, file content — none of that ever leaves the database.
Architecture
[anonymous visitor] → GET /didericis (Gitea)
↓ profile.tmpl override loads
<script> fetches https://heatmap.dideric.is/heatmap/didericis.json
↓
[this service] → SELECT FROM action WHERE act_user_id=? → JSON
Setup
1. Create a read-only Postgres user
Run db/setup.sql against the Gitea database as a superuser. Edit the
password first.
psql -U postgres -d gitea -f db/setup.sql
Only SELECT on "user" and "action" is granted. If Gitea ever renames
either table in a migration, the service will break loudly — that's the goal.
2. Build and run the sidecar
Edit docker-compose.example.yml, then:
docker compose -f docker-compose.example.yml up -d --build
Make sure the networks block matches your existing Gitea Docker network so
the sidecar can reach gitea-db by hostname.
Required env vars:
| Var | Description |
|---|---|
DATABASE_URL |
postgres://heatmap_ro:...@host:5432/gitea?sslmode=... |
ALLOWED_USERS |
Comma-separated lowercase usernames (e.g. didericis) |
ALLOWED_ORIGIN |
CORS origin — must match Gitea's URL |
OP_TYPES |
Optional. Comma-separated op_type ints. See below. |
LISTEN |
Optional. Default :8080. |
3. Reverse proxy
Expose the service at a hostname Gitea's frontend can reach over HTTPS — e.g.
heatmap.dideric.is → heatmap:8080. Use the same TLS setup as Gitea
itself (Caddy/Traefik/nginx).
4. Install the profile template override
Copy templates/user/profile.tmpl from the Gitea source matching your
running version into $GITEA_CUSTOM/templates/user/profile.tmpl, then merge
in the snippet from templates/profile-snippet.tmpl near the existing
heatmap block.
Replace HEATMAP_BASE_URL in the snippet with your sidecar's public URL
(e.g. https://heatmap.dideric.is) and didericis with the username you're
exposing.
Restart Gitea, hit the profile page in incognito, and you should see the heatmap populate.
TrueNAS SCALE setup
Tested on TrueNAS SCALE 24.10 (Electric Eel) with Gitea installed via the official app catalog. All commands below run over SSH on the TrueNAS host.
1. Find Gitea's DB container and Docker network
# List Gitea-related containers
docker ps --format '{{.Names}}' | grep -i gitea
# Find the network the Gitea app container is on
docker inspect <gitea-app-container> \
--format '{{range $k,$v := .NetworkSettings.Networks}}{{$k}}{{end}}'
Note both the DB container name (typically ends in -postgres or -db) and
the network name — you'll need them in the steps below.
2. Get the Gitea DB password
In the TrueNAS UI: Apps → Installed Apps → Gitea → Edit → Database Configuration. The Postgres password is visible there. You'll use it in steps 3 and 4.
3. Create the read-only DB user
Edit db/setup.sql to set a password for heatmap_ro, then run it against
the DB container:
docker exec -i <gitea-db-container> psql -U gitea -d gitea < db/setup.sql
4. Clone and configure
Put the repo somewhere on persistent storage:
git clone https://github.com/didericis/gitea-heatmap-sidecar \
/mnt/<pool>/gitea-heatmap-sidecar
cd /mnt/<pool>/gitea-heatmap-sidecar
cp docker-compose.example.yml docker-compose.yml
Edit docker-compose.yml:
- Set
DATABASE_URL— use the DB container name as the hostname, e.g.postgres://heatmap_ro:PASSWORD@gitea-db-container-name:5432/gitea?sslmode=disable - Set
ALLOWED_USERSandALLOWED_ORIGIN - Under
networks.gitea, setnameto the network name from step 1 and uncommentexternal: true
5. Build and start
docker compose -f /mnt/<pool>/gitea-heatmap-sidecar/docker-compose.yml up -d --build
Confirm it's healthy:
docker exec gitea-heatmap wget -qO- http://localhost:8080/healthz
6. Reverse proxy
Expose port 8080 at a public HTTPS hostname. With Nginx Proxy Manager (a common TrueNAS app on the same Docker network):
- Scheme:
http, Forward hostname:gitea-heatmap(the container name), Port:8080 - Enable SSL via Let's Encrypt
For Traefik or Caddy configured as TrueNAS apps, wire it up the same way — the sidecar is reachable by container name on the shared network.
7. Find the Gitea custom directory
In the TrueNAS UI: Apps → Installed Apps → Gitea → Edit → Storage.
Find the host path mapped to the Gitea data volume. The custom directory
Gitea reads templates from is the custom/ subdirectory of that path —
check the GITEA_CUSTOM env var in the container if unsure:
docker exec <gitea-app-container> printenv GITEA_CUSTOM
Then follow step 4 of the main setup to install the template override into that directory and restart Gitea.
Op type reference
Gitea's action.op_type is an integer enum. Defaults are commits-only
(5,9,18), which is the closest analog to what GitHub counts as a
"contribution" in their heatmap. Common values:
| Value | Meaning |
|---|---|
| 1 | Create repo |
| 5 | Push commits |
| 6 | Create issue |
| 7 | Create pull request |
| 9 | Push tag |
| 11 | Merge pull request |
| 18 | Mirror sync push |
| 24 | Publish release |
Set OP_TYPES=5,6,7,9,11,18,24 for a more inclusive count.
Endpoints
GET /heatmap/{username}.json— JSON[{"date":"YYYY-MM-DD","count":N,"repos":["name", ...]}, ...]for the past ~53 weeks. 1-hour cache header.GET /healthz— 200 if the DB is reachable, 503 otherwise.
Maintenance
- Gitea schema changes. The query reads from
"action"and"user". These tables have been stable for many Gitea versions. After each Gitea upgrade, hit/healthzand the JSON endpoint to confirm. - Template drift. The harder part. Gitea's
profile.tmpldoes change between versions — diff the new upstream against your override on each upgrade and re-merge the snippet. - Cache. 1-hour
Cache-Controlheader keeps load trivial. Drop to 5 minutes for snappier updates if needed; the underlying query is cheap thanks to the existing index on(user_id, act_user_id, created_unix)added in Gitea 1.24.
Local dev
go run . # requires DATABASE_URL and ALLOWED_USERS in env
go build ./... produces a binary named gitea-heatmap-sidecar (derived from
the module name). The Dockerfile overrides this with -o /heatmap; the two
names differ but refer to the same program.
On NixOS:
nix-shell -p go_1_23 postgresql
Limitations
- Postgres only. MySQL support would mean swapping
to_timestamp(...)forFROM_UNIXTIME(...)and adjusting identifier quoting — straightforward but not done here. - Single-tenant by allowlist. The service is meant for a personal Gitea instance with a small known set of users opting in. Don't expose it to arbitrary usernames; that would leak private activity counts for users who haven't consented.
- No auth on the JSON endpoint by design — the data is intentionally public.
License
MIT