Sync the self-hosting stack

This commit is contained in:
2026-08-30 09:58:08 +02:00
parent cd7d807a4f
commit 9b5020dea0
12 changed files with 392 additions and 2 deletions
+84
View File
@@ -97,6 +97,90 @@ about the install screen applies unchanged — including that **whoever reaches
instance first owns it**, with no deadline. Point a public hostname at this after you have
installed, not before.
## GitOps: Argo CD, Flux, or anything that reconciles
`kustomization.yaml` is the entry point. Point a controller at this directory and it renders every
manifest here except one — and `argocd-application.example.yaml` is a worked Application to copy.
```sh
kubectl apply -k . # the same thing, by hand
```
**The exception is `01-secret.example.yaml`, and it is left out on purpose.** It is a template full
of placeholders. A controller applies what it is pointed at, continuously, without asking — so a
Secret full of `REPLACE_ME` inside the source of truth is a controller that overwrites your real
credentials with placeholders on its next reconcile and puts them back every time you fix them. The
api cannot reach its database, the vault key is wrong, and nothing about the sync looks unhealthy
while it happens. Write `jarvis-secrets` by hand, or through SealedSecrets, ExternalSecrets or
Vault, whose ciphertext is safe to commit. `jarvis-generated` is minted in-cluster and never exists
in git at all.
### The order is in the manifests, not in the filenames
A controller reads this directory as a set; `00-`, `01-` and `02-` mean nothing to it. The order
that the `kubectl apply` sequence above keeps by hand is carried by
`argocd.argoproj.io/sync-wave` annotations on the resources themselves, so that it lives beside the
thing being ordered:
| wave | what | why it cannot be later |
| --- | --- | --- |
| `-3` | Namespace | it carries the Pod Security labels that decide whether any of these pods are admitted |
| `-2` | ConfigMaps, the minter's RBAC, Redis | everything below reads the config; the api's readiness probe reaches Redis |
| `-1` | `jarvis-mint-secrets` **(hook)** | the migration reads `DATABASE_URL` out of what it writes |
| `0` | `jarvis-migrate` **(hook)** | an api pod against an unmigrated schema is the failure all of this prevents |
| `1` | api and web Deployments, Services | the web pod's `/readyz` proxies the api's health endpoint, so it cannot be ready before the api is |
| `2` | Ingress, PodDisruptionBudgets | an Ingress applied early is a route to a 503 |
Argo does not begin a wave until the previous one reports healthy, which is what the `kubectl wait`
lines are doing by hand.
### Why the two Jobs are hooks
Without this, the directory works exactly once. **A Job's `spec.template` is immutable**, so the
second release — the first one with a different image digest — is a patch the API server refuses
with `field is immutable`, and the sync fails there and stays failed. `hook: Sync` with
`hook-delete-policy: BeforeHookCreation` deletes the previous Job before creating the new one, which
is the supported way to say "run it again, with new contents".
The Sync phase rather than PreSync, deliberately: a PreSync hook runs before any ordinary resource
is applied, which on a first sync is before the namespace, the ConfigMap and the Secret these Jobs
read exist. In the Sync phase they are ordered by the same waves as everything else.
Two consequences worth knowing:
- **The previous run's Job object, and its logs, are gone** by the time the next one starts. Ship
the logs if you need them after the fact.
- **Every sync runs the migration.** That is safe — the phase is idempotent and holds a Postgres
advisory lock — and it is why the example Application leaves `automated` sync OFF. A commit that
applies a schema change with nobody watching turns the one thing this directory works hardest to
make visible, a migration that REFUSES because it would drop a column, into a notification.
### Pin the images in one place
`kustomization.yaml` carries an `images:` block, so the three digests are set once rather than in
three files. Three, not two: `jarvis-agent-dist` is an initContainer in `04-api.yaml` and is easy to
miss, and missing it renders cleanly while the api pod never starts.
A version bump is then those lines and nothing else.
### What most estates should actually do
Point the Application at **your** repository, not at this one, with this directory as a remote base:
```yaml
# your-repo/jarvis/kustomization.yaml
resources:
- https://git.luxit.be/Luxit/jarvis-selfhost//kubernetes?ref=v1.2.3
images:
- name: git.luxit.be/luxit/jarvis-api
digest: sha256:...
patches:
- path: replicas.yaml # your node count, your ingress host, your storage class
```
Then the digests you run are in a commit you control, upgrading is a diff you can review and revert,
and `?ref=` pins the base to a tag rather than a branch that moves under you. Pin it.
## The three things most likely to bite
**Never mint secrets per pod.** On Compose a one-shot generates the vault key and the JWT secrets