deploy-app-from-local
Skill576 starsChanged 17 days agoApache-2.0View on GitHub
Deploy a local checkout to a production Hetzner App using the manual image-import runbook. Use when the user asks to ship local working-tree code to a live bex App rather than deploy from remote Git. Arguments are the local repo path and app name.
- Reads credentials
In this repository32 other files it gives its agents.
- AGENTS.mdAGENTS.md
- dashboard/AGENTS.mdAGENTS.md
- docs/AGENTS.mdAGENTS.md
- lego/AGENTS.mdAGENTS.md
- mobile/AGENTS.mdAGENTS.md
- .pm/AGENTS.mdAGENTS.md
- deploy-appSkill
- loopxSkill
- mermaidSkill
- mock-clusterSkill
- pm-brainstormSkill
- pmSkill
- qa-find-bugs-cliSkill
- qa-find-bugsSkill
- releaseSkill
- rt-fix-logic-bugsSkill
- rt-fix-prod-crashesSkill
- rt-flatten-abstractionsSkill
- rt-normalize-abstractionsSkill
- rt-prune-useless-testsSkill
- rt-remove-dead-codeSkill
- rt-simplify-logicSkill
- rt-unify-duplicatesSkill
- security-auditSkill
- shipSkill
- simplifySkill
- stripe-best-practicesSkill
- stripe-directorySkill
- stripe-docsSkill
- stripe-projectsSkill
- unblockSkill
- upgrade-stripeSkill
The file
We split this skill into the rules it gives an agent
2 rules. About 0 are common, 0 are distinctive, and 2 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈0
Distinctive · estimate
This author's own choices. Worth a look.
2
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 33
Never print $HCLOUD_TOKEN or kubeconfig contents (per repo rules) — only read specific jsonpath fields.
- Very specific: line 59
Use a fresh tag (git sha, never :latest) — the operator sets no imagePullPolicy, so k8s defaults to IfNotPresent.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Task: deploy a local checkout to prod Hetzner (manual image-import runbook)
Use this runbook only when the code to deploy exists on local disk and not in Git (uncommitted or unpushed work). Committed code goes through the operator's in-cluster build (BuildKit/kpack Jobs, lego/operator/internal/build/), which this skill bypasses by running docs/ADR004-app-deployment.md's manual image-import runbook end-to-end. Do not use the restart verb as a substitute for a deploy — it only rolls pods on the App's existing spec.image; it never rebuilds anything.
The image is built from whatever is on disk in $ARGUMENTS's repo path right now, not from origin/main. If you want "latest remote main" instead, git pull --ff-only first.
Step 0 — Confirm scope with the user before touching prod
This SSHes into a live Hetzner node and patches a running App CR — state a one-line plan (repo, app name, target image tag) and get a go-ahead before Step 4 (the actual ctr import + kubectl patch). Steps 1–3 (discovery, build, smoke test) are safe/local and don't need to wait for that confirmation.
Step 1 — Fetch the app-cluster kubeconfig (self-managed, no infra node)
There is no management cluster — scripts/fetch-app-kubeconfig.sh discovers a control-plane node via the hcloud API (label caph-cluster-bex) and SSH-fetches /etc/kubernetes/admin.conf:
# from the bex repo root (where this command runs)
set -a; source ./.env; set +a
# default key is ~/.ssh/id_bex (see scripts/fetch-app-kubeconfig.sh); override only if your key is elsewhere
BEX_SSH_KEY_PATH=~/.ssh/id_bex bash scripts/fetch-app-kubeconfig.sh /tmp/bex-app.kubeconfig
export KUBECONFIG=/tmp/bex-app.kubeconfig
kubectl get app <app-name> -o yaml # baseline: current image, port, generation, revisionNever print $HCLOUD_TOKEN or kubeconfig contents (per repo rules) — only read specific jsonpath fields.
Step 2 — Discover the node the App runs on (import target)
Apps run on the tenant pool (bex-worker-*), not the control plane — the image must be imported into the containerd of the node hosting the App's pod:
NODE=$(kubectl get pods -n default -l app.bex.co/app=<app-name> -o jsonpath='{.items[0].spec.nodeName}')
APP_NODE_IP=$(curl -sf -H "Authorization: Bearer $HCLOUD_TOKEN" \
"https://api.hetzner.cloud/v1/servers?per_page=50" \
| python3 -c "import json,sys; print([s['public_net']['ipv4']['ip'] for s in json.load(sys.stdin)['servers'] if s['name']=='$NODE'][0])")(If the pod is Pending with no node yet — e.g. first deploy — import into every bex-worker-* node instead. ⚠ Known fragility: ctr-imported images live only in that node's containerd; an autoscaler node replacement loses them and the App falls to ImagePullBackOff until re-imported — re-run this runbook then.)
Step 3 — Build + smoke-test — laptop, from the local repo path in $ARGUMENTS
cd <repo-path>
SHA=$(git rev-parse --short HEAD)
docker build --platform linux/amd64 -t <app-name>:$SHA . # amd64 required — Hetzner node is x86_64
docker run -d --rm --name smoke -p 127.0.0.1:3999:<container-port> <app-name>:$SHA
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:3999/ # expect 200
docker stop smokeUse a fresh tag (git sha, never :latest) — the operator sets no imagePullPolicy, so k8s defaults to IfNotPresent; reusing a tag would silently serve the node's cached image.
Step 4 — Import into the app node's containerd — laptop → app node (confirm with user first, see Step 0)
docker save <app-name>:$SHA | ssh -i ~/.ssh/id_bex "root@${APP_NODE_IP}" \
'ctr -n k8s.io images import -'Step 5 — Patch the App CR — laptop → apiserver → operator
KUBECONFIG=/tmp/bex-app.kubeconfig kubectl patch app <app-name> --type merge \
-p "{\"spec\":{\"image\":\"<app-name>:$SHA\"}}"This bumps metadata.generation, which is what makes the operator reconcile (it early-returns when already Running at the current generation).
Step 6 — Watch the rollout and verify the edge serves the new code
KUBECONFIG=/tmp/bex-app.kubeconfig kubectl rollout status deployment/<app-name>
KUBECONFIG=/tmp/bex-app.kubeconfig kubectl get app <app-name> # PHASE Running, REVISION bumped
curl -s https://<app-name>.onbex.co/ | grep -i '<something only in the new build>'A 200 alone doesn't prove anything — grep for content that only exists in the new commit (e.g. the new blog post title, a changed string). This is the step that actually confirms a deploy happened.
Step 7 — Report
App name, old image tag → new image tag (git sha), old revision → new revision, and the content check that proved the new code is live. Clean up /tmp/bex-app.kubeconfig when done.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

