Files
professional-website/docker-compose.swarm.yml
Jason Woltje b47c5e420a
All checks were successful
ci/woodpecker/push/web Pipeline was successful
feat(site): port stitch design system + seed-ready content (#5)
Co-authored-by: Jason Woltje <jason@diversecanvas.com>
Co-committed-by: Jason Woltje <jason@diversecanvas.com>
2026-04-15 01:16:41 +00:00

111 lines
4.0 KiB
YAML

# =============================================================================
# jasonwoltje.com — Docker Swarm / Portainer stack
# =============================================================================
#
# Deploy target: w-docker0 (10.1.1.45), Portainer endpoint 7.
# Routing:
# jasonwoltje.com / www.jasonwoltje.com -> web (Next.js + Payload CMS)
#
# Ingress pattern (mirrors MosaicStack):
# Edge Traefik (10.1.1.43) terminates TLS
# -> per-swarm Traefik on w-docker0 on entrypoint "web" (HTTP)
# -> web:3000
#
# Usage (Portainer):
# Stacks -> Add Stack -> Git repository
# URL: https://git.mosaicstack.dev/jason.woltje/professional-website
# Compose path: docker-compose.swarm.yml
# Env vars: see .env.example (all required unless marked optional)
# Deploy
#
# Image tag rule: WEB_IMAGE_TAG MUST be an immutable tag (sha-<8> or vX.Y.Z).
# Never point this stack at `latest`.
# =============================================================================
services:
web:
image: git.mosaicstack.dev/jason.woltje/professional-website:${WEB_IMAGE_TAG}
environment:
DATABASE_URI: postgresql://${PAYLOAD_POSTGRES_USER}:${PAYLOAD_POSTGRES_PASSWORD}@jasonwoltje_postgres:5432/${PAYLOAD_POSTGRES_DB}
PAYLOAD_SECRET: ${PAYLOAD_SECRET}
PAYLOAD_PUBLIC_SERVER_URL: https://${SITE_DOMAIN:-jasonwoltje.com}
NEXT_PUBLIC_SITE_URL: https://${SITE_DOMAIN:-jasonwoltje.com}
NEXT_PUBLIC_BUILD_SHA: ${WEB_IMAGE_TAG}
NEXT_PUBLIC_BUILD_REV: ${WEB_IMAGE_TAG}
NEXT_PUBLIC_TURNSTILE_SITE_KEY: ${NEXT_PUBLIC_TURNSTILE_SITE_KEY:-}
TURNSTILE_SECRET_KEY: ${TURNSTILE_SECRET_KEY:-}
NEXT_PUBLIC_UMAMI_SRC: ${NEXT_PUBLIC_UMAMI_SRC:-}
NEXT_PUBLIC_UMAMI_WEBSITE_ID: ${NEXT_PUBLIC_UMAMI_WEBSITE_ID:-}
RESEND_API_KEY: ${RESEND_API_KEY:-}
RESEND_FROM: ${RESEND_FROM:-no-reply@jasonwoltje.com}
RESEND_TO: ${RESEND_TO:-jason@diversecanvas.com}
MAUTIC_FORM_URL: ${MAUTIC_FORM_URL:-}
volumes:
- media:/app/media
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:3000/api/health || exit 1"]
interval: 30s
timeout: 10s
retries: 3
start_period: 60s
networks:
- internal
- traefik-public
deploy:
replicas: 1
update_config:
parallelism: 1
delay: 30s
order: start-first
restart_policy:
condition: on-failure
delay: 5s
max_attempts: 5
window: 120s
labels:
- "traefik.enable=true"
- "traefik.http.routers.jasonwoltje.rule=Host(`${SITE_DOMAIN:-jasonwoltje.com}`) || Host(`www.${SITE_DOMAIN:-jasonwoltje.com}`)"
- "traefik.http.routers.jasonwoltje.entrypoints=web"
- "traefik.http.services.jasonwoltje.loadbalancer.server.port=3000"
# www -> apex 301
- "traefik.http.middlewares.jasonwoltje-www-redirect.redirectregex.regex=^https?://www\\.${SITE_DOMAIN:-jasonwoltje.com}/(.*)"
- "traefik.http.middlewares.jasonwoltje-www-redirect.redirectregex.replacement=https://${SITE_DOMAIN:-jasonwoltje.com}/$${1}"
- "traefik.http.middlewares.jasonwoltje-www-redirect.redirectregex.permanent=true"
- "traefik.http.routers.jasonwoltje.middlewares=jasonwoltje-www-redirect"
postgres:
image: postgres:17-alpine
environment:
POSTGRES_DB: ${PAYLOAD_POSTGRES_DB:-payload}
POSTGRES_USER: ${PAYLOAD_POSTGRES_USER:-payload}
POSTGRES_PASSWORD: ${PAYLOAD_POSTGRES_PASSWORD}
volumes:
- postgres-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER}"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
networks:
- internal
deploy:
replicas: 1
restart_policy:
condition: on-failure
delay: 5s
max_attempts: 5
placement:
constraints:
- node.role == manager
volumes:
postgres-data:
media:
networks:
internal:
driver: overlay
traefik-public:
external: true