Background
local-stack has standardized on Docker Swarm stacks and stackctl.sh as the canonical deployment interface. The repository should now use Doco-CD as the deployment controller, but Doco-CD must not bypass the existing repository-specific deployment logic.
The critical constraint is that stacks/*.yml are generated artifacts and contain unresolved ${VAR} placeholders. They are not safe to deploy directly. stackctl.sh renders them into .rendered/*.rendered.yml and then runs docker stack deploy.
The hosted machine currently deploys from dev, uses stackctl, and is a single-node Swarm. This issue adds a Doco-CD-compatible deployment runner that preserves that flow.
Goals
- Add a Doco-CD deployment path for
local-stack.
- Deploy from the
dev branch.
- Support both Doco-CD polling and GitHub webhook triggers.
- Preserve the current
stackctl deployment model.
- Support the current single-node Swarm host.
- Keep the design reusable for other projects later.
- Allow patch/minor dependency updates to deploy immediately after merge.
- Keep major updates/manual promotions outside the automatic path.
Non-goals
- Do not replace
stackctl.sh in this issue.
- Do not make Doco-CD deploy unresolved
stacks/*.yml directly.
- Do not introduce Watchtower as the primary deployment system.
- Do not require GitHub Actions to access the production host.
- Do not store host credentials in GitHub Actions.
Required architecture
Use Doco-CD as the deployment controller, but deploy a purpose-built runner service that executes the existing canonical flow.
Required flow:
GitHub push to dev
-> Doco-CD polling or webhook detects the change
-> Doco-CD deploys/recreates local-stack-deployer
-> local-stack-deployer fetches AniTrend/local-stack@dev
-> local-stack-deployer installs stack render dependencies
-> local-stack-deployer runs stackctl checks
-> local-stack-deployer runs stackctl deployment
-> stackctl renders .rendered/*.rendered.yml
-> stackctl runs docker stack deploy
-> stackctl reports service status
The runner exists because Doco-CD deliberately should not be asked to run arbitrary shell against the repository, while this repository needs stackctl.sh for safe render/deploy behavior.
Required Doco-CD config
Add root .doco-cd.yml:
name: local-stack-deployer
reference: dev
working_dir: deploy/doco/local-stack-deployer
compose_files:
- docker-compose.yml
webhook_filter: "^refs/heads/dev$"
remove_orphans: true
prune_images: true
force_recreate: true
force_image_pull: true
timeout: 900
git_depth: 10
reconciliation:
enabled: true
Implementation notes:
reference must be dev.
webhook_filter must only accept refs/heads/dev.
- Polling must also be supported by Doco-CD configuration or documented host-side configuration.
force_recreate is intentional so the one-shot runner executes after a deployment-triggering change.
timeout should be high enough for image pulls and stack redeploys on a low-resource host.
Required deploy runner files
Add:
deploy/doco/local-stack-deployer/
docker-compose.yml
Dockerfile
entrypoint.sh
README.md
Required runner behavior
The runner must:
- Clone or update
https://github.com/AniTrend/local-stack.git.
- Use branch/ref
dev by default.
- Use
git fetch --prune origin dev and git reset --hard origin/dev.
- Install Python render dependencies using
tools/requirements.txt.
- Use
flock or equivalent locking so concurrent deploys do not race.
- Run
./stackctl.sh doctor --fix-network.
- Run
./stackctl.sh sync before deployment.
- Deploy using
./stackctl.sh secrets deploy when SOPS mode is enabled.
- Support fallback/plain env mode via explicit configuration only.
- Run
./stackctl.sh status after deployment.
- Exit non-zero on deployment failure.
- Emit logs to stdout/stderr for Doco-CD to capture.
- Accept no arbitrary shell command input from webhook payloads.
Required runner environment variables
The runner must support at least:
LOCAL_STACK_REPOSITORY=https://github.com/AniTrend/local-stack.git
LOCAL_STACK_REF=dev
LOCAL_STACK_WORKDIR=/workspace/local-stack
LOCAL_STACK_DEPLOY_MODE=secrets | plain-env | dry-run
LOCAL_STACK_TARGET_STACKS=infrastructure,observability,platform
Default mode should be secrets if the host is using SOPS + age.
Required runner compose baseline
services:
local-stack-deployer:
build:
context: .
dockerfile: Dockerfile
image: local/local-stack-deployer:latest
environment:
LOCAL_STACK_REPOSITORY: "https://github.com/AniTrend/local-stack.git"
LOCAL_STACK_REF: "dev"
LOCAL_STACK_WORKDIR: "/workspace/local-stack"
LOCAL_STACK_DEPLOY_MODE: "secrets"
LOCAL_STACK_TARGET_STACKS: "infrastructure,observability,platform"
volumes:
- local-stack-workspace:/workspace
- /var/run/docker.sock:/var/run/docker.sock
- /opt/local-stack/sops/age:/root/.config/sops/age:ro
restart: "no"
volumes:
local-stack-workspace:
The Docker socket mount is accepted for this project, but it must be documented as privileged and intentionally scoped to this runner/Doco-CD deployment model.
Required runner entrypoint baseline
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'
LOCK_FILE="/tmp/local-stack-deploy.lock"
REPO="${LOCAL_STACK_REPOSITORY:?LOCAL_STACK_REPOSITORY is required}"
REF="${LOCAL_STACK_REF:-dev}"
WORKDIR="${LOCAL_STACK_WORKDIR:-/workspace/local-stack}"
MODE="${LOCAL_STACK_DEPLOY_MODE:-secrets}"
STACKS="${LOCAL_STACK_TARGET_STACKS:-infrastructure,observability,platform}"
exec 9>"$LOCK_FILE"
if ! flock -n 9; then
echo "Another local-stack deployment is already running; exiting."
exit 75
fi
if [[ ! -d "$WORKDIR/.git" ]]; then
git clone --branch "$REF" --depth 10 "$REPO" "$WORKDIR"
fi
cd "$WORKDIR"
git fetch --prune origin "$REF"
git reset --hard "origin/$REF"
python3 -m venv tools/.venv
tools/.venv/bin/python -m pip install --upgrade pip
tools/.venv/bin/python -m pip install -r tools/requirements.txt
./stackctl.sh doctor --fix-network
./stackctl.sh sync
case "$MODE" in
secrets)
./stackctl.sh secrets deploy
;;
plain-env)
./stackctl.sh up --no-logs -s "$STACKS"
;;
dry-run)
./stackctl.sh up --dry-run --no-logs -s "$STACKS"
;;
*)
echo "Unknown LOCAL_STACK_DEPLOY_MODE: $MODE" >&2
exit 2
;;
esac
./stackctl.sh status
Important implementation detail:
Do not use git clean -fd unless all plaintext .env files are produced from encrypted .env.enc during the deployment and safely removed afterwards. If a host uses local untracked plaintext .env files, git clean -fd could delete operational configuration. For this project, prefer SOPS deploy mode and document it as the recommended production path.
Doco-CD bootstrap requirement
Add:
doco-cd/
docker-compose.yml
.env.example
README.md
This is a host bootstrap stack for running Doco-CD itself. It should be deployed manually first and not self-managed until the system is proven stable.
Baseline bootstrap compose:
services:
doco-cd:
image: ghcr.io/kimdre/doco-cd:<pinned-version>
container_name: doco-cd
restart: unless-stopped
ports:
- "127.0.0.1:8088:80"
environment:
TZ: Africa/Johannesburg
WEBHOOK_SECRET_FILE: /run/secrets/doco_webhook_secret
API_SECRET_FILE: /run/secrets/doco_api_secret
volumes:
- doco-cd-data:/data
- ./config:/config:ro
- /var/run/docker.sock:/var/run/docker.sock
secrets:
- doco_webhook_secret
- doco_api_secret
networks:
- doco-private
secrets:
doco_webhook_secret:
file: ./secrets/webhook_secret
doco_api_secret:
file: ./secrets/api_secret
volumes:
doco-cd-data:
networks:
doco-private:
The implementation must pin a real Doco-CD version. Do not use latest.
Polling and webhook support
Both must be supported.
Polling:
- Must be documented as the safer initial mode because it avoids exposing an inbound webhook endpoint.
- Recommended interval: 5-15 minutes.
- Must track
dev only.
Webhook:
- Must use Doco-CD webhook secret.
- Must restrict to
refs/heads/dev using webhook_filter.
- Must be exposed only through a controlled route, tunnel, or private network.
- Must not accept arbitrary commands or branch names from the payload.
Security requirements
- Docker socket access must be documented as privileged.
- Doco-CD and the runner must not be exposed publicly except for a minimal webhook path if webhook mode is enabled.
- The SOPS age private key must be mounted read-only.
- The runner must not print secrets.
- The runner must not leave plaintext
.env files behind in secrets mode.
- The runner must fail closed on missing SOPS key, missing render dependencies, stack drift, render failure, or failed Swarm deploy.
- The runner must use a lock to prevent concurrent deployments.
Acceptance criteria
Test plan
- Bootstrap Doco-CD manually on a test host.
- Configure polling against
AniTrend/local-stack@dev.
- Set deploy runner mode to
dry-run.
- Confirm Doco-CD runs the runner and logs successful render/dry-run output.
- Switch deploy runner to
secrets mode.
- Confirm SOPS age key is available read-only.
- Run deployment against a disposable single-node Swarm.
- Confirm
docker stack services infrastructure, observability, and platform return expected services.
- Confirm plaintext
.env files are removed after deploy.
- Configure webhook mode.
- Push a harmless commit to
dev and confirm webhook-triggered deployment.
- Confirm duplicate/rapid triggers do not race due to locking.
Links to related work
- Depends on the Dependabot/OpenCode gate issue.
- Deployment hardening and rollback are covered by a separate runbook issue.
- Branch protection and auto-merge policy are covered by a separate repository policy issue.
Background
local-stackhas standardized on Docker Swarm stacks andstackctl.shas the canonical deployment interface. The repository should now use Doco-CD as the deployment controller, but Doco-CD must not bypass the existing repository-specific deployment logic.The critical constraint is that
stacks/*.ymlare generated artifacts and contain unresolved${VAR}placeholders. They are not safe to deploy directly.stackctl.shrenders them into.rendered/*.rendered.ymland then runsdocker stack deploy.The hosted machine currently deploys from
dev, usesstackctl, and is a single-node Swarm. This issue adds a Doco-CD-compatible deployment runner that preserves that flow.Goals
local-stack.devbranch.stackctldeployment model.Non-goals
stackctl.shin this issue.stacks/*.ymldirectly.Required architecture
Use Doco-CD as the deployment controller, but deploy a purpose-built runner service that executes the existing canonical flow.
Required flow:
The runner exists because Doco-CD deliberately should not be asked to run arbitrary shell against the repository, while this repository needs
stackctl.shfor safe render/deploy behavior.Required Doco-CD config
Add root
.doco-cd.yml:Implementation notes:
referencemust bedev.webhook_filtermust only acceptrefs/heads/dev.force_recreateis intentional so the one-shot runner executes after a deployment-triggering change.timeoutshould be high enough for image pulls and stack redeploys on a low-resource host.Required deploy runner files
Add:
Required runner behavior
The runner must:
https://github.com/AniTrend/local-stack.git.devby default.git fetch --prune origin devandgit reset --hard origin/dev.tools/requirements.txt.flockor equivalent locking so concurrent deploys do not race../stackctl.sh doctor --fix-network../stackctl.sh syncbefore deployment../stackctl.sh secrets deploywhen SOPS mode is enabled../stackctl.sh statusafter deployment.Required runner environment variables
The runner must support at least:
Default mode should be
secretsif the host is using SOPS + age.Required runner compose baseline
The Docker socket mount is accepted for this project, but it must be documented as privileged and intentionally scoped to this runner/Doco-CD deployment model.
Required runner entrypoint baseline
Important implementation detail:
Do not use
git clean -fdunless all plaintext.envfiles are produced from encrypted.env.encduring the deployment and safely removed afterwards. If a host uses local untracked plaintext.envfiles,git clean -fdcould delete operational configuration. For this project, prefer SOPS deploy mode and document it as the recommended production path.Doco-CD bootstrap requirement
Add:
This is a host bootstrap stack for running Doco-CD itself. It should be deployed manually first and not self-managed until the system is proven stable.
Baseline bootstrap compose:
The implementation must pin a real Doco-CD version. Do not use
latest.Polling and webhook support
Both must be supported.
Polling:
devonly.Webhook:
refs/heads/devusingwebhook_filter.Security requirements
.envfiles behind insecretsmode.Acceptance criteria
.doco-cd.ymlexists at the repository root and targetsdev..doco-cd.ymldeploys onlydeploy/doco/local-stack-deployer/docker-compose.yml.dev.refs/heads/dev.doco-cd/README.mdwith pinned image usage.deploy/doco/local-stack-deployer/.stackctl.sh, not directdocker stack deployagainst unresolved files.secrets,plain-env, anddry-runmodes.dev../stackctl.sh statusis executed..envfiles are not left behind in SOPS mode.Test plan
AniTrend/local-stack@dev.dry-run.secretsmode.docker stack services infrastructure,observability, andplatformreturn expected services..envfiles are removed after deploy.devand confirm webhook-triggered deployment.Links to related work