# FileBrowser + Webpage deployment for felhom.eu --- apiVersion: v1 kind: PersistentVolumeClaim metadata: name: filebrowser-files namespace: felhom-system spec: accessModes: - ReadWriteMany storageClassName: longhorn resources: requests: storage: 1Gi --- apiVersion: v1 kind: PersistentVolumeClaim metadata: name: filebrowser-db namespace: felhom-system spec: accessModes: - ReadWriteOnce storageClassName: longhorn resources: requests: storage: 100Mi --- apiVersion: v1 kind: ConfigMap metadata: name: filebrowser-config namespace: felhom-system data: .filebrowser.json: | { "port": 8080, "baseURL": "", "address": "0.0.0.0", "log": "stdout", "database": "/database/filebrowser.db", "root": "/srv" } --- # =================== # NGINX CONFIG FOR CLEAN URLs # =================== apiVersion: v1 kind: ConfigMap metadata: name: nginx-config namespace: felhom-system data: default.conf: | server { listen 80; server_name _; root /usr/share/nginx/html/current/website; index index.html; # Enable clean URLs - serve .html files without extension location / { try_files $uri $uri.html $uri/ =404; } location = /sitemap.xml { types { application/xml xml; } default_type application/xml; } # Host-install script. It lives at the repo's /scripts (outside the website doc-root), # synced into .../current/scripts by git-sync (see the sparse-checkout ConfigMap). Served # as text/plain so operators can inspect it in a browser before download-then-run. # R-110: served from the INSTALLER TAG's tree, not the website's. The URL is unchanged # (https://felhom.eu/scripts/felhom-host-install.sh) — it never carried a ref, so every # producer of it (the bootstrap script, the hub's day-0 command) follows the tag with no # edit. What changed is which tree this root points at. location /scripts/ { root /usr/share/nginx/scripts/current; default_type text/plain; } # Cache static assets location ~* \.(css|js|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ { expires 7d; add_header Cache-Control "public, immutable"; } # Security headers add_header X-Frame-Options "SAMEORIGIN" always; add_header X-Content-Type-Options "nosniff" always; # Error pages error_page 404 /404.html; error_page 500 502 503 504 /50x.html; } --- # =================== # FILEBROWSER # =================== apiVersion: apps/v1 kind: Deployment metadata: name: filebrowser namespace: felhom-system labels: app: filebrowser spec: replicas: 1 strategy: type: Recreate selector: matchLabels: app: filebrowser template: metadata: labels: app: filebrowser spec: # filebrowser v2.63.13 (debian default) runs as a non-root UID by default # and can't write to PVC files left by the previous v2-alpine image (which # ran as root). Force root explicitly so the existing PVC contents are # readable + writable. (The alternative -- chown the PVC then drop perms -- # needs a one-shot initContainer; not worth the moving parts here.) securityContext: runAsUser: 0 runAsGroup: 0 containers: - name: filebrowser image: filebrowser/filebrowser:v2.63.13 # v2.63.x default config path is `/config/settings.json`; our ConfigMap # is mounted at `/.filebrowser.json`. Tell filebrowser to read it # explicitly so it picks up port 8080 (else it falls back to port 80 # and the readiness probe on 8080 fails). args: ["-c", "/.filebrowser.json"] ports: - containerPort: 8080 volumeMounts: - name: files mountPath: /srv - name: database mountPath: /database - name: config mountPath: /.filebrowser.json subPath: .filebrowser.json resources: requests: memory: "64Mi" cpu: "50m" limits: memory: "256Mi" cpu: "500m" livenessProbe: httpGet: path: /health port: 8080 initialDelaySeconds: 10 periodSeconds: 30 readinessProbe: httpGet: path: /health port: 8080 initialDelaySeconds: 5 periodSeconds: 10 volumes: - name: files persistentVolumeClaim: claimName: filebrowser-files - name: database persistentVolumeClaim: claimName: filebrowser-db - name: config configMap: name: filebrowser-config --- apiVersion: v1 kind: Service metadata: name: filebrowser namespace: felhom-system spec: selector: app: filebrowser ports: - port: 80 targetPort: 8080 --- apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: filebrowser namespace: felhom-system annotations: cert-manager.io/cluster-issuer: letsencrypt-prod nginx.ingress.kubernetes.io/proxy-body-size: "100m" spec: ingressClassName: nginx-internal tls: - hosts: - files.felhom.eu secretName: filebrowser-tls rules: - host: files.felhom.eu http: paths: - path: / pathType: Prefix backend: service: name: filebrowser port: number: 80 --- apiVersion: v1 kind: ConfigMap metadata: name: git-sync-sparse-checkout namespace: felhom-system data: # R-110: TWO sparse-checkouts, because there are now two syncs with two different refs. # The website tracks `main` (a copy edit must never need a release); /scripts/ tracks the # installer TAG (pushing the installer must never publish it). sparse-checkout: | /website/ sparse-checkout-scripts: | /scripts/ --- # =================== # WEBPAGE (nginx) # =================== apiVersion: apps/v1 kind: Deployment metadata: name: felhom-webpage namespace: felhom-system labels: app: felhom-webpage spec: replicas: 1 selector: matchLabels: app: felhom-webpage template: metadata: labels: app: felhom-webpage spec: containers: - name: nginx image: nginx:alpine ports: - containerPort: 80 volumeMounts: - name: git-data mountPath: /usr/share/nginx/html readOnly: true - name: git-data-scripts mountPath: /usr/share/nginx/scripts readOnly: true - name: nginx-config mountPath: /etc/nginx/conf.d/default.conf subPath: default.conf resources: requests: memory: "32Mi" cpu: "10m" limits: memory: "128Mi" cpu: "200m" livenessProbe: httpGet: path: / port: 80 initialDelaySeconds: 5 periodSeconds: 30 readinessProbe: httpGet: path: / port: 80 initialDelaySeconds: 3 periodSeconds: 10 # ── The WEBSITE sync — tracks `main`, unchanged cadence ────────────────────────────── # Deliberately still a branch: the site is content, and a typo fix must reach felhom.eu in # thirty seconds without cutting a release. Only /scripts/ moved to a tag (R-110). - name: git-sync image: registry.k8s.io/git-sync/git-sync:v4.4.0 args: - --repo=https://gitea.dooplex.hu/admin/felhom.eu.git - --ref=main - --root=/git - --link=current - --period=30s # Only sync the website subdirectory - --sparse-checkout-file=/etc/git-sync/sparse-checkout volumeMounts: - name: git-data mountPath: /git - name: sparse-checkout mountPath: /etc/git-sync resources: requests: memory: "32Mi" cpu: "10m" limits: memory: "128Mi" cpu: "100m" securityContext: runAsUser: 65534 # nobody # ── The INSTALLER sync — tracks a TAG (R-110, operator ruling 2026-08-03) ───────────── # felhom-host-install.sh runs as root on a virgin machine. Before this it was served # straight from `main`, so pushing it WAS publishing it: within thirty seconds it was what # every new machine downloaded and ran, with no staging and no rollback but another push. # # Publishing is now moving this tag; rolling back is moving it back. PROVEN, not assumed: # git-sync v4.4.0 follows a tag AND notices a moved one — measured 2026-08-03 on a # throwaway sync against this very repo (`update required … local: remote:` → # `updated successfully`, one period, ~20 s). # # Bump this ref when the installer's published version changes. `hostinstall_gates.py` # gate 6 fails if this sync stops naming an `installer-v…` tag. - name: git-sync-scripts image: registry.k8s.io/git-sync/git-sync:v4.4.0 args: - --repo=https://gitea.dooplex.hu/admin/felhom.eu.git - --ref=installer-v1.25.0 - --root=/git-scripts - --link=current - --period=30s - --sparse-checkout-file=/etc/git-sync-scripts/sparse-checkout volumeMounts: - name: git-data-scripts mountPath: /git-scripts - name: sparse-checkout-scripts mountPath: /etc/git-sync-scripts resources: requests: memory: "32Mi" cpu: "10m" limits: memory: "128Mi" cpu: "100m" securityContext: runAsUser: 65534 # nobody # Init container: wait for first sync before nginx starts initContainers: # BOTH trees are seeded before nginx accepts traffic. The second one is why /scripts/ has # no 404 window across this change: a fresh pod does not become ready until the installer # tag has been checked out, exactly as the website already worked. - name: git-sync-init image: registry.k8s.io/git-sync/git-sync:v4.4.0 args: - --repo=https://gitea.dooplex.hu/admin/felhom.eu.git - --ref=main - --root=/git - --link=current - --one-time - --sparse-checkout-file=/etc/git-sync/sparse-checkout volumeMounts: - name: git-data mountPath: /git - name: sparse-checkout mountPath: /etc/git-sync securityContext: runAsUser: 65534 - name: git-sync-scripts-init image: registry.k8s.io/git-sync/git-sync:v4.4.0 args: - --repo=https://gitea.dooplex.hu/admin/felhom.eu.git - --ref=installer-v1.25.0 - --root=/git-scripts - --link=current - --one-time - --sparse-checkout-file=/etc/git-sync-scripts/sparse-checkout volumeMounts: - name: git-data-scripts mountPath: /git-scripts - name: sparse-checkout-scripts mountPath: /etc/git-sync-scripts securityContext: runAsUser: 65534 volumes: - name: git-data emptyDir: {} - name: git-data-scripts emptyDir: {} - name: nginx-config configMap: name: nginx-config - name: sparse-checkout configMap: name: git-sync-sparse-checkout items: - key: sparse-checkout path: sparse-checkout - name: sparse-checkout-scripts configMap: name: git-sync-sparse-checkout items: - key: sparse-checkout-scripts path: sparse-checkout --- apiVersion: v1 kind: Service metadata: name: felhom-webpage namespace: felhom-system spec: selector: app: felhom-webpage ports: - port: 80 targetPort: 80 --- apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: felhom-webpage namespace: felhom-system annotations: cert-manager.io/cluster-issuer: letsencrypt-prod spec: ingressClassName: nginx-internal tls: - hosts: - felhom.eu - www.felhom.eu secretName: felhom-webpage-tls rules: - host: felhom.eu http: paths: - path: / pathType: Prefix backend: service: name: felhom-webpage port: number: 80 - host: www.felhom.eu http: paths: - path: / pathType: Prefix backend: service: name: felhom-webpage port: number: 80