Self-Updating Homepage: Git-Sync + Nginx on Kubernetes
Running a website that auto-updates from your git repo is a solved problem. Deploy git-sync as a sidecar, let it pull every few minutes, and your latest commits appear live without pod restarts.
The Setup
Goal: Push to main branch → content appears on www.r3r4um.org within 5 minutes, no manual intervention.
Architecture:
- Kubernetes Deployment with two containers: nginx + git-sync
- Shared
emptyDirvolume for git clone - git-sync sidecar syncs repo →
/git, creates symlink to worktree at/git/html - nginx serves from
/git/html - Init container handles SSH key setup and GitHub host key configuration
- NodePort 30000 exposed through reverse proxy
The Container Problem
First attempt: mount SSH key directly to /root/.ssh/id_ed25519. Sounds simple. Doesn’t work.
The issue: git-sync container doesn’t read /root/.ssh/id_ed25519 by default. It looks for keys in /etc/git-secret/ssh or requires explicit --ssh-key-file flag. Additionally, strict host key checking was failing because GitHub’s host key wasn’t in known_hosts.
The fix: Init container. Copy the key, set permissions, pre-populate known_hosts, and let git-sync reference it explicitly.
The Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: www-r3r4um-org
spec:
replicas: 1
selector:
matchLabels:
app: www-r3r4um-org
template:
metadata:
labels:
app: www-r3r4um-org
spec:
securityContext:
runAsUser: 0
# Init container: SSH key setup
initContainers:
- name: ssh-key-setup
image: busybox:latest
command:
- sh
- -c
- |
cp /ssh-secret/id_ed25519 /ssh/id_ed25519
chmod 600 /ssh/id_ed25519
# Pre-populate known_hosts with GitHub's SSH keys
echo "github.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl" > /ssh/known_hosts
volumeMounts:
- name: ssh-secret
mountPath: /ssh-secret
readOnly: true
- name: ssh-config
mountPath: /ssh
containers:
# git-sync sidecar
- name: git-sync
image: registry.k8s.io/git-sync/git-sync:v4.4.0
args:
- --repo=git@github.com:r3r4um-code/www-r3r4um-org.git
- --ref=main
- --root=/git
- --period=300s
- --link=html
- --ssh-key-file=/ssh/id_ed25519
- --ssh-known-hosts-file=/ssh/known_hosts
volumeMounts:
- name: git-volume
mountPath: /git
- name: ssh-config
mountPath: /ssh
readOnly: true
# nginx web server
- name: nginx
image: nginx:latest
ports:
- containerPort: 80
volumeMounts:
- name: git-volume
mountPath: /git
- name: nginx-config
mountPath: /etc/nginx/conf.d
volumes:
- name: git-volume
emptyDir: {}
- name: ssh-secret
secret:
secretName: git-ssh-key
defaultMode: 0400
- name: ssh-config
emptyDir: {}
- name: nginx-config
configMap:
name: nginx-config
---
apiVersion: v1
kind: Service
metadata:
name: www-r3r4um-org
spec:
type: NodePort
selector:
app: www-r3r4um-org
ports:
- port: 80
targetPort: 80
nodePort: 30000
The init container:
- Copies the private SSH key from the Kubernetes Secret
- Sets correct permissions (chmod 600)
- Pre-populates
/ssh/known_hostswith GitHub’s public host keys - Runs as root, finishes before git-sync starts
git-sync sidecar:
- Uses command-line arguments instead of environment variables (more reliable)
--period=300s= sync every 5 minutes--link=html= create symlink from the git worktree to/git/html--ssh-key-fileand--ssh-known-hosts-filepoint to init container’s setup
nginx:
- Serves from
/git/html(the symlink git-sync creates) - ConfigMap provides nginx config
The Secrets
SSH key lives in a Kubernetes Secret:
kubectl create secret generic git-ssh-key \
--from-file=id_ed25519=/path/to/your/private/key
Then add the public key as a Deploy Key on GitHub:
ssh-keygen -y -f /path/to/your/private/key
# Copy the output and paste into GitHub → Settings → Deploy Keys
Deploy keys are read-only by default and limited to one repo. Safer than personal access tokens.
The Reverse Proxy
The Kubernetes NodePort (30000) is internal. To expose it on a real domain:
Caddyfile:
www.r3r4um.org {
reverse_proxy http://worker-1:30000 \
http://worker-2:30000 \
http://worker-3:30000
}
Caddy handles HTTPS automatically (Let’s Encrypt), redirects HTTP → HTTPS, and load-balances across all worker nodes.
The Workflow
To update your homepage:
- Edit
index.htmllocally - Commit and push to main branch
- Wait up to 5 minutes
- Refresh www.r3r4um.org
No pod restarts. No manual syncing. No rolling deployments. Just push and wait.
What Could Go Wrong
git-sync not syncing?
- Check
kubectl logs deployment/www-r3r4um-org -c git-sync - Verify SSH key is added as a Deploy Key on GitHub (not just your personal key)
- Make sure known_hosts is pre-populated (strict host key checking is on by default)
nginx serving old content?
- git-sync creates a symlink; nginx follows it automatically
- If git-sync hasn’t synced recently, check logs for authentication errors
- If synced but nginx stale, wait for the 5-minute sync cycle
SSH key permission errors?
- Init container must run as root and set chmod 600
- Secret volume must have defaultMode: 0400 (read-only)
- Check
kubectl execinto the pod and verify/ssh/id_ed25519exists with correct permissions
Lessons
Init containers are underrated. They run once, do setup work, and get out of the way. Perfect for SSH key configuration.
Pre-populate known_hosts. git-sync won’t add GitHub’s host key automatically. Do it in the init container or disable strict host key checking (less secure, but faster).
Command-line args > environment variables. git-sync’s ENV variable parsing is fragile (missing units, misspellings). Use
args:in the pod spec instead.Symlinks work. git-sync’s
--linkflag creates a symlink to the current worktree. nginx serves right through it. No extra configuration needed.NodePort + reverse proxy. Don’t expose Kubernetes NodePorts directly. Put a reverse proxy in front, handle HTTPS there, and load-balance across multiple nodes.
This setup has been running www.r3r4um.org since 2026-02-14. Push a commit to the repo, wait 5 minutes, and the site updates. Simple and effective.