Files
professional-website/docker-compose.swarm.yml
Jason Woltje 462d938297 feat(scaffold): Next 16 + Payload 3 scaffold with Kaniko CI and Swarm deploy
Initial app scaffold wired end-to-end: Payload 3.82 CMS integrated with Next
16.2 App Router (standalone output), PostgreSQL 17 adapter, Lexical rich text,
Tailwind 3 with Material 3 token palette ported from the stitch technical-
editorial design, self-hosted Space Grotesk + Inter via next/font, and
lucide-react icons. Admin lives at /admin, REST/GraphQL at /api/*, and
/api/health returns build SHA/REV for deploy verification.

Seven collections (Users, Media, Categories, Projects, Posts, Gear,
ContactSubmissions) and six globals (Home, About, Contact, Resume,
Navigation, SEO) model the content outlined in docs/PRD.md.

Multi-stage Dockerfile builds a non-root standalone runner; Woodpecker
pipeline lints, typechecks, builds, audits, builds with Kaniko to
git.mosaicstack.dev, scans with Trivy, and links the package. Swarm
compose mirrors the mosaic-stack-website Traefik entrypoints=web pattern
with www->apex redirect and immutable WEB_IMAGE_TAG.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-13 21:18:01 -05:00

111 lines
3.9 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}
TURNSTILE_SITE_KEY: ${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