Deploying the gitweb browser
How to build and deploy the gitweb service to the upspin VM (instance-2). It is
a single Go binary (page templates + static assets embedded via go:embed) that
runs as a docker-compose service alongside the existing upspin stack, exactly
like vault and markdown.
Architecture at a glance
- Image:
gcr.io/cosmocr-at-upspin/gitweb:latest - Host:
instance-2(GCP,us-central1-a, COS 121), static IP34.28.234.94 - TLS host:
git.cosmocr.at— the container terminates its own TLS with a certbot cert; nginx SNI-routesgit.cosmocr.at → gitweb:8446. (DNS is a*.cosmocr.atwildcard →34.28.234.94, so no per-host record is needed.) - Runs as upspin user
git@cosmocr.at. It reads any bare repo whoseAccessfile grants itread,list; push write-back additionally needscreate,write,deleteforgit@cosmocr.aton the repo. The demo repo isgit@cosmocr.at/demo.git; third parties expose a repo by grantingread,list: git@cosmocr.at. - Config & keys live on the persistent data disk (not in the image):
/mnt/disks/data/gitweb/config— upspin config (secrets: /gitweb/secrets)/mnt/disks/data/gitweb/secrets/{public,secret}.upspinkey/mnt/disks/data/letsencrypt/live/git.cosmocr.at/— TLS cert
- Authoritative infra sources (in the
controlrepo):upspin/vm/cloud-init-cos121.yaml— the VM's cloud-init (embeds the docker-compose + nginx.conf used on boot). Edit here first, and it is only live once pushed to the instance metadatauser-datakey (see "If you changed config" — cloud-init re-applies the metadata on every boot and overwrites the/var/upspinconfig files, so anything not in the metadata is reverted on the next reboot).upspin/vm/docker-compose.vault.yml— rendered from the cloud-init for local reference; the live/var/upspin/docker-compose.ymlis written from the metadata on boot (all of vault, markdown, and gitweb live in it).upspin/nginx/nginx.conf— canonical nginx config (the SNI map).upspin/vm/deploy-git.sh— first-time end-to-end deploy script.upspin/secrets/git/— the browser's upspin config + keys (git-ignored). Note the workstation-side secrets are undersecrets/git/, but the on-VM service + data dir are namedgitweb(matching the compose service).
Prerequisites
dockerlocally (for--platform linux/amd64builds) andgcloudconfigured for projectcosmocr-at-upspin. (The Dockerfile is COPY-only over a cross-compiledCGO_ENABLED=0 GOOS=linux GOARCH=amd64binary, so the amd64 image builds on an arm workstation with no qemu.)- SSH to the VM. The deploy script defaults to
~/.ssh/google_compute_engine; override withDEPLOY_SSH_KEYto use a passphrase-less key:DEPLOY_SSH_KEY=$HOME/.ssh/nopass_claude_code ./deploy-git.sh - The
git@cosmocr.atupspin user must exist on the keyserver and be in the keyserver's Writers. (One-time; already done — seeupspin/doc/provision_user.md.)
Routine redeploy (code change)
This is the common case. Because the templates + assets are embedded, any change needs a rebuilt image.
-
Build + push the image from the gitweb repo:
make push(=
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build,docker build --platform linux/amd64, tag,docker push.) -
Pull + recreate the container on the VM:
# authenticate the Docker daemon to gcr.io sudo env HOME=/var/upspin /bin/sh /var/upspin/auth.sh # pull the new image sudo docker run --rm \ -v /var/run/docker.sock:/var/run/docker.sock \ -v /var/upspin/.docker:/root/.docker \ -v /var/upspin:/var/upspin -w /var/upspin \ docker/compose:1.29.2 -p patrick pull gitweb # recreate just the gitweb container (rm + create; see "compose recreate" note) sudo docker rm -f patrick_gitweb_1 sudo docker run --rm \ -v /var/run/docker.sock:/var/run/docker.sock \ -v /var/upspin/.docker:/root/.docker \ -v /var/upspin:/var/upspin -w /var/upspin \ docker/compose:1.29.2 -p patrick up -d --no-recreate gitweb(Re-running
deploy-git.shalso works and is idempotent; the cert step is a no-op once the cert exists — but see the certbot gotcha below.) -
Verify:
sudo docker logs --tail 5 patrick_gitweb_1 # "gitweb: serving on :8446" curl -sI https://git.cosmocr.at/git@cosmocr.at/demo.git/ # 200
If you changed config, the compose service, or nginx
This covers any change to the compose services (incl. restart: policies),
the nginx routing/SNI map, or the upspin config — i.e. anything that lives in
cloud-init-cos121.yaml.
⚠️ The instance metadata is the source of truth, not the files on the VM. On every boot, cloud-init re-applies the VM's
user-datametadata and overwrites/var/upspin/docker-compose.ymland/var/upspin/nginx/nginx.conf. A change you onlysudo cponto the VM (ornginx -s reload) works until the next reboot, then silently reverts. This is exactly how the vault service once vanished on its first reboot: it was added to the running VM and to the repo, but never pushed to the metadata. Always push the metadata.
1. Edit + push the metadata (authoritative — survives reboots):
# edit upspin/vm/cloud-init-cos121.yaml in the control repo, then from that repo:
gcloud compute instances add-metadata instance-2 --zone us-central1-a \
--metadata-from-file user-data=upspin/vm/cloud-init-cos121.yaml
Mirror the same edit into upspin/vm/docker-compose.vault.yml and
upspin/nginx/nginx.conf so the local reference copies stay identical. The push
takes effect on the next reboot; verify it landed (run on the VM):
curl -s -H "Metadata-Flavor: Google" \
http://metadata.google.internal/computeMetadata/v1/instance/attributes/user-data \
| grep -c 'gitweb:' # expect a non-zero count
2. Apply now without waiting for a reboot (optional, for a live change): copy the rendered files up and reload/recreate only the affected pieces:
# control/upspin/vm/docker-compose.vault.yml -> /var/upspin/docker-compose.yml
# control/upspin/nginx/nginx.conf -> /var/upspin/nginx/nginx.conf
sudo docker exec patrick_nginx_1 nginx -t && sudo docker exec patrick_nginx_1 nginx -s reload
# recreate any container whose service/config changed (rm + up; see note below)
These hot-apply the change immediately; step 1 is what makes it durable. Do both — they are not alternatives.
First-time setup (new VM / from scratch)
Use control/upspin/vm/deploy-git.sh, which does it all idempotently: stages the
config + keys onto the data disk, issues the TLS cert (certbot webroot via the
running nginx), updates nginx + compose, pulls the image, recreates nginx (for the
SNI route) and starts gitweb, then verifies. Manual outline:
- DNS: covered by the
*.cosmocr.atwildcard →34.28.234.94(no per-host record needed). - Config + keys onto the data disk:
/mnt/disks/data/gitweb/config+/mnt/disks/data/gitweb/secrets/*.upspinkey(keys0600, root-owned). - TLS cert (nginx must be serving
:80for the ACME challenge):sudo docker run --rm \ -v /mnt/disks/data/certbot-www:/var/www/certbot:rw \ -v /mnt/disks/data/letsencrypt:/etc/letsencrypt:rw \ certbot/certbot certonly --webroot --webroot-path=/var/www/certbot \ --register-unsafely-without-email --agree-tos --no-eff-email \ -d git.cosmocr.at - nginx route and the gitweb compose service (already in
cloud-init-cos121.yaml): SNI routegit.cosmocr.at gitweb:8446;. - Recreate nginx, then start gitweb (commands as in "Routine redeploy").
Before the first run, make sure the infra edits are in cloud-init-cos121.yaml
and pushed to the metadata (see "If you changed config"): the gitweb:
compose service and the git.cosmocr.at gitweb:8446; line in the nginx map.
Gotchas / troubleshooting
-
Config on the VM reverts on reboot if it isn't in the instance metadata. cloud-init re-applies
user-dataon every boot and overwrites the/var/upspinconfig files. Asudo cp/nginx -s reloadonly holds until the next reboot. Push every infra change withgcloud compute instances add-metadata … --metadata-from-file user-data=cloud-init-cos121.yaml(see "If you changed config"). Symptom of a miss: after a reboot the service is gone fromdocker-compose.yml(compose logsFound orphan containers …) or nginx returnsno host in upstream "". -
certbot step must stay non-interactive. The
certonlyinvocation runs with--keep-until-expiring -n, so on a re-run where the cert already exists it's a silent no-op instead of prompting1: Keep / 2: Renew. Without those flags it would block on stdin and die withEOFError, aborting the script (set -e) before the pull/recreate steps — don't remove them. (deploy-markdown.shanddeploy-vault.shcarry the same flags.) -
Compose
command:is flags only. The imageENTRYPOINTis already/bin/gitweb; do not repeat/bin/gitwebin the composecommand:. If you do, Go's flag parser stops at that leading positional and silently ignores all flags (including-tls_cert), so it serves plain HTTP → clients get a TLS "wrong version number" through nginx. -
Compose recreate
KeyError: ContainerConfig. docker-compose 1.29.2 on COS's Docker errors on in-place recreate. Alwaysdocker rm -f <container>first, thenup -d --no-recreate. (This is also why a redeploy with a new image mustdocker rm -f patrick_gitweb_1—upalone with--no-recreatekeeps the old container/image.) -
nginx network aliases (hairpin NAT). GCP does not let a VM reach its own external IP, so the gitweb container can't talk to
upspin.cosmocr.at/key.cosmocr.atvia the public IP. The shared nginx service carries those as networkaliases, so the gitweb upspin client resolves them to nginx and routes back in via SNI. Don't remove them. -
Reading other users'
ee-encrypted files. Aread,listgrant lets gitweb list and fetch a repo, but anee-packed (encrypted) object is only readable if a key was wrapped forgit@cosmocr.at— the owner must runupspin share -fix -r <repo>after granting.plain/eeintegrityobjects render with just the grant. Objects gitweb can't decrypt surface as404. -
TLS cert. nginx must be serving
:80for the certbot webroot ACME challenge; the cert lives at/mnt/disks/data/letsencrypt/live/git.cosmocr.at/and the container hot-reloads it via thecertReloader(no restart on renewal). -
Restart policy is
unless-stopped(like every service in the stack). Adocker.servicedrop-in gates the daemon on the data-disk mount, so it can't start against an unmounted disk. The store makes no upspin calls until the first request, so there's no startup readiness race.