Accessing the Forgejo Web UI
How to open the Forgejo web interface of a parallel-services environment, whether it's running locally under Docker Compose or on AWS (EKS). Forgejo is the worked example from the parallel-services-guide.md; the same approach applies to any HTTP service in the environment.
Locally (Docker Compose)
When you run the app's docker-compose services on your machine, Forgejo is
reachable on whatever host port the compose file maps to its container port
(3000). For example, a ports: ["3080:3000"] mapping gives:
http://localhost:3080On AWS (parallel namespace)
On AWS the service runs in the isolated namespace wbsp-svc-‹tenant›-‹env› and
is internal-only — there is no public URL, and a NetworkPolicy denies all
ingress except the allow-listed apps. To reach it from your machine,
port-forward the cluster service. kubectl port-forward tunnels through the
Kubernetes API server, so it works regardless of the NetworkPolicy:
export AWS_PROFILE=<profile>
aws eks update-kubeconfig --name <cluster> --region <region> # once
kubectl -n wbsp-svc-‹tenant›-‹env› port-forward svc/forgejo 3080:3000Then open:
http://localhost:3080Git access (clone / push / pull)
Forgejo serves git over HTTP (on its web port, 3000) and SSH (on port
22). You clone/push against whatever host and port you actually reach Forgejo
on — the forwarded local port — regardless of the URL Forgejo displays in its
UI (that reflects its configured ROOT_URL).
HTTP
# Locally (web port mapped to, e.g., 3080)
git clone http://localhost:3080/‹owner›/‹repo›.git
# On AWS — forward the web port first, then clone
kubectl -n wbsp-svc-‹tenant›-‹env› port-forward svc/forgejo 3080:3000
git clone http://localhost:3080/‹owner›/‹repo›.gitPushing over HTTP requires credentials. Use a Forgejo username with a personal access token (or password) — either let git prompt, or inline it:
git clone http://‹user›:‹token›@localhost:3080/‹owner›/‹repo›.gitSSH
SSH needs the container's port 22 exposed. Locally that's whatever your compose
file maps it to (e.g. "2222:22"); on AWS, port-forward it. The SSH user is
always git, and your public key must be registered in Forgejo first
(User Settings → SSH / GPG Keys).
# Locally (SSH mapped to, e.g., 2222)
git clone ssh://git@localhost:2222/‹owner›/‹repo›.git
# On AWS — forward the SSH port, then clone
kubectl -n wbsp-svc-‹tenant›-‹env› port-forward svc/forgejo 2222:22
git clone ssh://git@localhost:2222/‹owner›/‹repo›.gitOnce cloned, the remote stays pointed at the forwarded address, so
git push/git pullwork as long as the relevantkubectl port-forwardis running. You can forward the web and SSH ports at the same time (in separate shells).
Notes
- First visit on AWS may show the install page. Don't complete it via the
web form (see Troubleshooting). Instead configure Forgejo with
FORGEJO__*environment variables in the compose service and setFORGEJO__security__INSTALL_LOCK=trueso it skips the installer entirely. - Ephemeral storage. If the environment definition sets
ephemeral_storage: true(used when the cluster has no persistent-volume provisioner), any state in the AWS Forgejo's/datavolume is lost on pod restart. With env-based config this is fine — Forgejo regenerates itsapp.inifrom the env on each start; persistent state lives in the database.
Troubleshooting
The database settings are invalid: dial tcp 127.0.0.1:5432: connect: connection refused
Forgejo is trying to reach Postgres on 127.0.0.1, but Postgres is a separate
container/pod — localhost inside Forgejo is Forgejo itself. The DB connection
is made server-side (from the Forgejo pod), so point it at the service name, not
the address you reach the UI on:
- Host:
postgres:5432(the compose service / Kubernetes Service name in the same network/namespace) — not127.0.0.1. - Username / Password / Database: the values the compose
postgresservice provisions for Forgejo (e.g.forgejo/forgejo/forgejo).
Failed to save configuration: open /data/gitea/conf/app.ini: is a directory
The installer needs to write app.ini, but in a parallel-services
environment a bind-mounted config file is mounted as a read-only ConfigMap,
so Forgejo can't write it (the read-only mount surfaces as is a directory).
Don't use the web installer. Configure Forgejo via environment variables and
remove the app.ini bind-mount, so Forgejo writes its own config into its
writable /data volume at startup:
forgejo:
environment:
- FORGEJO__database__DB_TYPE=postgres
- FORGEJO__database__HOST=postgres:5432
- FORGEJO__database__NAME=forgejo
- FORGEJO__database__USER=forgejo
- FORGEJO__database__PASSWD=forgejo
- FORGEJO__security__INSTALL_LOCK=true
- FORGEJO__server__ROOT_URL=http://localhost:3080/
volumes:
- forgejo-data:/data
# - ./docker/forgejo/app.ini:/data/gitea/conf/app.ini ← remove this lineThe Forgejo image's entrypoint runs environment-to-ini, writing those
FORGEJO__section__key values into app.ini on the writable volume; this works
both locally and in the parallel namespace. Re-apply and restart:
wbsp services up --config <env-def>.yaml --prune
kubectl -n wbsp-svc-‹tenant›-‹env› rollout restart deploy/forgejoThis reflects a parallel-services limitation: bind-mounted config files are mounted read-only, which suits files an app only reads but breaks apps that need to write their config file (like Forgejo's
app.ini). Driving config through env vars avoids the mount entirely.
Webhooks out of Forgejo (three gates, not one)
Forgejo delivering a webhook to an app running in this cluster has to pass three separate checks. They fail in different ways and only the first one names itself clearly, which is why a webhook can look like a single-setting problem and be three.
- Forgejo's own allow-list.
webhook.ALLOWED_HOST_LISTdefaults toexternal, which permits public addresses and denies private ones. An in-cluster address is denied here, before any packet leaves — Forgejo says so in the delivery log ("webhook can only call allowed HTTP servers"). - Forgejo's namespace egress. The generated
egress-dns-and-intrapolicy permits DNS and this namespace only, so the connection is dropped on the way out. This one is silent: it looks like a timeout. - The destination namespace's ingress. An app namespace denies everything
except traffic from
traefikand its own gateway front. Also silent, also a timeout, and indistinguishable from (2) without checking both sides.
Fix each on its own terms:
# 1. Allow-list the exact destination host. Prefer the DNS name to a ClusterIP,
# which changes when the Service is recreated. `external` is kept so existing
# outbound webhooks to real internet endpoints still work.
kubectl -n <forgejo-ns> set env statefulset/forgejo \
FORGEJO__webhook__ALLOWED_HOST_LIST='external,<svc>.<app-ns>.svc'Forgejo reads this at startup, so setting it restarts the pod — which briefly
interrupts git for every repository on the instance. Take a backup first
(the wbsp-backup-forgejo skill) and pick your moment.
For (2) and (3), add sibling NetworkPolicies — one egress policy in the Forgejo namespace, one ingress policy in the app's namespace — rather than editing the existing ones. This matters:
egress-dns-and-intra,allow-requiredanddefault-deny-allare all generated.wbsp services uprewrites the first and a deploy rewrites the others, so an edit to any of them is silently reverted later, in exactly the invisible way the original problem failed.- NetworkPolicy rules are additive — a connection is allowed if any policy
permits it — so a sibling widens the union without touching what a generator
owns. This is the same pattern the platform itself uses for inter-app access
(
allow-inter-app-ingress/-egress). --pruneremoves only deployments, statefulsets and services, so a sibling policy survives a reconcile.
Scope both to the one host and the one port. Every repository owner on the
instance can register a webhook, so a wildcard such as *.svc would let any of
them aim one at any service in the cluster and read the reply from the delivery
log.
Live example: the allow-egress-website-webhook and
allow-ingress-forgejo-webhook policies, added 2026-09-04 so pushes refresh
wbsp.ai's variant data. Both carry a wbsp.ai/why annotation explaining
themselves.
Confirmed end to end on 2026-09-05: a push to a repository whose hook points at the in-cluster address was delivered and recorded by the destination app. Until that delivery there was no proof the three changes were sufficient — a fourth gate would have looked identical from this side. Verify from the receiving end, not from Forgejo. Forgejo's delivery log shows a green tick for anything that got a response, and a probe from inside its pod proves only that gate 1 is open; the destination's own data is the only evidence all three are.
See also: Parallel services guide · Useful commands.