Under the hood
One commit, two images, no hand-copied secrets—eventually—and hopefully no 2am surprises.
Follow a deployment ↓Markdown becomes a site. A push starts the build.
Astro validates the content collections, renders static HTML, and Pagefind indexes the finished output. A push to main — or a manual dispatch — starts the repository's Gitea Actions workflow.
Why this choice · Failure mode · Evidence
- Why this choice
- Most changes are content in Git. Static generation keeps the public runtime small, while malformed frontmatter fails the build instead of quietly breaking a production page. Git is also the content history, review trail, deployment trigger, and source for the site's generated changelog.
- Failure mode
- A schema, Astro, or Pagefind failure stops the image build — the currently deployed site is untouched. No successful workflow means no new artifacts; existing containers keep serving the previous version.
npm run build
# astro build && pagefind --site dist on:
push:
branches: [main]
workflow_dispatch: One commit becomes two images.
A self-hosted Gitea runner checks out the full history, derives image names and SHA tags, authenticates to the private registry, and starts Buildx. The workflow builds a static nginx web image and a separate Node API image, tagged as both latest and the short commit SHA. Deploy targets pull these artifacts instead of rebuilding source.
Astro's generated HTML, CSS, JavaScript, project images, and the Pagefind search index, served by nginx.
node:24-alpine → nginx:alpine A deliberately separate Express process for Spotify, Last.fm, Immich, Gitea, and the small pieces that cannot be static.
node:24-alpine → node api/index.mjs Why this choice · Failure mode · Evidence
- Why this choice
- The complete build loop stays on infrastructure I control — full history is intentional, since the changelog is generated from git log during the build. The web and API have different dependency and runtime profiles; separating them keeps the API image scoped to what it actually runs. Artifact distribution stays separate from both source checkout and runtime configuration: CI can publish while deploy hosts only need to pull.
- Failure mode
- A failed runner or registry login fails the workflow without affecting the live containers. Either image failing to build fails the whole workflow — the SHA tags preserve a concrete artifact identity for diagnosis and rollback. A registry outage blocks new pulls, but already-running containers don't need the registry to keep serving traffic.
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: docker/setup-buildx-action@v3 goonk-cv:latest goonk-cv:a1b2c3d
goonk-cv-api:latest goonk-cv-api:a1b2c3d tags:
- <registry>/<project>:latest
- <registry>/<project>:<short-sha> Artifacts travel. Secrets don't.
I SSH to the target host and pull the current images; Docker Compose reconciles the two services. Runtime configuration arrives separately — keep pulls an encrypted environment bundle and decrypts it locally with a read-only deploy identity.
goonk-api:abc123
Why this choice · Failure mode · Evidence
- Why this choice
- At this scale, an explicit deploy is understandable and auditable — a registry watcher or orchestration control plane would automate a step without removing the need to understand it. A secret is pushed once, granted explicitly, rotated centrally, and audited when fetched, without the keep server ever receiving plaintext or recipient private keys.
- Failure mode
- A build can succeed without changing production. A failed pull leaves the currently running image available; replacement and health behavior remain visible, manual operations. An existing container keeps its current environment if keep is unavailable — a new deployment should stop rather than invent missing values.
docker compose pull
keep pull <project>/production > .env
docker compose up -d Compose runs the boring bits.
nginx serves the generated site while Express handles same-origin API routes and private integrations.
Two containers, one persistent data volume, and one deploy host do not need a control plane. Docker Compose describes the system that actually exists, keeps recovery understandable, and leaves fewer things to break at 2am.
Why this choice · Failure mode · Evidence
- Boundary
- The browser sees one public origin. Spotify, Immich, Last.fm, and Gitea credentials and private upstream locations stay in the API process.
- Failure mode
- Static pages remain useful when an integration fails. Individual widgets are designed to degrade without taking the whole site with them.
web → nginx → static Astro + Pagefind
api → Express → Spotify / Immich / Last.fm / Gitea What is actually running?
node-exporter, Prometheus, Grafana, and the public status page handle operational visibility — this page only explains the system. Build identity is embedded in the artifact; deployment identity comes separately from Trace, after the deploy host has run its health checks. If either source is unavailable, this panel says so instead of guessing.
- Commit
- loading…
- Built
- loading…
- Deployed
- loading…
- Health
- unknown
Why this choice · Failure mode · Evidence
- Why this choice
- A portfolio page should not become a second monitoring console. Detailed metrics stay in tools designed to store, query, and alert on them.
- Failure mode
- Monitoring being unreachable means status is unknown — not proof that the application itself is unhealthy.
application state ≠ monitoring state
unknown ≠ unhealthy What keep can — and cannot — see
The server distributes encrypted material and records access. Encryption and decryption happen at the edges, so the server never needs the plaintext, or the private key that would reveal it.
wrapped keys →
wrapped recipient keys
pull →
- Vault identifiers & encrypted payloads
- Recipients, grants, and access timestamps
- Which wrapped key belongs to which recipient
- Plaintext secret values
- Recipient private keys
- The unwrapped symmetric payload key
- A compromised developer or deploy machine
- Secrets exposed by the application itself
- Metadata visibility — the service still has an audit trail
- Build once.
- Deploy artifacts, not source.
- Keep secrets out of artifacts.
- Give credentials only the authority they need.
- Let integrations fail independently.
- Prefer understandable recovery over clever automation.
- Don't run Kubernetes because the internet told you to.
Where does this actually run? Explore the homelab →