git-hook-tamper.yaml's condition used proc.exepath, which resolves to the script interpreter's path (e.g. /bin/busybox) for shebang scripts, not the script's own path - switched to proc.cmdline, which retains the originally invoked path. Confirmed via live-testing both ways. ssh-persistence/cloud-metadata-probe/db-spawned-process round out the post-incident hardening pass with a few more incubating-ruleset adaptations.
4.0 KiB
name, description
| name | description |
|---|---|
| deploy-stack | Deploy a docker-infrastructure change - push to Gitea, then redeploy the stack via Portainer. Use whenever a service's docker-compose.yaml (or other tracked file) changed and needs to go live. |
Deploy a stack (Gitea → Portainer)
This repo's services go live via GitOps: commit → push to Gitea (self-hosted, SSH
remote) → tell Portainer to pull + redeploy. Always use ./portainer.sh for the
Portainer side; never raw curl unless portainer.sh can't do it.
Steps
-
Stage and commit only the files that changed for this task (never
git add -A). Imperative mood, first line < 72 chars, explain why not what. -
Sync with remote before pushing.
git push origin mainfails non-fast-forward if anyone (or the Gitea web UI) committed since your last pull. Prefer:git pull --no-edit origin main # fetch + merge in one step git push origin mainThe remote is SSH (
git@gitea.kolpacksoftware.com:...).GITEA_TOKENin.credentialsis NOT valid for HTTPS push (403) - don't try HTTPS for push. -
If push/fetch fails with a low-level git protocol error (
bad pack header,unable to fork git-pack-objects,cannot exec '.../hooks/...') - this is NOT a normal merge conflict, it means Gitea's git service itself is broken (corrupted repo, poisoned global gitconfig, etc). Do not just retry blindly. Stop and investigate server-side (docker logs gitea, check/data/gitea/home/.gitconfiginside the container) before continuing - this exact failure mode was an active RCE backdoor once (seegitea-log-poisoning-attack-2026-08.mdmemory). HTTPS fetch withGITEA_TOKENcan help bisect whether it's SSH-specific or instance-wide:source .credentials git fetch "https://${GITEA_USER}:${GITEA_TOKEN}@gitea.kolpacksoftware.com/homelab/docker-infrastructure.git" main -
Find the stack name/ID if you don't already know it:
source .credentials && ./portainer.sh listStack names in Portainer usually match the service directory name, but not always (verify with
list, don't assume). -
Redeploy (pulls latest git commit + recreates containers):
source .credentials && ./portainer.sh redeploy <stack-name>This only works for stacks that are git-linked in Portainer. If it's not git-linked, changes to
.envvalues must go through./portainer.sh set-env <stack-name> KEY=VALUEinstead - a.envfile at the repo path is gitignored and NOT read by git-linked Portainer deploys. -
Verify:
docker ps --filter name=<container>for status,docker logs <container> --tail 50to confirm it actually came up clean, not just "Up".
Gotchas
.envfiles are gitignored and invisible to git-linked stacks. Portainer reads env vars it has stored for the stack (set via UI orportainer.sh set-env), not the local.envfile. Check current values first with./portainer.sh get-env <stack-name>before adding new ones, and set any new var explicitly - editing the local.envalone does nothing for a deployed git-linked stack.- Not every service is a Portainer stack. Some (e.g.
falco) are run via plaindocker run/manualdocker compose, bind-mounting config directly from this repo checkout. For those,portainer.sh redeploywill fail with "stack not found" - checkdocker inspect <container> --format '{{json .Mounts}}'to see if it's bind-mounted from this repo (if so, agit pushalone is enough to update the source files; the container itself needs a manualdocker restartor recreate to pick up new volume mounts). - No
docker composeCLI on this host.docker compose up -dfails outright - always go through Portainer, ordocker run/docker restartdirectly for manually-managed services. - Portainer's
GET /api/stacks/<id>can return Unauthorized for some stacks -portainer.shalready works around this by listing + filtering; don't call the raw API directly.