SKILL.md
stacks-deploy
Generates minimal deployment stack entries from containerized git repos. Separates build concerns (git repo) from deployment topology (stacks monorepo) behind the delo.sh Traefik proxy.
Invariants
- No source code in stacks. Config files, scripts, and .env only.
- No submodules. Links between repos are image references + mise tasks.
- No
build:in stacks compose files. Everything usesimage:. - Runtime data lives at
/home/delorenj/data/<service>/, not in stacks or git repos. network_mode: hostservices cannot join the proxy network. Use a companion service for Traefik routing.- Secrets resolve from 1Password at deploy time, never live in the repo. Commit
.env.templatewithop://...references; resolve via~/docker/stacks/_lib/op-inject.sh(seereferences/secret-injection.md)..envis always gitignored.
Workflow
Step 1: Analyze Source Repo
Read the source repo's compose file(s), Dockerfile(s), and any deployment docs.
Identify for each service:
- Does it use
build:orimage:? - What ports does it expose?
- What volumes does it need? (config files vs runtime data vs source mounts)
- Does it need special networking? (
network_mode: host,privileged, GPU passthrough) - Which service(s) are web-facing (need Traefik labels)?
Step 2: Classify Deployment Mode
| Mode | When | Example |
|---|---|---|
| Image reference | Custom code, needs build: in source repo |
ssbnk-watcher |
| Config-only | Public base image + mounted configs | ssbnk-web (nginx:alpine) |
| Hybrid | Mix of custom and public images | ssbnk (watcher + nginx + alpine) |
For services with custom images, the source repo must publish the image (Docker Hub, GHCR, or local tag).
Step 3: Extract Deployment Artifacts
From the source repo, identify files needed at deploy time:
Copy to stacks entry:
- Config files mounted into containers (nginx confs, prometheus configs, etc.)
- Scripts mounted as volumes (cron scripts, entrypoints)
.env.exampleas template for.env
Do NOT copy:
- Source code, Dockerfiles, test files, docs, CI configs
- Compiled binaries, node_modules, build artifacts
Create at /home/delorenj/data/<service>/:
- Runtime data directories (databases, uploads, caches, logs)
- Any directory that containers write to at runtime
Step 4: Determine Stacks Category
| Category | Purpose | Path |
|---|---|---|
utils |
Developer tools, internal services | ~/docker/stacks/utils/ |
websites |
Public-facing web applications | ~/docker/stacks/websites/ |
ai |
AI/ML services, agents | ~/docker/stacks/ai/ |
monitoring |
Observability, metrics, alerts | ~/docker/stacks/monitoring/ |
persistence |
Databases, search engines, caches | ~/docker/stacks/persistence/ |
media |
Media servers, streaming | ~/docker/stacks/media/ |
Step 5: Generate Stack Entry
Create the directory structure:
~/docker/stacks/<category>/<service>/
compose.yml # image: references only
.env # runtime configuration
[configs/] # mounted config files
[scripts/] # mounted scripts
compose.yml Template
services:
<service-name>:
image: <registry>/<image>:<tag>
container_name: <service-name>
restart: unless-stopped
volumes:
- /home/delorenj/data/<service>/<subdir>:/container/path
- ./configs/some.conf:/etc/some.conf:ro
environment:
- KEY=${ENV_VAR}
networks:
- proxy
labels:
- "traefik.enable=true"
- "traefik.http.routers.<service>.entrypoints=websecure"
- "traefik.http.routers.<service>.rule=Host(`<service>.delo.sh`)"
- "traefik.http.routers.<service>.tls=true"
- "traefik.http.routers.<service>.tls.certresolver=letsencrypt"
- "traefik.http.services.<service>.loadbalancer.server.port=<port>"
- "traefik.docker.network=proxy"
networks:
proxy:
external: true
Host Networking Pattern
When a service needs network_mode: host (clipboard, raw sockets, etc.), it CANNOT have Traefik labels or join the proxy network. Instead:
- The host-networked service listens on a host port (e.g., 31243)
- A companion web-facing service (e.g., nginx) joins the
proxynetwork - The companion proxies API routes to
http://host.docker.internal:<port> - The companion needs
extra_hosts: ["host.docker.internal:host-gateway"]
Step 6: Apply Traefik Integration
Standard label set (see references/traefik-labels.md). Additional patterns:
- Auth-protected: Add
traefik.http.routers.<svc>.middlewares=google-auth@file - Multi-subdomain: Define separate routers for each subdomain
- Custom headers: Define middleware in traefik dynamic config at
~/docker/core/traefik/traefik-data/dynamic/<service>.yml
Step 7: Update Aggregator
Each stacks category has a top-level compose.yml that uses extends:
# ~/docker/stacks/<category>/compose.yml
services:
<service-name>:
extends:
file: ./<service>/compose.yml
service: <service-name>
Limitations of extends:
- Cannot extend services with
network_mode: hostorprivileged: true - Networks must be re-declared at the aggregator level
- Named volumes must be declared at the aggregator level
Services incompatible with extends should be noted in a comment and run standalone.
Step 8: Generate Mise Tasks in Source Repo
For services with custom images, add to the source repo's mise.toml:
[tasks.build]
description = "Build the Docker image"
run = "docker build -t <user>/<image>:latest -f <dockerfile> <context>/"
[tasks.inject]
description = "Resolve secrets from 1Password into <service>/.env"
run = """
cd ~/docker/stacks/<category>/<service>
~/docker/stacks/_lib/op-inject.sh
"""
[tasks.deploy]
description = "Re-inject secrets, pull and restart in stacks deployment"
depends = ["inject"]
run = """
cd ~/docker/stacks/<category>/<service>
docker compose pull <service-name>
docker compose up -d <service-name>
"""
The inject step is required whenever the stack uses .env.template with op://... references (preferred pattern — see references/secret-injection.md). Skip it only for stacks that legitimately have no secrets.
Reference Files
references/traefik-labels.md- Label templates, middleware patterns, host-networking workaroundreferences/stacks-layout.md- Monorepo structure, aggregator format, naming conventionsreferences/deployment-modes.md- Mode comparison with real examples from existing stacksreferences/secret-injection.md- 1Password +op injectpattern. Commit.env.template, resolve.envat deploy via~/docker/stacks/_lib/op-inject.sh. Migration recipe for existing stacks included.