arcane@prod : ~/infra
online· p99 12ms· 142 rps

// infra

Infrastructure

arcane@prod:~$ cat infra/**

How this page reaches you — the actual infrastructure, read straight from the repo at build time (no hand-copied snippets, so it can never drift). A single-node k3s box behind a Cloudflare Tunnel, GitOps-deployed: push to main, CI builds the image, ArgoCD syncs the cluster.

CI/CD — build & deploy the web app

.github/workflows/web.yml

GitHub Actions builds the Next.js image, pushes it to GHCR, then pins the tag into kustomize with [skip ci]; ArgoCD on the box watches main and syncs. Push to main = deploy.

name: web image

on:
  push:
    branches: [main]
    paths:
      - "apps/web/**"
      - "packages/**"
      - "package.json"
      - "package-lock.json"
      - ".github/workflows/web.yml"
  workflow_dispatch:

concurrency:
  group: web-image
  cancel-in-progress: false

env:
  IMAGE: ghcr.io/gabrielipcarvalho/gipc-web

jobs:
  build:
    runs-on: ubuntu-latest
    permissions:
      contents: write     # to commit the GitOps tag bump
      packages: write      # to push to GHCR
    steps:
      - uses: actions/checkout@v4

      - uses: docker/setup-buildx-action@v3

      - name: Log in to GHCR
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Build & push image
        id: build
        uses: docker/build-push-action@v6
        with:
          context: .
          file: apps/web/Dockerfile
          push: true
          tags: |
            ${{ env.IMAGE }}:${{ github.sha }}
            ${{ env.IMAGE }}:latest
          cache-from: type=gha
          cache-to: type=gha,mode=max

      - name: GitOps — pin manifest to this image (deploy feed)
        id: pin
        run: |
          # rebase-retry: two workflows can race to pin on the same main push; the loser fetches the
          # winner's commit, re-applies its own tag, and retries. FETCH_HEAD is fresh on a shallow clone.
          git config user.name  "gipc-ci"
          git config user.email "[email protected]"
          for attempt in 1 2 3 4 5; do
            git fetch origin main
            git reset --hard FETCH_HEAD
            sed -i "s|newTag: .*|newTag: \"${GITHUB_SHA}\"|" infra/k8s/web/kustomization.yaml
            if git diff --quiet; then echo "already pinned"; exit 0; fi
            git commit -am "deploy(web): ${GITHUB_SHA:0:7} → gipc.dev [skip ci]"
            if git push; then echo "pinned (attempt $attempt)"; exit 0; fi
            echo "push rejected — another workflow pushed; retrying"
            [ "$attempt" -lt 5 ] && sleep $((attempt * 3))
          done
          echo "::error::failed to pin web after 5 attempts"; exit 1

      - name: Notify deploy feed
        # post whenever the image built (even if the pin retried/failed), but never on a cancelled run.
        if: ${{ !cancelled() && github.ref == 'refs/heads/main' && steps.build.outcome == 'success' }}
        env:
          DEPLOY_HOOK_KEY: ${{ secrets.DEPLOY_HOOK_KEY }}
          # released reflects the PIN outcome — a failed pin = manifest not bumped = site NOT on the new image
          RELEASED_STATUS: ${{ steps.pin.outcome == 'success' && 'success' || 'failure' }}
        run: |
          [ -z "$DEPLOY_HOOK_KEY" ] && { echo "no key"; exit 0; }
          SUBJECT=$(git log -1 --format=%s "$GITHUB_SHA"); TS=$(date -u +%FT%TZ)
          post() {   # $1 stage, $2 status — jq builds subject-safe JSON; sign the exact bytes sent
            body=$(jq -nc --arg sha "$GITHUB_SHA" --arg subject "$SUBJECT" --arg stage "$1" --arg status "$2" --arg ts "$TS" \
              '{sha:$sha,subject:$subject,stage:$stage,status:$status,ts:$ts}')
            sig=$(printf '%s' "$body" | openssl dgst -sha256 -hmac "$DEPLOY_HOOK_KEY" | awk '{print $2}')
            curl -fsS -X POST -H "X-Signature: sha256=$sig" -H 'Content-Type: application/json' --data "$body" https://gipc.dev/api/hooks/deploy || true
          }
          post build success
          post released "$RELEASED_STATUS"

Go core — Deployment, Service & config

infra/k8s/core/core.yaml

The stdlib Go backend: single replica, distroless non-root, read-only rootfs, 12-factor config via ConfigMap. The HMAC secret is referenced by name, never inlined.

apiVersion: v1
kind: ConfigMap
metadata:
  name: core-config
  namespace: gipc
data:
  PORT: "8080"
  CORS_ORIGIN: "https://gipc.dev"
  RATE_LIMIT_RPS: "10"
  RATE_LIMIT_BURST: "20"
  PROMETHEUS_URL: "http://prometheus.observability:9090"   # used from P3
  LOKI_URL: "http://loki.observability:3100"               # P6 log surface (cross-ns ClusterIP)
  WEB_URL: "http://web:80"                                  # P7 uptime probe target
  UPTIME_INTERVAL: "30s"                                    # P7 probe cadence
  SHUTDOWN_TIMEOUT: "25s"
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: core
  namespace: gipc
  labels: { app: core }
spec:
  replicas: 1
  strategy:
    type: RollingUpdate
    rollingUpdate: { maxSurge: 1, maxUnavailable: 0 }
  selector:
    matchLabels: { app: core }
  template:
    metadata:
      labels: { app: core }
    spec:
      terminationGracePeriodSeconds: 30   # ≥ SHUTDOWN_TIMEOUT so /api/* drains on a roll
      securityContext:
        runAsNonRoot: true
        runAsUser: 65532        # distroless:nonroot
        fsGroup: 65532
        seccompProfile: { type: RuntimeDefault }
      containers:
        - name: core
          image: ghcr.io/gabrielipcarvalho/gipc-core:latest   # tag pinned by CI via kustomization
          ports:
            - { containerPort: 8080, name: http }
          envFrom:
            - configMapRef: { name: core-config }
          env:
            - name: DEPLOY_HOOK_KEY   # HMAC key for /api/hooks/deploy — imperative Secret, never committed
              valueFrom:
                secretKeyRef: { name: deploy-hook, key: hmac }
          readinessProbe:
            httpGet: { path: /api/readyz, port: 8080 }
            initialDelaySeconds: 3
            periodSeconds: 10
          livenessProbe:
            httpGet: { path: /api/healthz, port: 8080 }
            initialDelaySeconds: 5
            periodSeconds: 20
          resources:
            requests: { cpu: 25m, memory: 32Mi }
            limits: { cpu: 200m, memory: 128Mi }
          securityContext:               # static binary writes nothing → full lockdown
            allowPrivilegeEscalation: false
            readOnlyRootFilesystem: true
            capabilities: { drop: [ALL] }
---
apiVersion: v1
kind: Service
metadata:
  name: core
  namespace: gipc
  labels: { app: core }
spec:
  type: ClusterIP
  selector: { app: core }
  ports:
    - { port: 8080, targetPort: 8080, name: http }

Kustomize — the image pin CI writes

infra/k8s/core/kustomization.yaml

CI rewrites the built image tag here; ArgoCD reconciles the cluster to match. This file is the deploy handshake.

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
# One app manages core + caddy. Each manifest sets its own namespace: gipc (no namespace directive here).
# Caddy's image is pinned inline in ../caddy (no images: entry), so CI's tag-pin sed matches only the
# core image line below. Keep comments free of the literal tag-key token so the sed can't match them.
resources:
  - core.yaml
  - ../caddy
images:
  - name: ghcr.io/gabrielipcarvalho/gipc-core
    newTag: "691b07dfe1e6e378f705c9326d42e3e886c01714"

Caddy — reverse proxy & metrics

infra/k8s/caddy/caddy.yaml

Fronts the cluster inside k3s: routes /api/* to core (SSE-safe, flush-immediate), everything else to web, and exposes its own Prometheus metrics.

apiVersion: v1
kind: ConfigMap
metadata:
  name: caddy-config
  namespace: gipc
data:
  Caddyfile: |
    {
      auto_https off
      admin off
      servers {
        trusted_proxies static private_ranges   # trust cloudflared → preserve X-Forwarded-Proto/For
        metrics                                  # enable per-server Prometheus metrics
      }
    }
    :80 {
      handle /caddy-health {
        respond 200
      }
      handle /api/telemetry {
        reverse_proxy web:80
      }
      handle /api/* {
        reverse_proxy core:8080 {
          flush_interval -1
        }
      }
      handle {
        reverse_proxy web:80
      }
      header -Server
    }
    :2019 {
      metrics /metrics                           # internal only — NOT in the Service/tunnel
    }
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: caddy
  namespace: gipc
  labels: { app: caddy }
spec:
  replicas: 1
  selector:
    matchLabels: { app: caddy }
  template:
    metadata:
      labels: { app: caddy }
      annotations:
        prometheus.io/scrape: "true"
        prometheus.io/port: "2019"
        prometheus.io/path: "/metrics"
    spec:
      securityContext:
        seccompProfile: { type: RuntimeDefault }
      containers:
        - name: caddy
          image: caddy:2.8-alpine   # pinned INLINE (not in kustomization images:) so CI's sed can't touch it
          ports:
            - { containerPort: 80, name: http }
            - { containerPort: 2019, name: metrics }
          readinessProbe:
            httpGet: { path: /caddy-health, port: 80 }
            initialDelaySeconds: 3
            periodSeconds: 10
          livenessProbe:
            httpGet: { path: /caddy-health, port: 80 }
            initialDelaySeconds: 10
            periodSeconds: 20
          resources:
            requests: { cpu: 25m, memory: 32Mi }
            limits: { cpu: 150m, memory: 128Mi }
          securityContext:
            allowPrivilegeEscalation: false   # runs root for :80 bind + /data,/config writes — lighter than core
          volumeMounts:
            - { name: config, mountPath: /etc/caddy }
      volumes:
        - name: config
          configMap: { name: caddy-config }
---
apiVersion: v1
kind: Service
metadata:
  name: caddy
  namespace: gipc
  labels: { app: caddy }
spec:
  type: NodePort
  selector: { app: caddy }
  ports:
    - { port: 80, targetPort: 80, nodePort: 30082, name: http }

Cloudflare Tunnel — the front door

infra/cloudflared/config.yml

cloudflared dials out to Cloudflare's edge with zero inbound ports open on the box; gipc.dev ingress lands on the in-cluster Caddy NodePort. The tunnel id is redacted here.

tunnel: ‹tunnel-id›
credentials-file: /etc/cloudflared/gipc.json
originRequest:
  connectTimeout: 30s
ingress:
  - hostname: gipc.dev
    service: http://localhost:30082
  - hostname: www.gipc.dev
    service: http://localhost:30082
  - service: http_status:404