# porthole Porthole: timeline media library (Next.js web + worker), backed by Postgres/Redis/MinIO. ## How to try it - Create a values file (example minimal): - set `secrets.postgres.password` - set `secrets.minio.accessKeyId` + `secrets.minio.secretAccessKey` - set `images.web.repository/tag` and `images.worker.repository/tag` - set `global.tailscale.tailnetFQDN` (recommended), or set `app.minio.publicEndpointTs` (must be `https://minio.`) - Render locally: `helm template porthole helm/porthole -f your-values.yaml --namespace porthole` - Install (to `porthole` namespace): `helm upgrade --install porthole helm/porthole -f your-values.yaml --namespace porthole` ## ArgoCD A ready-to-apply ArgoCD `Application` manifest is included at `argocd/porthole-application.yaml` (it deploys the Helm release name `porthole`). Reference example (deploys into the `porthole` namespace; the Helm chart itself does not hardcode a namespace): ```yaml apiVersion: argoproj.io/v1alpha1 kind: Application metadata: name: porthole namespace: argocd spec: project: default source: repoURL: git@gitea-gitea-ssh.taildb3494.ts.net:will/porthole.git targetRevision: main path: helm/porthole helm: releaseName: porthole valueFiles: - values.yaml # - values-porthole.yaml # Alternative to valueFiles: set a few values inline. # parameters: # - name: global.tailscale.tailnetFQDN # value: tailxyz.ts.net # - name: images.web.repository # value: gitea-gitea-http.taildb3494.ts.net/will/porthole-web # - name: images.web.tag # value: dev destination: server: https://kubernetes.default.svc namespace: porthole # Optional: if you use image pull secrets, you can set them via values files # or inline Helm parameters. # source: # helm: # parameters: # - name: imagePullSecrets[0] # value: my-registry-secret # - name: registrySecret.create # value: "true" # - name: registrySecret.server # value: registry.lan:5000 # - name: registrySecret.username # value: your-user # - name: registrySecret.password # value: your-pass syncPolicy: automated: prune: true selfHeal: true syncOptions: - CreateNamespace=false - ApplyOutOfSyncOnly=true ``` ## Notes - MinIO bucket creation: the app does not auto-create buckets. You can either create the bucket yourself, or enable the Helm hook job: - `jobs.ensureBucket.enabled=true` - Staging cleanup: disabled by default; enable with: - `cronjobs.cleanupStaging.enabled=true` ## Build + push images (multi-arch) This repo is a Bun monorepo, but container builds use Docker Buildx. - Assumptions: - You have an **in-cluster registry** reachable over **insecure HTTP** (example: `registry.lan:5000`). - Your Docker daemon is configured to allow that registry as an insecure registry. - Create/use a buildx builder (one-time): - `docker buildx create --name porthole --use` - Build + push **web** (Next standalone): - `REGISTRY=gitea-gitea-http.taildb3494.ts.net TAG=dev` - `docker buildx build --platform linux/amd64,linux/arm64 -f apps/web/Dockerfile -t "$REGISTRY/will/porthole-web:$TAG" --push .` - Notes: - The Dockerfile uses `bun install --frozen-lockfile` and copies all workspace `package.json` files first to keep Bun from mutating `bun.lock`. - Runtime entrypoint comes from Next standalone output (the image runs `node app/apps/web/server.js`). - Build + push **worker** (includes `ffmpeg` + `exiftool`): - `REGISTRY=gitea-gitea-http.taildb3494.ts.net TAG=dev` - `docker buildx build --platform linux/amd64,linux/arm64 -f apps/worker/Dockerfile -t "$REGISTRY/will/porthole-worker:$TAG" --push .` - Notes: - The Dockerfile uses `bun install --frozen-lockfile --production` and also copies all workspace `package.json` files first for stable `workspace:*` resolution. - Then set Helm values: - `images.web.repository: gitea-gitea-http.taildb3494.ts.net/will/porthole-web` - `images.web.tag: dev` - `images.worker.repository: gitea-gitea-http.taildb3494.ts.net/will/porthole-worker` - `images.worker.tag: dev` ### Private registry auth (optional) If your registry requires auth, you can either: - reference an existing Secret via `imagePullSecrets`, or - have the chart create a `kubernetes.io/dockerconfigjson` Secret via `registrySecret`. Example values: ```yaml # Option A: reference an existing secret imagePullSecrets: - my-registry-secret # Option B: create a secret from values (stores creds in values) registrySecret: create: true server: "registry.lan:5000" username: "your-user" password: "your-pass" email: "you@example.com" ``` ## MinIO exposure (Tailscale) MinIO S3 URLs must be signed against `https://minio.`. You can expose MinIO over tailnet either via: - **Tailscale Ingress** (default), or - **Tailscale LoadBalancer Service** (often more reliable for streaming/Range) Example values (LoadBalancer for S3 + console): ```yaml global: tailscale: tailnetFQDN: "tailxyz.ts.net" minio: tailscaleServiceS3: enabled: true hostnameLabel: minio tailscaleServiceConsole: enabled: true hostnameLabel: minio-console # Optional: if you prefer explicit override instead of deriving from tailnetFQDN # app: # minio: # publicEndpointTs: "https://minio.tailxyz.ts.net" ``` ## Example values (Pi cluster) This chart assumes you label nodes like: - Pi 5 nodes: `node-class=compute` - Pi 3 node: `node-class=tiny` The default scheduling in `helm/porthole/values.yaml` pins heavy pods to `node-class=compute`. Example `values.yaml` you can start from: ```yaml secrets: postgres: password: "change-me" minio: accessKeyId: "minioadmin" secretAccessKey: "minioadmin" images: web: repository: gitea-gitea-http.taildb3494.ts.net/will/porthole-web tag: dev worker: repository: gitea-gitea-http.taildb3494.ts.net/will/porthole-worker tag: dev global: tailscale: tailnetFQDN: "tailxyz.ts.net" # Optional, but common for Pi clusters (Longhorn default shown as example) # global: # storageClass: longhorn minio: # Prefer LB Services for streaming/Range reliability tailscaleServiceS3: enabled: true hostnameLabel: minio tailscaleServiceConsole: enabled: true hostnameLabel: minio-console jobs: ensureBucket: enabled: true # Optional staging cleanup (never touches originals/**) # cronjobs: # cleanupStaging: # enabled: true # olderThanDays: 7 ``` ## Quick checks - Range support through ingress (expect `206`): - `curl -sS -D- -H 'Range: bytes=0-1023' "$(curl -sS https://app./api/assets//url?variant=original | jq -r .url)" -o /dev/null`