121 lines
4.7 KiB
YAML
121 lines
4.7 KiB
YAML
# =============================================================================
|
||
# FUTURE-STACK BLUEPRINT — copy, rename, and fill in the TODO items.
|
||
# This file is the minimum viable Swarm stack template for this homelab.
|
||
# Every field section is either REQUIRED or RECOMMENDED. Delete nothing;
|
||
# fill TODOs or leave the defaults for sections that do not apply.
|
||
#
|
||
# Naming: rename this file to <service>.stack.yml in kebab-case.
|
||
# Deploy: copy playbooks/docker/deploy_example_stack.yml -> deploy_<service>.yml
|
||
# and fill in the TODO items there too.
|
||
# =============================================================================
|
||
x-info:
|
||
# TODO: fill in canonical upstream URLs.
|
||
github: https://github.com/TODO/TODO
|
||
docs: https://TODO.example.com/docs
|
||
changelog: https://github.com/TODO/TODO/releases
|
||
# lifecycle: planned | active | stable | deprecated
|
||
homelab_status: planned
|
||
last_updated: 2026-03-14
|
||
|
||
# Managed by Ansible — manual edits will be overwritten on next deploy.
|
||
# Source: ansible/templates/stacks/<service>.stack.yml
|
||
# Deploy: ansible-playbook -i inventory/hosts.ini playbooks/docker/deploy_<service>.yml
|
||
|
||
version: "3.9"
|
||
|
||
services:
|
||
example-app:
|
||
# REQUIRED: Pin to a specific digest or semver tag. Never use :latest.
|
||
# WHY: Floating tags break idempotency — redeploying a service with an
|
||
# unpinned image may silently change the running version.
|
||
image: nginx:1.27.4-alpine
|
||
|
||
environment:
|
||
- TZ=America/New_York
|
||
# REQUIRED for secrets: reference vault variables injected by the deploy
|
||
# playbook. Never hardcode passwords here.
|
||
# Example: - EXAMPLE_DB_PASSWORD={{ vault_example_db_password }}
|
||
# TODO: add service-specific environment variables.
|
||
|
||
# REQUIRED for services with persistent data: use absolute bind-mount paths.
|
||
# WHY: Swarm services have no well-defined working directory; relative paths
|
||
# (e.g. ./data) are unsafe and non-portable in Swarm stacks.
|
||
# WHY pre-existence check: the deploy playbook MUST assert these paths exist
|
||
# before deploy to protect existing data from accidental fresh bootstrap.
|
||
volumes:
|
||
- /mnt/homelab/apps/example/data:/data # TODO: adjust per service
|
||
|
||
ports:
|
||
# TODO: choose a port in the 8200–8299 range to avoid collisions.
|
||
- "8299:80" # host:container
|
||
|
||
# REQUIRED: Always declare healthcheck so Swarm scheduler can detect failure.
|
||
healthcheck:
|
||
test: ["CMD", "curl", "-sf", "http://localhost:80/"]
|
||
interval: 30s
|
||
timeout: 5s
|
||
retries: 5
|
||
start_period: 30s
|
||
|
||
networks:
|
||
- proxy-net
|
||
|
||
# Top-level labels: for non-Swarm consumers (homepage, glance).
|
||
# Do NOT put traefik labels here — traefik-kop reads deploy.labels only.
|
||
labels:
|
||
- "homepage.name=Example App"
|
||
- "homepage.icon=si:nginx"
|
||
- "homepage.url=https://example.castaldifamily.com"
|
||
- "homepage.description=TODO: one-line description"
|
||
|
||
deploy:
|
||
replicas: 1
|
||
|
||
placement:
|
||
constraints:
|
||
# REQUIRED: pin to a specific node when using bind-mount volumes or
|
||
# hardware (GPU, USB). Otherwise, remove this block for free placement.
|
||
- node.hostname == swarm-manager-1 # TODO: adjust or remove
|
||
|
||
# REQUIRED: declare deploy labels for traefik-kop route publication.
|
||
# WHY deploy.labels (not top-level): traefik-kop reads Swarm *service*
|
||
# labels via the Docker API. Top-level labels are on the container image.
|
||
labels:
|
||
- "traefik.enable=true"
|
||
- "traefik.http.routers.example.rule=Host(`example.castaldifamily.com`)"
|
||
- "traefik.http.routers.example.entrypoints=websecure"
|
||
- "traefik.http.routers.example.tls=true"
|
||
- "traefik.http.routers.example.tls.certresolver=cloudflare"
|
||
# WHY server.url (not server.port): routes Traefik to the Swarm routing
|
||
# mesh IP rather than a container IP, which is ephemeral in Swarm.
|
||
- "traefik.http.services.example.loadbalancer.server.url=http://{{ edge_routing.swarm.bind_ip }}:8299"
|
||
|
||
resources:
|
||
limits:
|
||
memory: 512M # TODO: set appropriate limit for this service
|
||
cpus: "0.5" # TODO: set appropriate cpu limit
|
||
|
||
restart_policy:
|
||
condition: on-failure
|
||
delay: 10s
|
||
max_attempts: 3
|
||
window: 60s
|
||
|
||
update_config:
|
||
parallelism: 1
|
||
order: start-first # prefer start-first for stateless; stop-first for stateful/DBs
|
||
failure_action: rollback
|
||
delay: 10s
|
||
monitor: 30s
|
||
|
||
rollback_config:
|
||
parallelism: 1
|
||
order: stop-first
|
||
|
||
# REQUIRED: declare all external networks the stack consumes.
|
||
# Networks NOT listed here will fail silently at deploy time.
|
||
networks:
|
||
proxy-net:
|
||
external: true
|
||
name: proxy-net
|