Compare commits
54
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0577496bb6 | ||
|
|
c609c3b45a | ||
|
|
00f9fa352c | ||
|
|
37aca1de0a | ||
|
|
cebc6a0a8f | ||
|
|
7dbc6f490e | ||
|
|
03b32ee449 | ||
|
|
aef4be5a94 | ||
|
|
92b01d915c | ||
|
|
07a81f96aa | ||
|
|
07635d1722 | ||
|
|
b4fed87f24 | ||
|
|
e06d300b3c | ||
|
|
39c34c8923 | ||
|
|
0dc7e915f3 | ||
|
|
1ad8e41799 | ||
|
|
0ef9c8d6e5 | ||
|
|
b6c3fcdd58 | ||
|
|
9b6322208c | ||
|
|
26dc2b50bc | ||
|
|
81ab653402 | ||
|
|
d12ac4daa9 | ||
|
|
8f00be333e | ||
|
|
7fcc8a9ca5 | ||
|
|
c79ade9914 | ||
|
|
c38e41c741 | ||
|
|
712516ac29 | ||
|
|
bf13c74c1d | ||
|
|
d456c96454 | ||
|
|
ab20c708a0 | ||
|
|
835e534ec1 | ||
|
|
a2b52c5ba1 | ||
|
|
9a18cda853 | ||
|
|
0f3901152a | ||
|
|
afdd23184d | ||
|
|
6f65326890 | ||
|
|
f212fa07b7 | ||
|
|
ae605838c3 | ||
|
|
2c8db4118d | ||
|
|
e1a7fb59e3 | ||
|
|
45b0d7109e | ||
|
|
1659b5b861 | ||
|
|
d7727fd788 | ||
|
|
40fb1329c3 | ||
|
|
308ac008ba | ||
|
|
b6bf14f917 | ||
|
|
3a5483cf53 | ||
|
|
5454f7acf4 | ||
|
|
48d20d613f | ||
|
|
cf82e36380 | ||
|
|
b25429091f | ||
|
|
5f0459eb35 | ||
|
|
c3c05b7164 | ||
|
|
97cdb74506 |
@@ -1,8 +1,6 @@
|
||||
when:
|
||||
- event: push
|
||||
branch: main
|
||||
- event: manual
|
||||
branch: main
|
||||
|
||||
steps:
|
||||
validate:
|
||||
@@ -217,7 +215,7 @@ steps:
|
||||
|
||||
for STACK in $ALL_STACKS; do
|
||||
echo "--- Deploying: $STACK ---"
|
||||
# Sync flat file or entire folder
|
||||
# Sync files to host first (always, even for bootstrap stacks)
|
||||
if [ -d "$STACK" ]; then
|
||||
rsync -av -e "ssh -o StrictHostKeyChecking=no -i ~/.ssh/id_rsa" \
|
||||
"$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" \
|
||||
"${STACK}.yaml" root@${SWARM_MANAGER_IP}:/volume1/docker/compose-files/
|
||||
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} \
|
||||
"bash /volume1/docker/compose-files/deploy/stack-deploy.sh $STACK" \
|
||||
&& 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)
|
||||
ALL_STACKS=$(printf '%s\n%s' "$FLAT_STACKS" "$FOLDER_STACKS" | grep -v '^$' | sort -u)
|
||||
[ -z "$ALL_STACKS" ] && exit 0
|
||||
|
||||
# Give Swarm time to schedule tasks after deploy
|
||||
echo "Waiting 15s for Swarm to schedule tasks..."
|
||||
sleep 15
|
||||
|
||||
FAILED=0
|
||||
sleep 5
|
||||
for STACK in $ALL_STACKS; do
|
||||
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 \
|
||||
--format ' {{.Name}} {{.CurrentState}}'" 2>&1)
|
||||
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
|
||||
--format ' {{.Name}} {{.CurrentState}}'"
|
||||
done
|
||||
|
||||
[ "$FAILED" -eq 0 ] || exit 1
|
||||
|
||||
notify-success:
|
||||
image: alpine:latest
|
||||
environment:
|
||||
@@ -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
@@ -56,8 +56,8 @@ services:
|
||||
- traefik.http.services.open-webui.loadbalancer.sticky.cookie.secure=true
|
||||
- traefik.http.services.open-webui.loadbalancer.sticky.cookie.httponly=true
|
||||
- traefik.swarm.network=traefik_backend
|
||||
litellm: #
|
||||
image: docker.litellm.ai/berriai/litellm-database:v1.90.3 #main-latest
|
||||
litellm:
|
||||
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
|
||||
environment:
|
||||
- AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID}
|
||||
@@ -68,14 +68,10 @@ services:
|
||||
- DATABASE_URL=${DATABASE_URL}
|
||||
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
|
||||
- LITELLM_MODIFY_PARAMS=${LITELLM_MODIFY_PARAMS}
|
||||
- LITELLM_DATABASE_MIGRATIONS=${LITELLM_DATABASE_MIGRATIONS}
|
||||
networks:
|
||||
- traefik_backend
|
||||
- postgresql_db-backend
|
||||
# ports:
|
||||
# - target: 4000
|
||||
# published: 4000
|
||||
# protocol: tcp
|
||||
# mode: ingress
|
||||
volumes:
|
||||
- type: bind
|
||||
source: /volume1/docker/litellm/app/config.yaml
|
||||
@@ -119,26 +115,17 @@ services:
|
||||
- ${MCPO_API_KEY}
|
||||
- --hot-reload
|
||||
environment:
|
||||
- PROXMOX_ALLOW_UNSAFE_COMMANDS=false #true
|
||||
- PROXMOX_ALLOW_UNSAFE_COMMANDS=false
|
||||
volumes:
|
||||
- /volume1/docker/mcpo/config-critical.json:/app/config/config.json:ro
|
||||
- /volume1/docker/mcpo/data:/mcpo_data
|
||||
- /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
|
||||
networks:
|
||||
- traefik_backend
|
||||
deploy:
|
||||
labels:
|
||||
- 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:
|
||||
image: ghcr.io/open-webui/mcpo:main
|
||||
command:
|
||||
@@ -153,28 +140,17 @@ services:
|
||||
- /volume1/docker/mcpo/config.json:/app/config/config.json:ro
|
||||
- /volume1/docker/mcpo/data:/mcpo_data
|
||||
- /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
|
||||
networks:
|
||||
- traefik_backend
|
||||
deploy:
|
||||
labels:
|
||||
- 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:
|
||||
image: docker.n8n.io/n8nio/n8n
|
||||
networks:
|
||||
- traefik_backend
|
||||
- postgresql_db-backend
|
||||
# ports:
|
||||
# - 5678:5678
|
||||
volumes:
|
||||
- /volume1/docker/n8n/data:/home/node/.n8n
|
||||
- /volume1/docker/n8n/files:/files
|
||||
@@ -188,7 +164,7 @@ services:
|
||||
- traefik.http.services.n8n.loadbalancer.server.port=5678
|
||||
- traefik.http.services.n8n.loadbalancer.server.scheme=http
|
||||
- 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.contentTypeNosniff=true
|
||||
- traefik.http.middlewares.n8n.headers.forceSTSHeader=true
|
||||
@@ -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())
|
||||
@@ -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
|
||||
@@ -0,0 +1,2 @@
|
||||
# Home Assistant - Pattern B env (folder-mode)
|
||||
FRIGATE_RTSP_PASSWORD=v6@PLPA36H7jqn
|
||||
@@ -0,0 +1 @@
|
||||
# Maintenance (Cronicle + Uptime Kuma) - Pattern B env (folder-mode)
|
||||
@@ -66,6 +66,8 @@ services:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
deploy:
|
||||
restart_policy:
|
||||
delay: 30s
|
||||
labels:
|
||||
- traefik.enable=true
|
||||
- traefik.http.routers.uptime.rule=Host(`uptime.${DOMAIN}`)
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -2,8 +2,6 @@ version: "3.6"
|
||||
services:
|
||||
postgresql:
|
||||
stop_grace_period: 60s
|
||||
env_file:
|
||||
- postgresql.env
|
||||
command:
|
||||
- postgres
|
||||
environment:
|
||||
@@ -68,8 +66,6 @@ services:
|
||||
|
||||
|
||||
pgadmin:
|
||||
env_file:
|
||||
- postgresql.env
|
||||
image: dpage/pgadmin4:latest
|
||||
environment:
|
||||
PGADMIN_DEFAULT_EMAIL: ${PGADMIN_DEFAULT_EMAIL}
|
||||
|
||||
+102
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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: {}
|
||||
@@ -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
|
||||
@@ -32,7 +32,7 @@ services:
|
||||
memory: 512M
|
||||
|
||||
crowdsec-web-ui:
|
||||
image: ghcr.io/thebluffman85/crowdsec-web-ui:latest
|
||||
image: ghcr.io/theduffman85/crowdsec-web-ui:latest
|
||||
environment:
|
||||
CROWDSEC_API_URL: http://crowdsec:8080
|
||||
CROWDSEC_API_KEY: ${CROWDSEC_API_KEY}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
KEEPALIVED_PASSWORD=gSbk8tjtBtrfk8BbHtdiYvKBVW3YJk
|
||||
KEEPALIVED_VIRTUAL_IPS="#PYTHON2BASH:['192.168.4.30']"
|
||||
@@ -20,6 +20,18 @@ services:
|
||||
labels: [traefik.enable=false]
|
||||
resources:
|
||||
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:
|
||||
environment:
|
||||
@@ -41,6 +53,17 @@ services:
|
||||
labels: [traefik.enable=false]
|
||||
resources:
|
||||
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:
|
||||
image: public.ecr.aws/docker/library/traefik:latest
|
||||
@@ -108,6 +131,20 @@ services:
|
||||
- traefik.http.routers.traefik.middlewares=forwardAuth-authentik@file, crowdsec@file
|
||||
resources:
|
||||
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:
|
||||
image: ghcr.io/ldez/traefik-certs-dumper:v2.10.0
|
||||
@@ -119,6 +156,16 @@ services:
|
||||
labels: [traefik.enable=false]
|
||||
resources:
|
||||
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:
|
||||
image: openspeedtest/latest
|
||||
@@ -142,6 +189,16 @@ services:
|
||||
- autoheal=true
|
||||
resources:
|
||||
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:
|
||||
image: networkstatic/iperf3:latest
|
||||
@@ -164,6 +221,16 @@ services:
|
||||
- autoheal=true
|
||||
resources:
|
||||
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:
|
||||
image: emilevauge/whoami
|
||||
@@ -181,6 +248,16 @@ services:
|
||||
- traefik.http.routers.whoami.middlewares=crowdsec@file, forwardAuth-authentik@file
|
||||
resources:
|
||||
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:
|
||||
traefik_frontend:
|
||||
@@ -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
@@ -1,37 +1,22 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
# Woodpecker Server - Central orchestration
|
||||
woodpecker-server:
|
||||
image: woodpeckerci/woodpecker-server:latest
|
||||
ports:
|
||||
- "8000:8000" # HTTP
|
||||
- "9000:9000" # gRPC (agents communicate here)
|
||||
- "8000:8000"
|
||||
- "9000:9000"
|
||||
environment:
|
||||
# Server configuration
|
||||
WOODPECKER_HOST: https://${WOODPECKER_HOST}
|
||||
WOODPECKER_PROTO: https
|
||||
WOODPECKER_OPEN: "false" # Private instance
|
||||
|
||||
# Gitea integration
|
||||
WOODPECKER_OPEN: "false"
|
||||
WOODPECKER_GITEA: "true"
|
||||
WOODPECKER_GITEA_URL: https://${GITEA_HOST}
|
||||
WOODPECKER_GITEA_CLIENT: ${WOODPECKER_GITEA_CLIENT}
|
||||
WOODPECKER_GITEA_SECRET: ${WOODPECKER_GITEA_SECRET}
|
||||
|
||||
# Agent security
|
||||
WOODPECKER_AGENT_SECRET: ${WOODPECKER_AGENT_SECRET}
|
||||
|
||||
# Allow organizations
|
||||
WOODPECKER_ORGS: homelab,admin
|
||||
|
||||
# Logging
|
||||
WOODPECKER_LOG_LEVEL: info
|
||||
|
||||
# Database (using existing PostgreSQL)
|
||||
WOODPECKER_DATABASE_DRIVER: postgres
|
||||
WOODPECKER_DATABASE_DATASOURCE: postgres://${DB_USER}:${DB_PASS}@${POSTGRES_HOST}:${POSTGRES_PORT}/woodpecker?sslmode=disable
|
||||
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- /etc/timezone:/etc/timezone:ro
|
||||
@@ -43,7 +28,7 @@ services:
|
||||
replicas: 1
|
||||
placement:
|
||||
constraints:
|
||||
- node.hostname == docker-2 # Run on manager node only
|
||||
- node.hostname == docker-2
|
||||
labels:
|
||||
- traefik.enable=true
|
||||
- traefik.http.routers.woodpecker.entrypoints=websecure
|
||||
@@ -52,23 +37,26 @@ services:
|
||||
- traefik.http.routers.woodpecker.tls.certresolver=letsencrypt
|
||||
- traefik.docker.network=traefik_backend
|
||||
- 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:
|
||||
image: woodpeckerci/woodpecker-agent:latest
|
||||
command: agent
|
||||
environment:
|
||||
# Server connection
|
||||
WOODPECKER_SERVER: woodpecker-server:9000
|
||||
WOODPECKER_AGENT_SECRET: ${WOODPECKER_AGENT_SECRET}
|
||||
|
||||
# Agent configuration
|
||||
WOODPECKER_MAX_PROCS: 4 # Max parallel builds per agent
|
||||
WOODPECKER_MAX_PROCS: 4
|
||||
WOODPECKER_LOG_LEVEL: info
|
||||
|
||||
# Labels for scheduling
|
||||
WOODPECKER_FILTER_LABELS: "true"
|
||||
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- /etc/timezone:/etc/timezone:ro
|
||||
@@ -76,9 +64,17 @@ services:
|
||||
networks:
|
||||
- traefik_backend
|
||||
deploy:
|
||||
mode: global # One agent per node
|
||||
depends_on:
|
||||
- woodpecker-server
|
||||
mode: global
|
||||
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:
|
||||
traefik_backend:
|
||||
|
||||
Reference in New Issue
Block a user