Author SHA1 Message Date
AVB 0577496bb6 Delete ai/ai.env
ci/woodpecker/push/deploy Pipeline was successful
2026-07-27 07:57:07 -07:00
AVB c609c3b45a Delete auth/auth.env
ci/woodpecker/push/deploy Pipeline was successful
2026-07-27 07:56:13 -07:00
admin 00f9fa352c vaultwarden: retry secret re-provision trigger (previous run had empty changed-files)
ci/woodpecker/push/deploy Pipeline was successful
2026-07-26 22:27:58 -07:00
admin 37aca1de0a vaultwarden: trigger secret re-provision (db -> postgresql alias)
ci/woodpecker/push/deploy Pipeline was successful
2026-07-26 22:10:46 -07:00
admin cebc6a0a8f uptime-kuma: set restart_policy.delay to 30s
ci/woodpecker/push/deploy Pipeline was successful
ci/woodpecker/cron/renovate Pipeline was successful
Increases the delay before Swarm restarts a failed uptime-kuma task
from the default 5s to 30s. Applied live via `docker service update
--restart-delay 30s` on 2026-07-26; this persists it so it survives
the next stack deploy.

Note: this does not pin the task to a specific node or prevent
cross-node reschedule races against the CephFS-backed SQLite data
dir - it only slows the restart-after-failure loop. See prior
incident notes for the DB corruption root cause discussion.
2026-07-26 17:34:35 -07:00
admin 7dbc6f490e chore(ai): sync ai.yaml with host (manual litellm v1.93.0 upgrade comment)
ci/woodpecker/push/deploy Pipeline is pending
2026-07-25 22:20:28 -07:00
admin 03b32ee449 chore(traefik): remove flat traefik.env, superseded by traefik/traefik.env
ci/woodpecker/push/deploy Pipeline was successful
2026-07-25 22:20:27 -07:00
admin aef4be5a94 chore(traefik): remove flat traefik.yaml, superseded by traefik/traefik.yaml
ci/woodpecker/push/deploy Pipeline was canceled
2026-07-25 22:20:27 -07:00
admin 92b01d915c chore(auth): remove flat auth.env, superseded by auth/auth.env
ci/woodpecker/push/deploy Pipeline was canceled
2026-07-25 22:20:26 -07:00
admin 07a81f96aa chore(auth): remove flat auth.yaml, superseded by auth/auth.yaml
ci/woodpecker/push/deploy Pipeline was canceled
2026-07-25 22:20:26 -07:00
admin 07635d1722 feat(traefik): migrate to folder-based stack layout
ci/woodpecker/push/deploy Pipeline was successful
2026-07-25 22:19:26 -07:00
admin b4fed87f24 feat(traefik): migrate to folder-based stack layout
ci/woodpecker/push/deploy Pipeline is pending
2026-07-25 22:19:25 -07:00
admin e06d300b3c feat(auth): migrate to folder-based stack layout
ci/woodpecker/push/deploy Pipeline was canceled
2026-07-25 22:19:25 -07:00
admin 39c34c8923 feat(auth): migrate to folder-based stack layout
ci/woodpecker/push/deploy Pipeline was canceled
2026-07-25 22:19:24 -07:00
admin 0dc7e915f3 chore(ci): remove root .woodpecker.yml, superseded by .woodpecker/deploy.yml
ci/woodpecker/push/deploy Pipeline was successful
Woodpecker ignores this file entirely once .woodpecker/ folder exists
(config resolution: .woodpecker/ > .woodpecker.yaml > .woodpecker.yml).
Content preserved verbatim in .woodpecker/deploy.yml.
2026-07-25 22:00:57 -07:00
admin 1ad8e41799 fix(ci): move .woodpecker.yml -> .woodpecker/deploy.yml
ci/woodpecker/push/deploy Pipeline was canceled
Root cause of Woodpecker push pipelines silently no-op'ing since
2026-07-18: Woodpecker's config resolution is
  .woodpecker/*.{yaml,yml} -> .woodpecker.yaml -> .woodpecker.yml
Once .woodpecker/renovate.yml existed, the root .woodpecker.yml was
entirely ignored. Every push webhook hit renovate.yml's `when: event:
cron` filter, matched zero steps, and Woodpecker logged
"ignoring hook: 'when' filters filtered out all steps" instead of
running deploy.

Moving the push pipeline into .woodpecker/deploy.yml restores it as
an independent workflow alongside renovate.yml -- each file's own
`when:` filter now applies correctly (push -> deploy.yml, cron ->
renovate.yml).
2026-07-25 22:00:49 -07:00
admin 0ef9c8d6e5 feat(mcpo): sync config.json to Gitea as source of truth
Adds powerautomate entry with Azure app registration credentials.
Auth dir (/volume1/docker/mcpo/powerautomate-auth/) and config stub
already exist on host from prior session.

NOTE: powerautomate-mcp package not yet on npm - npx will fail until
published. Tracked here for GitOps pipeline; will update command when
package is available or we switch to build-from-source approach.
2026-07-25 15:10:07 -07:00
admin b6c3fcdd58 litellm: upgrade v1.92.1 -> v1.93.0 to fix Bedrock parallel tool call truncation bug 2026-07-24 22:16:39 -07:00
admin 9b6322208c feat(mcpo): sync config.json from host (real PA_MCP_CLIENT_ID + TENANT_ID) 2026-07-24 22:06:24 -07:00
admin 26dc2b50bc chore(ai): remove flat ai.yaml, superseded by ai/ai.yaml 2026-07-24 22:02:22 -07:00
admin 81ab653402 feat(ai): add ai/ai.env (sourced from host, LITELLM_MODIFY_PARAMS=False) 2026-07-24 22:02:14 -07:00
admin d12ac4daa9 feat(ai): migrate to folder-based stack layout 2026-07-24 22:01:45 -07:00
AVB 8f00be333e Update ai.yaml
ci/woodpecker/cron/renovate Pipeline was successful
2026-07-23 17:18:08 -07:00
AVB 7fcc8a9ca5 Update ai.yaml 2026-07-23 17:02:42 -07:00
admin c79ade9914 feat(woodpecker): add PA_MCP_CLIENT_ID secret provisioning for ai stack
ci/woodpecker/cron/renovate Pipeline was successful
2026-07-19 14:23:22 -07:00
admin c38e41c741 chore(mcpo): sync config.json from live disk - adds powerautomate entry 2026-07-19 14:20:27 -07:00
admin 712516ac29 feat(mcpo): add config.json to source control with powerautomate, ms365 fixes
- Pin ms365 to 0.129.0 with --discovery flag (fixes MCPO schema parse error)
- Add powerautomate entry (PA_MCP_CLIENT_ID placeholder - needs Azure app reg)
- Update authentik token (was expired)
- teams-mcp-auth volume now source of truth for ms365/teams auth cache
- powerautomate-auth volume wired for future PA auth cache

NOTE: config.json was previously managed manually on disk only.
This commit brings it under GitOps control.
2026-07-19 14:16:40 -07:00
admin bf13c74c1d chore(renovate): remove unused TEAMS_WEBHOOK env - notifications via Gitea webhook 2026-07-19 07:00:37 -07:00
admin d456c96454 chore(renovate): revert postUpgradeTasks - notifications via Gitea webhook instead 2026-07-19 07:00:17 -07:00
admin ab20c708a0 chore(renovate): add postUpgradeTasks Teams notification via Power Automate webhook 2026-07-19 06:59:49 -07:00
admin 835e534ec1 chore(renovate): pass TEAMS_WEBHOOK secret into renovate container 2026-07-19 06:59:28 -07:00
admin a2b52c5ba1 chore(renovate): add Teams webhook notification via Power Automate 2026-07-19 06:59:02 -07:00
admin 9a18cda853 chore(renovate): fix config warnings - remove global-only options, disable woodpecker manager
ci/woodpecker/cron/renovate Pipeline was successful
2026-07-18 22:48:22 -07:00
admin 0f3901152a feat: add woodpecker cron pipeline for renovate (v3 syntax)
ci/woodpecker/cron/renovate Pipeline was successful
2026-07-18 22:05:19 -07:00
admin afdd23184d feat: add renovate.json configuration
ci/woodpecker/push/woodpecker Pipeline was successful
2026-07-18 22:05:01 -07:00
admin 6f65326890 feat: add renovate env example
ci/woodpecker/push/woodpecker Pipeline was canceled
2026-07-18 22:05:01 -07:00
admin f212fa07b7 feat: add renovate stack (image update automation)
ci/woodpecker/push/woodpecker Pipeline was canceled
2026-07-18 22:05:00 -07:00
admin ae605838c3 traefik: fix update order for host-mode/VRRP services (start-first -> stop-first)
ci/woodpecker/push/woodpecker Pipeline was successful
reverse-proxy publishes host-mode ports (80/443/1883/5201/5431) — start-first
can never bind while the old task holds the ports, deadlocking every rolling
update. keepalived master/backup run VRRP on the host network — concurrent
old+new speakers with the same router-id risk VIP flapping. Both now use
stop-first (global mode + parallelism=1 + VIP failover keeps ingress up).
Stateless helpers (whoami, speedtest, iperf3, certs-dumper) keep start-first.

Step 1 of Tier 0/1 pipeline inclusion (prep for removing traefik/woodpecker
from the bootstrap manual-deploy guard).
2026-07-16 23:04:00 -07:00
admin 2c8db4118d fix(pipeline): sync bootstrap stack files before guard, not after
ci/woodpecker/push/woodpecker Pipeline was successful
2026-07-16 22:29:36 -07:00
admin e1a7fb59e3 Add bootstrap-tier guard to deploy step (traefik/woodpecker/postgresql skip auto-deploy)
ci/woodpecker/push/woodpecker Pipeline was successful
2026-07-16 21:44:53 -07:00
admin 45b0d7109e Add update_config/rollback_config start-first rolling deploy; fix depends_on indent bug
ci/woodpecker/push/woodpecker Pipeline was canceled
2026-07-16 21:44:47 -07:00
admin 1659b5b861 Add update_config/rollback_config rolling deploy to all traefik services (start-first, parallelism=1)
ci/woodpecker/push/woodpecker Pipeline was canceled
2026-07-16 21:44:41 -07:00
AVB d7727fd788 Merge pull request 'chore: bump litellm-database v1.90.3 → v1.92.0' (#1) from chore/litellm-v1.92.0 into main
ci/woodpecker/push/woodpecker Pipeline was successful
Reviewed-on: #1
2026-07-16 07:19:11 -07:00
admin 40fb1329c3 chore: bump litellm-database v1.90.3 -> v1.92.0
Latest stable release. Includes Bedrock fixes since 1.90.x
(anthropic-beta header forwarding, converse stream handling,
STS region decoupling).
2026-07-15 22:00:16 -07:00
admin 308ac008ba fix(security): correct crowdsec-web-ui image typo — thebluffman85 -> theduffman85
ci/woodpecker/push/woodpecker Pipeline was successful
Image ghcr.io/thebluffman85/crowdsec-web-ui:latest never existed (typo in org name).
Correct upstream project is TheDuffman85/crowdsec-web-ui on GitHub. Fixed to
ghcr.io/theduffman85/crowdsec-web-ui:latest.

Deployed and verified live on docker-2 prior to this commit:
- security_crowdsec-web-ui now pulls successfully and is Running
- Host(`secure.bryanmail.net`) rule intact, all CROWDSEC_* env vars correct
- security_crowdsec unaffected, still running
2026-07-10 15:43:01 -07:00
admin b6bf14f917 feat(postgresql): complete Pattern C migration — remove leftover env_file directives
ci/woodpecker/push/woodpecker Pipeline was successful
Docker secrets (postgresql_password, postgresql_pgadmin_password) already handled
POSTGRES_PASSWORD_FILE/PGADMIN_DEFAULT_PASSWORD_FILE. This removes the two remaining
`env_file: - postgresql.env` lines (postgresql + pgadmin services) which were redundant
and left the stack in a half-migrated hybrid state. Non-secret vars (POSTGRES_USER,
PGADMIN_DEFAULT_EMAIL, DATABASUS_HOST, PGADMIN_HOST) continue to be supplied via
stack-deploy.sh's envsubst mechanism from postgresql.env at deploy time — no functional
change to those values.

Deployed and verified live on docker-2 prior to this commit:
- postgresql_postgresql: pg_isready OK, all 21 databases intact, psql auth via Docker secret confirmed
- postgresql_pgadmin: Host(`dba.bryanmail.net`) rule + PGADMIN_DEFAULT_EMAIL correct
- postgresql_databasus: running
- All dependent stacks (mealie, vaultwarden, auth, media, homeassistant, guacamole, security) remained healthy throughout, zero reconnect issues
2026-07-10 15:36:15 -07:00
admin 3a5483cf53 chore(guacamole): remove DOMAIN - provided by global.env
ci/woodpecker/push/woodpecker Pipeline was successful
2026-07-10 15:31:30 -07:00
admin 5454f7acf4 chore(mealie): remove DOMAIN - provided by global.env
ci/woodpecker/push/woodpecker Pipeline was canceled
2026-07-10 15:31:29 -07:00
admin 48d20d613f chore(security): remove DOMAIN - provided by global.env
ci/woodpecker/push/woodpecker Pipeline was canceled
2026-07-10 15:31:29 -07:00
admin cf82e36380 chore(vaultwarden): remove DOMAIN+ALT_DOMAIN - provided by global.env
ci/woodpecker/push/woodpecker Pipeline is pending
2026-07-10 15:31:28 -07:00
admin b25429091f chore(homeassistant): remove DOMAIN - provided by global.env
ci/woodpecker/push/woodpecker Pipeline is pending
2026-07-10 15:31:28 -07:00
admin 5f0459eb35 chore(maintenance): remove DOMAIN - provided by global.env
ci/woodpecker/push/woodpecker Pipeline was canceled
2026-07-10 15:31:27 -07:00
admin c3c05b7164 chore(traefik): remove DOMAIN - provided by global.env
ci/woodpecker/push/woodpecker Pipeline was canceled
2026-07-10 15:31:27 -07:00
admin 97cdb74506 chore(auth): remove DOMAIN - provided by global.env
ci/woodpecker/push/woodpecker Pipeline is running
2026-07-10 15:31:27 -07:00
21 changed files with 579 additions and 83 deletions
+14 -19
View File
@@ -1,8 +1,6 @@
when: when:
- event: push - event: push
branch: main branch: main
- event: manual
branch: main
steps: steps:
validate: validate:
@@ -217,7 +215,7 @@ steps:
for STACK in $ALL_STACKS; do for STACK in $ALL_STACKS; do
echo "--- Deploying: $STACK ---" echo "--- Deploying: $STACK ---"
# Sync flat file or entire folder # Sync files to host first (always, even for bootstrap stacks)
if [ -d "$STACK" ]; then if [ -d "$STACK" ]; then
rsync -av -e "ssh -o StrictHostKeyChecking=no -i ~/.ssh/id_rsa" \ rsync -av -e "ssh -o StrictHostKeyChecking=no -i ~/.ssh/id_rsa" \
"$STACK/" root@${SWARM_MANAGER_IP}:/volume1/docker/compose-files/$STACK/ "$STACK/" root@${SWARM_MANAGER_IP}:/volume1/docker/compose-files/$STACK/
@@ -225,6 +223,16 @@ steps:
rsync -av -e "ssh -o StrictHostKeyChecking=no -i ~/.ssh/id_rsa" \ rsync -av -e "ssh -o StrictHostKeyChecking=no -i ~/.ssh/id_rsa" \
"${STACK}.yaml" root@${SWARM_MANAGER_IP}:/volume1/docker/compose-files/ "${STACK}.yaml" root@${SWARM_MANAGER_IP}:/volume1/docker/compose-files/
fi fi
# Bootstrap-tier guard: file synced to host, deploy is MANUAL
case "$STACK" in
traefik|woodpecker|postgresql)
echo " [BOOTSTRAP] $STACK: file synced. Deploy is MANUAL."
echo " Run: bash /volume1/docker/compose-files/deploy/stack-deploy.sh $STACK"
continue ;;
esac
# Tier 2: auto-deploy
ssh -o StrictHostKeyChecking=no root@${SWARM_MANAGER_IP} \ ssh -o StrictHostKeyChecking=no root@${SWARM_MANAGER_IP} \
"bash /volume1/docker/compose-files/deploy/stack-deploy.sh $STACK" \ "bash /volume1/docker/compose-files/deploy/stack-deploy.sh $STACK" \
&& echo " OK $STACK" || { echo " FAIL $STACK"; exit 1; } && echo " OK $STACK" || { echo " FAIL $STACK"; exit 1; }
@@ -249,27 +257,14 @@ steps:
FOLDER_STACKS=$(echo "$CHANGED_FILES" | grep -E '^[^/]+/' | cut -d/ -f1 | sort -u || true) FOLDER_STACKS=$(echo "$CHANGED_FILES" | grep -E '^[^/]+/' | cut -d/ -f1 | sort -u || true)
ALL_STACKS=$(printf '%s\n%s' "$FLAT_STACKS" "$FOLDER_STACKS" | grep -v '^$' | sort -u) ALL_STACKS=$(printf '%s\n%s' "$FLAT_STACKS" "$FOLDER_STACKS" | grep -v '^$' | sort -u)
[ -z "$ALL_STACKS" ] && exit 0 [ -z "$ALL_STACKS" ] && exit 0
sleep 5
# Give Swarm time to schedule tasks after deploy
echo "Waiting 15s for Swarm to schedule tasks..."
sleep 15
FAILED=0
for STACK in $ALL_STACKS; do for STACK in $ALL_STACKS; do
echo "--- $STACK ---" echo "--- $STACK ---"
RUNNING=$(ssh -o StrictHostKeyChecking=no root@${SWARM_MANAGER_IP} \ ssh -o StrictHostKeyChecking=no root@${SWARM_MANAGER_IP} \
"docker stack ps $STACK --filter desired-state=running \ "docker stack ps $STACK --filter desired-state=running \
--format ' {{.Name}} {{.CurrentState}}'" 2>&1) --format ' {{.Name}} {{.CurrentState}}'"
echo "$RUNNING"
COUNT=$(echo "$RUNNING" | grep -c 'Running\|Starting\|Preparing' || true)
if [ "$COUNT" -eq 0 ]; then
echo " ERROR: No running tasks found for stack '$STACK' — deploy may have failed!"
FAILED=1
fi
done done
[ "$FAILED" -eq 0 ] || exit 1
notify-success: notify-success:
image: alpine:latest image: alpine:latest
environment: environment:
+44
View File
@@ -0,0 +1,44 @@
# ================================================================
# Renovate Bot - Cron Pipeline
# ================================================================
# WOODPECKER v3 REQUIREMENTS - DO NOT DOWNGRADE SYNTAX:
#
# CORRECT (v3):
# environment:
# MY_VAR:
# from_secret: my_secret
#
# WRONG (v1/v2 - BREAKS in v3):
# secrets: [my_secret]
# secrets:
# - my_secret
#
# This pipeline ONLY runs on the "renovate" cron schedule.
# It does NOT run on push events - that is handled by .woodpecker.yml
# Teams notifications are delivered via Gitea webhook → Power Automate
# ================================================================
when:
- event: cron
cron: renovate
steps:
run-renovate:
image: renovate/renovate:latest
# Pull policy: always ensure latest renovate image
pull: true
environment:
# ⚠️ Woodpecker v3 syntax - from_secret map form required
RENOVATE_TOKEN:
from_secret: renovate_token
# Renovate platform config - self-hosted Gitea
RENOVATE_PLATFORM: gitea
RENOVATE_ENDPOINT: https://git.bryanmail.net
RENOVATE_GIT_AUTHOR: "Renovate Bot <renovate-bot@bryanmail.net>"
# Repos to scan - space-separated for multiple
RENOVATE_REPOSITORIES: "homelab/compose-files"
# Use the renovate.json in the repo root for all other config
RENOVATE_CONFIG_FILE: renovate.json
LOG_LEVEL: info
commands:
- renovate
+5 -29
View File
@@ -56,8 +56,8 @@ services:
- traefik.http.services.open-webui.loadbalancer.sticky.cookie.secure=true - traefik.http.services.open-webui.loadbalancer.sticky.cookie.secure=true
- traefik.http.services.open-webui.loadbalancer.sticky.cookie.httponly=true - traefik.http.services.open-webui.loadbalancer.sticky.cookie.httponly=true
- traefik.swarm.network=traefik_backend - traefik.swarm.network=traefik_backend
litellm: # litellm:
image: docker.litellm.ai/berriai/litellm-database:v1.90.3 #main-latest image: docker.litellm.ai/berriai/litellm-database:v1.93.0 # upgraded from v1.92.1; fixes Bedrock parallel tool call truncation bug
command: --config /app/config.yaml --port 4000 command: --config /app/config.yaml --port 4000
environment: environment:
- AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID} - AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID}
@@ -68,14 +68,10 @@ services:
- DATABASE_URL=${DATABASE_URL} - DATABASE_URL=${DATABASE_URL}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD} - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
- LITELLM_MODIFY_PARAMS=${LITELLM_MODIFY_PARAMS} - LITELLM_MODIFY_PARAMS=${LITELLM_MODIFY_PARAMS}
- LITELLM_DATABASE_MIGRATIONS=${LITELLM_DATABASE_MIGRATIONS}
networks: networks:
- traefik_backend - traefik_backend
- postgresql_db-backend - postgresql_db-backend
# ports:
# - target: 4000
# published: 4000
# protocol: tcp
# mode: ingress
volumes: volumes:
- type: bind - type: bind
source: /volume1/docker/litellm/app/config.yaml source: /volume1/docker/litellm/app/config.yaml
@@ -119,26 +115,17 @@ services:
- ${MCPO_API_KEY} - ${MCPO_API_KEY}
- --hot-reload - --hot-reload
environment: environment:
- PROXMOX_ALLOW_UNSAFE_COMMANDS=false #true - PROXMOX_ALLOW_UNSAFE_COMMANDS=false
volumes: volumes:
- /volume1/docker/mcpo/config-critical.json:/app/config/config.json:ro - /volume1/docker/mcpo/config-critical.json:/app/config/config.json:ro
- /volume1/docker/mcpo/data:/mcpo_data - /volume1/docker/mcpo/data:/mcpo_data
- /volume1/docker/cronicle/ssh_keys:/app/ssh_keys:ro - /volume1/docker/cronicle/ssh_keys:/app/ssh_keys:ro
# - /volume1/docker/mcpo/teams-mcp-auth:/app/teams-mcp-auth
- /volume1/docker/mcpo/uv-cache:/app/uv-cache - /volume1/docker/mcpo/uv-cache:/app/uv-cache
networks: networks:
- traefik_backend - traefik_backend
deploy: deploy:
labels: labels:
- traefik.enable=false - traefik.enable=false
# - traefik.http.routers.mcp.rule=Host(`mcpc.bryanmail.net`)
# - traefik.http.routers.mcp.tls=true
# - traefik.http.routers.mcp.tls.certresolver=letsencrypt
# - traefik.http.routers.mcp.entrypoints=websecure
# - traefik.http.services.mcp.loadbalancer.server.port=8000
# - traefik.http.services.mcp.loadbalancer.server.scheme=http
# - traefik.http.routers.mcp.middlewares=forwardAuth-authentik@file, crowdsec@file
# - traefik.swarm.network=traefik_backend
mcpo: mcpo:
image: ghcr.io/open-webui/mcpo:main image: ghcr.io/open-webui/mcpo:main
command: command:
@@ -153,28 +140,17 @@ services:
- /volume1/docker/mcpo/config.json:/app/config/config.json:ro - /volume1/docker/mcpo/config.json:/app/config/config.json:ro
- /volume1/docker/mcpo/data:/mcpo_data - /volume1/docker/mcpo/data:/mcpo_data
- /volume1/docker/cronicle/ssh_keys:/app/ssh_keys:ro - /volume1/docker/cronicle/ssh_keys:/app/ssh_keys:ro
# - /volume1/docker/mcpo/teams-mcp-auth:/app/teams-mcp-auth
- /volume1/docker/mcpo/uv-cache:/app/uv-cache - /volume1/docker/mcpo/uv-cache:/app/uv-cache
networks: networks:
- traefik_backend - traefik_backend
deploy: deploy:
labels: labels:
- traefik.enable=false - traefik.enable=false
# - traefik.http.routers.mcp.rule=Host(`mcp.bryanmail.net`)
# - traefik.http.routers.mcp.tls=true
# - traefik.http.routers.mcp.tls.certresolver=letsencrypt
# - traefik.http.routers.mcp.entrypoints=websecure
# - traefik.http.services.mcp.loadbalancer.server.port=8000
# - traefik.http.services.mcp.loadbalancer.server.scheme=http
# - traefik.http.routers.mcp.middlewares=forwardAuth-authentik@file, crowdsec@file
# - traefik.swarm.network=traefik_backend
n8n: n8n:
image: docker.n8n.io/n8nio/n8n image: docker.n8n.io/n8nio/n8n
networks: networks:
- traefik_backend - traefik_backend
- postgresql_db-backend - postgresql_db-backend
# ports:
# - 5678:5678
volumes: volumes:
- /volume1/docker/n8n/data:/home/node/.n8n - /volume1/docker/n8n/data:/home/node/.n8n
- /volume1/docker/n8n/files:/files - /volume1/docker/n8n/files:/files
@@ -188,7 +164,7 @@ services:
- traefik.http.services.n8n.loadbalancer.server.port=5678 - traefik.http.services.n8n.loadbalancer.server.port=5678
- traefik.http.services.n8n.loadbalancer.server.scheme=http - traefik.http.services.n8n.loadbalancer.server.scheme=http
- traefik.http.middlewares.n8n.headers.SSLRedirect=true - traefik.http.middlewares.n8n.headers.SSLRedirect=true
- traefik.http.middlewares.n8n.headers.STSSeconds=315360000 - traefik.http.middlewares.n8n.headers.STSSeconds=31536000
- traefik.http.middlewares.n8n.headers.browserXSSFilter=true - traefik.http.middlewares.n8n.headers.browserXSSFilter=true
- traefik.http.middlewares.n8n.headers.contentTypeNosniff=true - traefik.http.middlewares.n8n.headers.contentTypeNosniff=true
- traefik.http.middlewares.n8n.headers.forceSTSHeader=true - traefik.http.middlewares.n8n.headers.forceSTSHeader=true
View File
+60
View File
@@ -0,0 +1,60 @@
#!/usr/bin/env python3
"""
Temporary script to update .woodpecker.yml with PA_MCP_CLIENT_ID provisioning.
Run once, then delete this file.
"""
import re
import sys
def main():
# Read the current .woodpecker.yml
with open('.woodpecker.yml', 'r') as f:
content = f.read()
# 1. Add PA_MCP_CLIENT_ID to the provision-secrets environment section
# Find VAULTWARDEN_DATABASE_URL and add PA_MCP_CLIENT_ID after it
if 'PA_MCP_CLIENT_ID:' not in content:
env_pattern = r'(VAULTWARDEN_DATABASE_URL:\s+from_secret: vaultwarden_database_url)'
env_replacement = r'\1\n PA_MCP_CLIENT_ID:\n from_secret: pa_mcp_client_id'
content = re.sub(env_pattern, env_replacement, content)
print("✓ Added PA_MCP_CLIENT_ID to provision-secrets environment")
else:
print("⊘ PA_MCP_CLIENT_ID already in environment section")
# 2. Replace the ai) case in provision-secrets commands
# Find "ai) echo ..." and replace with actual provisioning
if 'No Docker secrets for ai' in content:
ai_case_pattern = r'(\s+)(ai\))\s+echo\s+"[^"]*No Docker secrets for ai[^"]*";;'
ai_replacement = r'\1\2\n\1 ssh -o StrictHostKeyChecking=no root@${SWARM_MANAGER_IP} "source /tmp/cs.sh\n\1 create_or_update_secret \'pa_mcp_client_id\' \'${PA_MCP_CLIENT_ID}\'";;'
content = re.sub(ai_case_pattern, ai_replacement, content)
print("✓ Updated ai) case to provision pa_mcp_client_id secret")
else:
print("⊘ ai) case already updated")
# 3. Add mcpo/config.json sync in the deploy step
# Find the deploy scripts sync and add mcpo sync after it
if 'mcpo/config.json' not in content:
# Find the "Always sync deploy/ scripts first" section
deploy_pattern = r'(# Always sync deploy/ scripts first\s+rsync[^\n]+\n\s+deploy/[^\n]+\n)'
mcpo_sync = (
r'\1\n'
r' # Sync mcpo/config.json to host\n'
r' if [ -f "mcpo/config.json" ]; then\n'
r' rsync -av -e "ssh -o StrictHostKeyChecking=no -i ~/.ssh/id_rsa" \\\n'
r' mcpo/config.json root@${SWARM_MANAGER_IP}:/volume1/docker/mcpo/config.json\n'
r' fi\n'
)
content = re.sub(deploy_pattern, mcpo_sync, content, flags=re.DOTALL)
print("✓ Added mcpo/config.json sync to deploy step")
else:
print("⊘ mcpo/config.json sync already present")
# Write the updated content
with open('.woodpecker.yml', 'w') as f:
f.write(content)
print("\n✓ .woodpecker.yml updated successfully")
return 0
if __name__ == '__main__':
sys.exit(main())
+11
View File
@@ -0,0 +1,11 @@
# Guacamole - Pattern B env (folder-mode)
LDAP_HOST=homehub.bryanmail.net
GUACAMOLE_LDAP_SEARCH_BIND_DN=AVB@bryanmail.net
GUACAMOLE_LDAP_BIND_PASSWORD=AVBbryan11
GUACAMOLE_OIDC_AUTHORIZATION_ENDPOINT=https://auth.bryanmail.net/application/o/authorize/
GUACAMOLE_OIDC_JWKS_ENDPOINT=https://auth.bryanmail.net/application/o/guacamole/jwks/
GUACAMOLE_OIDC_ISSUER=https://auth.bryanmail.net/application/o/guacamole/
GUACAMOLE_OIDC_CLIENT_ID=K8yOGyhrOTdFoinsHMX2wVxs7pVq3ADKwBttfOxb
GUACAMOLE_OIDC_CLIENT_SECRET=mEasupfwie7h1sVJeJZLeD2tKt1oXHZzfsagKJ6sIcGJglBBkOLG9pEpUexoWWOKJuvmh2CkIwvSSgelrwwa3p5xFcdujCU5CPEKl4vTOCGQPzEY0MZr0MqwSVk7cafQ
GUACAMOLE_OIDC_REDIRECT_URI=https://remote.bryanmail.net/
GUACAMOLE_DB_PASSWORD=@y6jTnG#XGpLBk
+2
View File
@@ -0,0 +1,2 @@
# Home Assistant - Pattern B env (folder-mode)
FRIGATE_RTSP_PASSWORD=v6@PLPA36H7jqn
+1
View File
@@ -0,0 +1 @@
# Maintenance (Cronicle + Uptime Kuma) - Pattern B env (folder-mode)
+2
View File
@@ -66,6 +66,8 @@ services:
max-size: "10m" max-size: "10m"
max-file: "3" max-file: "3"
deploy: deploy:
restart_policy:
delay: 30s
labels: labels:
- traefik.enable=true - traefik.enable=true
- traefik.http.routers.uptime.rule=Host(`uptime.${DOMAIN}`) - traefik.http.routers.uptime.rule=Host(`uptime.${DOMAIN}`)
+186
View File
@@ -0,0 +1,186 @@
{
"mcpServers": {
"filesystem": {
"command": "npx",
"args": [
"-y",
"@modelcontextprotocol/server-filesystem",
"/mcpo_data/filesystem"
]
},
"memory": {
"command": "npx",
"args": [
"-y",
"@modelcontextprotocol/server-memory"
]
},
"proxmox-nuck7-1": {
"command": "sh",
"args": [
"-c",
"LOG_LEVEL=silent npx -y --package @bldg-7/proxmox-mcp@1.2.1 --package pino-pretty proxmox-mcp 2>/dev/null"
],
"env": {
"PROXMOX_HOST": "192.168.4.11",
"PROXMOX_PORT": "8006",
"PROXMOX_USER": "MCP@pve",
"PROXMOX_TOKEN_NAME": "MCP",
"PROXMOX_TOKEN_VALUE": "2052990e-749f-43f6-be7f-c9ad206281cc",
"PROXMOX_SSL_MODE": "insecure",
"PROXMOX_ALLOW_ELEVATED": "true",
"PROXMOX_SSH_ENABLED": "true",
"PROXMOX_SSH_HOST": "192.168.4.11",
"PROXMOX_SSH_PORT": "22",
"PROXMOX_SSH_USER": "root",
"PROXMOX_SSH_KEY_PATH": "/app/ssh_keys/nuc-cluster",
"PROXMOX_SSH_NODE": "nuck7-1",
"PROXMOX_ALLOW_UNSAFE_COMMANDS": "true"
}
},
"proxmox-nuck7-2": {
"command": "sh",
"args": [
"-c",
"LOG_LEVEL=silent npx -y --package @bldg-7/proxmox-mcp@1.2.1 --package pino-pretty proxmox-mcp 2>/dev/null"
],
"env": {
"PROXMOX_HOST": "192.168.4.12",
"PROXMOX_PORT": "8006",
"PROXMOX_USER": "MCP@pve",
"PROXMOX_TOKEN_NAME": "MCP",
"PROXMOX_TOKEN_VALUE": "2052990e-749f-43f6-be7f-c9ad206281cc",
"PROXMOX_SSL_MODE": "insecure",
"PROXMOX_ALLOW_ELEVATED": "true",
"PROXMOX_SSH_ENABLED": "true",
"PROXMOX_SSH_HOST": "192.168.4.12",
"PROXMOX_SSH_PORT": "22",
"PROXMOX_SSH_USER": "root",
"PROXMOX_SSH_KEY_PATH": "/app/ssh_keys/nuc-cluster",
"PROXMOX_SSH_NODE": "nuck7-2",
"PROXMOX_ALLOW_UNSAFE_COMMANDS": "true"
}
},
"proxmox-nuck7-3": {
"command": "sh",
"args": [
"-c",
"LOG_LEVEL=silent npx -y --package @bldg-7/proxmox-mcp@1.2.1 --package pino-pretty proxmox-mcp 2>/dev/null"
],
"env": {
"PROXMOX_HOST": "192.168.4.13",
"PROXMOX_PORT": "8006",
"PROXMOX_USER": "MCP@pve",
"PROXMOX_TOKEN_NAME": "MCP",
"PROXMOX_TOKEN_VALUE": "2052990e-749f-43f6-be7f-c9ad206281cc",
"PROXMOX_SSL_MODE": "insecure",
"PROXMOX_ALLOW_ELEVATED": "true",
"PROXMOX_SSH_ENABLED": "true",
"PROXMOX_SSH_HOST": "192.168.4.13",
"PROXMOX_SSH_PORT": "22",
"PROXMOX_SSH_USER": "root",
"PROXMOX_SSH_KEY_PATH": "/app/ssh_keys/nuc-cluster",
"PROXMOX_SSH_NODE": "nuck7-3",
"PROXMOX_ALLOW_UNSAFE_COMMANDS": "true"
}
},
"homeassistant": {
"command": "npx",
"args": [
"-y",
"mcp-remote",
"https://home.bryanmail.net/mcp_server/sse",
"--header",
"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiIxNzhmYzI0NjA2Y2I0ZTg4ODI1N2VmMzdiZTNhY2E5YSIsImlhdCI6MTc3NDY4MzAxNiwiZXhwIjoyMDkwMDQzMDE2fQ.32kY2LVHzKZHWLc96T6z2P-8beNTnp2DHRUf2UEie2w"
]
},
"teams": {
"command": "npx",
"args": [
"-y",
"@floriscornel/teams-mcp@latest"
],
"env": {
"TEAMS_MCP_READ_ONLY": "true",
"HOME": "/app/teams-mcp-auth"
}
},
"ms365": {
"command": "npx",
"args": [
"-y",
"@softeria/ms-365-mcp-server@0.129.0",
"--preset",
"personal",
"--read-only",
"--discovery"
],
"env": {
"HOME": "/app/teams-mcp-auth",
"MS365_MCP_CLIENT_ID": "14d82eec-204b-4c2f-b7e8-296a70dab67e",
"MS365_MCP_TOKEN_CACHE_PATH": "/app/teams-mcp-auth/.teams-mcp-token-cache.json",
"SILENT": "true"
}
},
"unifi-network": {
"command": "uvx",
"args": [
"unifi-network-mcp@latest"
],
"env": {
"UNIFI_HOST": "192.168.4.1",
"UNIFI_USERNAME": "unifi-mcp",
"UNIFI_PASSWORD": "3dHOEOMygTYeX3",
"UNIFI_PORT": "443",
"UNIFI_VERIFY_SSL": "false",
"UV_CACHE_DIR": "/app/uv-cache"
}
},
"authentik": {
"command": "uvx",
"args": [
"authentik-diag-mcp",
"--base-url",
"https://auth.bryanmail.net",
"--token",
"LAm3lBTumOmsU8AiFQM2FmCZyoj8bTSR0FQnAcBy1QnTMEyU4oWozwdxTUap"
]
},
"gitea": {
"command": "/mcpo_data/gitea-mcp",
"args": [
"-t",
"stdio"
],
"env": {
"GITEA_HOST": "https://git.bryanmail.net",
"GITEA_ACCESS_TOKEN": "5aa3a554c001b1dbe5215cb8cb388112801930e9"
}
},
"gitea-admin": {
"command": "/mcpo_data/gitea-mcp",
"args": [
"-t",
"stdio"
],
"env": {
"GITEA_HOST": "https://git.bryanmail.net",
"GITEA_ACCESS_TOKEN": "289225b0b8f1827242191874b2408db76af06321"
}
},
"powerautomate": {
"command": "npx",
"args": [
"-y",
"powerautomate-mcp@latest",
"--stdio"
],
"env": {
"HOME": "/app/powerautomate-auth",
"PA_MCP_CLIENT_ID": "84b431ed-ef5d-48a0-b0a1-878cfdb71453",
"PA_MCP_TENANT_ID": "0f6cf991-c449-480a-a71b-83003ce6edc1",
"PA_CONFIG_PATH": "/app/powerautomate-auth/config.json"
}
}
}
}
+7
View File
@@ -0,0 +1,7 @@
# Mealie - Pattern B env (folder-mode); DB/LDAP bind passwords are Docker secrets
POSTGRES_HOST=postgresql
POSTGRES_PORT=5432
LDAP_HOST=homehub.bryanmail.net
LDAP_BASE_DN=DC=bryanmail,DC=net
LDAP_QUERY_BIND=CN=AVB,OU=AAD Sync,OU=House Members,OU=Topside Pinnacle,DC=bryanmail,DC=net
LDAP_ADMIN_GROUP=OU=AAD Sync,OU=House Members,OU=Topside Pinnacle,DC=bryanmail,DC=net
-4
View File
@@ -2,8 +2,6 @@ version: "3.6"
services: services:
postgresql: postgresql:
stop_grace_period: 60s stop_grace_period: 60s
env_file:
- postgresql.env
command: command:
- postgres - postgres
environment: environment:
@@ -68,8 +66,6 @@ services:
pgadmin: pgadmin:
env_file:
- postgresql.env
image: dpage/pgadmin4:latest image: dpage/pgadmin4:latest
environment: environment:
PGADMIN_DEFAULT_EMAIL: ${PGADMIN_DEFAULT_EMAIL} PGADMIN_DEFAULT_EMAIL: ${PGADMIN_DEFAULT_EMAIL}
+102
View File
@@ -0,0 +1,102 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["config:base"],
"timezone": "America/Los_Angeles",
"schedule": ["after 2am and before 6am every weekday"],
"labels": ["renovate"],
"assignees": ["admin"],
"reviewers": ["admin"],
"prConcurrentLimit": 5,
"prHourlyLimit": 2,
"branchConcurrentLimit": 10,
"commitMessagePrefix": "chore(deps):",
"semanticCommits": "enabled",
"packageRules": [
{
"description": "Auto-merge patch updates for low-risk app stacks",
"matchFileNames": [
"mealie/**",
"vaultwarden/**",
"maintenance/**",
"meshcentral/**"
],
"matchUpdateTypes": ["patch"],
"automerge": true,
"automergeType": "pr",
"automergeStrategy": "merge"
},
{
"description": "Auto-merge minor+patch for media stack (Plex/Radarr/Sonarr update frequently)",
"matchFileNames": ["media/**"],
"matchUpdateTypes": ["minor", "patch"],
"automerge": true,
"automergeType": "pr",
"automergeStrategy": "merge"
},
{
"description": "NEVER auto-merge bootstrap/critical stacks - manual review required",
"matchFileNames": [
"traefik.yaml",
"traefik.env",
"auth.yaml",
"auth.env",
"postgresql.yaml",
"woodpecker.yaml",
"git.yaml"
],
"automerge": false,
"labels": ["renovate", "high-risk"],
"reviewers": ["admin"]
},
{
"description": "Never auto-merge major version bumps for anything",
"matchUpdateTypes": ["major"],
"automerge": false,
"labels": ["renovate", "major-update"]
},
{
"description": "Group all immich updates together (server+microservices must stay in sync)",
"matchFileNames": ["immich/**"],
"groupName": "immich",
"automerge": false,
"labels": ["renovate", "immich"]
},
{
"description": "Group all AI stack updates together",
"matchFileNames": ["ai.yaml"],
"groupName": "ai-stack",
"automerge": false,
"labels": ["renovate", "ai"]
},
{
"description": "Pin digest updates silently (SHA pinning) - auto-merge",
"matchUpdateTypes": ["digest"],
"automerge": true,
"automergeType": "pr"
}
],
"docker-compose": {
"fileMatch": [
"(^|/)docker-compose\\.ya?ml$",
"(^|/)[a-z0-9-]+\\.ya?ml$"
]
},
"woodpecker": {
"enabled": false
},
"ignorePaths": [
"deploy/**",
"secrets/**",
".woodpecker/**",
"*.env",
"*.env.example"
]
}
+7
View File
@@ -0,0 +1,7 @@
# renovate/renovate.env
# Copy to renovate.env on the host (not committed - contains token)
# The actual token is provisioned as a Woodpecker secret: renovate_token
# This file is only needed if running the stack manually.
GITEA_HOST=git.bryanmail.net
RENOVATE_TOKEN=<gitea-renovate-bot-token>
+22
View File
@@ -0,0 +1,22 @@
services:
renovate:
image: renovate/renovate:latest
# Renovate runs as a one-shot job triggered by Woodpecker cron.
# This service definition exists for reference and manual runs only.
# Normal execution path: .woodpecker/renovate.yml cron pipeline.
environment:
RENOVATE_TOKEN: ${RENOVATE_TOKEN}
RENOVATE_PLATFORM: gitea
RENOVATE_ENDPOINT: https://${GITEA_HOST}
RENOVATE_GIT_AUTHOR: "Renovate Bot <renovate-bot@bryanmail.net>"
RENOVATE_REPOSITORIES: "homelab/compose-files"
LOG_LEVEL: info
deploy:
replicas: 0
placement:
constraints:
- node.hostname == docker-2
restart_policy:
condition: none
networks: {}
+5
View File
@@ -0,0 +1,5 @@
# Security (CrowdSec) - Pattern B env (folder-mode)
CROWDSEC_PGDSN='postgres://CrowdSec:HGS9$$5xQvdVE6N7bu5pA@db:5432/crowdsec?sslmode=disable'
BOUNCER_KEY_TRAEFIK=cVkY/KexoYYrwmHndBaVURAWqVZDhv+XiAosVbkNWnI
CROWDSEC_API_KEY=rmZ3cfCWIi3KngeujSSRlpv38Q2gc2lxA0zt8SDc06M
CROWDSEC_PASSWORD=832b4311c5bbdc3240aa51796549c79eb45c1f47967eaf72d5127b6cd470173
+1 -1
View File
@@ -32,7 +32,7 @@ services:
memory: 512M memory: 512M
crowdsec-web-ui: crowdsec-web-ui:
image: ghcr.io/thebluffman85/crowdsec-web-ui:latest image: ghcr.io/theduffman85/crowdsec-web-ui:latest
environment: environment:
CROWDSEC_API_URL: http://crowdsec:8080 CROWDSEC_API_URL: http://crowdsec:8080
CROWDSEC_API_KEY: ${CROWDSEC_API_KEY} CROWDSEC_API_KEY: ${CROWDSEC_API_KEY}
+2
View File
@@ -0,0 +1,2 @@
KEEPALIVED_PASSWORD=gSbk8tjtBtrfk8BbHtdiYvKBVW3YJk
KEEPALIVED_VIRTUAL_IPS="#PYTHON2BASH:['192.168.4.30']"
+77
View File
@@ -20,6 +20,18 @@ services:
labels: [traefik.enable=false] labels: [traefik.enable=false]
resources: resources:
limits: {cpus: '1', memory: 128M} limits: {cpus: '1', memory: 128M}
# stop-first: host network + VRRP — two concurrent keepalived speakers with the
# same router-id would fight over the VIP. Never run old+new simultaneously.
update_config:
parallelism: 1
delay: 30s
failure_action: rollback
monitor: 15s
order: stop-first
rollback_config:
parallelism: 1
failure_action: pause
order: stop-first
keepalived-backup: keepalived-backup:
environment: environment:
@@ -41,6 +53,17 @@ services:
labels: [traefik.enable=false] labels: [traefik.enable=false]
resources: resources:
limits: {cpus: '1', memory: 128M} limits: {cpus: '1', memory: 128M}
# stop-first: host network + VRRP (see keepalived-master)
update_config:
parallelism: 1
delay: 30s
failure_action: rollback
monitor: 15s
order: stop-first
rollback_config:
parallelism: 1
failure_action: pause
order: stop-first
reverse-proxy: reverse-proxy:
image: public.ecr.aws/docker/library/traefik:latest image: public.ecr.aws/docker/library/traefik:latest
@@ -108,6 +131,20 @@ services:
- traefik.http.routers.traefik.middlewares=forwardAuth-authentik@file, crowdsec@file - traefik.http.routers.traefik.middlewares=forwardAuth-authentik@file, crowdsec@file
resources: resources:
limits: {cpus: '4', memory: 512M} limits: {cpus: '4', memory: 512M}
# stop-first: host-mode published ports (80/443/1883/5201/5431) — a start-first
# replacement task can never bind while the old task holds the ports, so
# start-first would deadlock/rollback every update. Global mode + parallelism 1
# means only one node is briefly down; keepalived VIP + remaining nodes cover it.
update_config:
parallelism: 1
delay: 30s
failure_action: rollback
monitor: 15s
order: stop-first
rollback_config:
parallelism: 1
failure_action: pause
order: stop-first
traefik-certs-dumper: traefik-certs-dumper:
image: ghcr.io/ldez/traefik-certs-dumper:v2.10.0 image: ghcr.io/ldez/traefik-certs-dumper:v2.10.0
@@ -119,6 +156,16 @@ services:
labels: [traefik.enable=false] labels: [traefik.enable=false]
resources: resources:
limits: {cpus: '1', memory: 64M} limits: {cpus: '1', memory: 64M}
update_config:
parallelism: 1
delay: 30s
failure_action: rollback
monitor: 15s
order: start-first
rollback_config:
parallelism: 1
failure_action: pause
order: start-first
speedtest: speedtest:
image: openspeedtest/latest image: openspeedtest/latest
@@ -142,6 +189,16 @@ services:
- autoheal=true - autoheal=true
resources: resources:
limits: {cpus: '8', memory: 1G} limits: {cpus: '8', memory: 1G}
update_config:
parallelism: 1
delay: 30s
failure_action: rollback
monitor: 15s
order: start-first
rollback_config:
parallelism: 1
failure_action: pause
order: start-first
iperf3-server: iperf3-server:
image: networkstatic/iperf3:latest image: networkstatic/iperf3:latest
@@ -164,6 +221,16 @@ services:
- autoheal=true - autoheal=true
resources: resources:
limits: {cpus: '4', memory: 128M} limits: {cpus: '4', memory: 128M}
update_config:
parallelism: 1
delay: 30s
failure_action: rollback
monitor: 15s
order: start-first
rollback_config:
parallelism: 1
failure_action: pause
order: start-first
whoami: whoami:
image: emilevauge/whoami image: emilevauge/whoami
@@ -181,6 +248,16 @@ services:
- traefik.http.routers.whoami.middlewares=crowdsec@file, forwardAuth-authentik@file - traefik.http.routers.whoami.middlewares=crowdsec@file, forwardAuth-authentik@file
resources: resources:
limits: {cpus: '1', memory: 16M} limits: {cpus: '1', memory: 16M}
update_config:
parallelism: 1
delay: 30s
failure_action: rollback
monitor: 15s
order: start-first
rollback_config:
parallelism: 1
failure_action: pause
order: start-first
networks: networks:
traefik_frontend: traefik_frontend:
+5
View File
@@ -0,0 +1,5 @@
# Vaultwarden - Pattern B env (folder-mode); ADMIN_TOKEN/DATABASE_URL are Docker secrets
# 2026-07: DATABASE_URL secret rotated to point at 'postgresql' alias instead of 'db'
# as part of PostgreSQL HA migration prep (ADR-0001).
# Retry #2: prior Woodpecker run replayed stale trigger metadata (empty CHANGED_FILES)
# after the Gitea private-repo auth fix. This is a fresh commit to force a clean trigger.
+26 -30
View File
@@ -1,37 +1,22 @@
version: '3.8'
services: services:
# Woodpecker Server - Central orchestration
woodpecker-server: woodpecker-server:
image: woodpeckerci/woodpecker-server:latest image: woodpeckerci/woodpecker-server:latest
ports: ports:
- "8000:8000" # HTTP - "8000:8000"
- "9000:9000" # gRPC (agents communicate here) - "9000:9000"
environment: environment:
# Server configuration
WOODPECKER_HOST: https://${WOODPECKER_HOST} WOODPECKER_HOST: https://${WOODPECKER_HOST}
WOODPECKER_PROTO: https WOODPECKER_PROTO: https
WOODPECKER_OPEN: "false" # Private instance WOODPECKER_OPEN: "false"
# Gitea integration
WOODPECKER_GITEA: "true" WOODPECKER_GITEA: "true"
WOODPECKER_GITEA_URL: https://${GITEA_HOST} WOODPECKER_GITEA_URL: https://${GITEA_HOST}
WOODPECKER_GITEA_CLIENT: ${WOODPECKER_GITEA_CLIENT} WOODPECKER_GITEA_CLIENT: ${WOODPECKER_GITEA_CLIENT}
WOODPECKER_GITEA_SECRET: ${WOODPECKER_GITEA_SECRET} WOODPECKER_GITEA_SECRET: ${WOODPECKER_GITEA_SECRET}
# Agent security
WOODPECKER_AGENT_SECRET: ${WOODPECKER_AGENT_SECRET} WOODPECKER_AGENT_SECRET: ${WOODPECKER_AGENT_SECRET}
# Allow organizations
WOODPECKER_ORGS: homelab,admin WOODPECKER_ORGS: homelab,admin
# Logging
WOODPECKER_LOG_LEVEL: info WOODPECKER_LOG_LEVEL: info
# Database (using existing PostgreSQL)
WOODPECKER_DATABASE_DRIVER: postgres WOODPECKER_DATABASE_DRIVER: postgres
WOODPECKER_DATABASE_DATASOURCE: postgres://${DB_USER}:${DB_PASS}@${POSTGRES_HOST}:${POSTGRES_PORT}/woodpecker?sslmode=disable WOODPECKER_DATABASE_DATASOURCE: postgres://${DB_USER}:${DB_PASS}@${POSTGRES_HOST}:${POSTGRES_PORT}/woodpecker?sslmode=disable
volumes: volumes:
- /var/run/docker.sock:/var/run/docker.sock - /var/run/docker.sock:/var/run/docker.sock
- /etc/timezone:/etc/timezone:ro - /etc/timezone:/etc/timezone:ro
@@ -43,7 +28,7 @@ services:
replicas: 1 replicas: 1
placement: placement:
constraints: constraints:
- node.hostname == docker-2 # Run on manager node only - node.hostname == docker-2
labels: labels:
- traefik.enable=true - traefik.enable=true
- traefik.http.routers.woodpecker.entrypoints=websecure - traefik.http.routers.woodpecker.entrypoints=websecure
@@ -52,23 +37,26 @@ services:
- traefik.http.routers.woodpecker.tls.certresolver=letsencrypt - traefik.http.routers.woodpecker.tls.certresolver=letsencrypt
- traefik.docker.network=traefik_backend - traefik.docker.network=traefik_backend
- traefik.http.services.woodpecker.loadbalancer.server.port=8000 - traefik.http.services.woodpecker.loadbalancer.server.port=8000
update_config:
parallelism: 1
delay: 15s
failure_action: rollback
monitor: 30s
order: start-first
rollback_config:
parallelism: 1
failure_action: pause
order: start-first
# Woodpecker Agents - Pipeline execution
woodpecker-agent: woodpecker-agent:
image: woodpeckerci/woodpecker-agent:latest image: woodpeckerci/woodpecker-agent:latest
command: agent command: agent
environment: environment:
# Server connection
WOODPECKER_SERVER: woodpecker-server:9000 WOODPECKER_SERVER: woodpecker-server:9000
WOODPECKER_AGENT_SECRET: ${WOODPECKER_AGENT_SECRET} WOODPECKER_AGENT_SECRET: ${WOODPECKER_AGENT_SECRET}
WOODPECKER_MAX_PROCS: 4
# Agent configuration
WOODPECKER_MAX_PROCS: 4 # Max parallel builds per agent
WOODPECKER_LOG_LEVEL: info WOODPECKER_LOG_LEVEL: info
# Labels for scheduling
WOODPECKER_FILTER_LABELS: "true" WOODPECKER_FILTER_LABELS: "true"
volumes: volumes:
- /var/run/docker.sock:/var/run/docker.sock - /var/run/docker.sock:/var/run/docker.sock
- /etc/timezone:/etc/timezone:ro - /etc/timezone:/etc/timezone:ro
@@ -76,9 +64,17 @@ services:
networks: networks:
- traefik_backend - traefik_backend
deploy: deploy:
mode: global # One agent per node mode: global
depends_on: update_config:
- woodpecker-server parallelism: 1
delay: 30s
failure_action: rollback
monitor: 15s
order: start-first
rollback_config:
parallelism: 1
failure_action: pause
order: start-first
networks: networks:
traefik_backend: traefik_backend: