Deploy new apps or push updates to existing deployments via Docker Compose + Caddy + Gitea webhooks. Multi-server profiles, auto-detection of deployment status, full infrastructure provisioning. - SKILL.md: 715-line workflow documentation - scripts/detect_deployment.py: deployment status detection - scripts/validate_compose.py: compose file validation - references/: infrastructure, compose patterns, Caddy patterns - assets/: Makefile and compose templates - config.json: mew server profile
11 KiB
Infrastructure Reference — mew Server (155.94.170.136)
This document describes every infrastructure component on the mew server relevant to deploying Docker Compose applications behind Caddy with automated Gitea-triggered deployments.
1. Deploy Listener
Overview
A Python webhook listener that receives push events from Gitea/Forgejo and automatically deploys the corresponding Docker Compose stack.
Filesystem Locations
| Item | Path |
|---|---|
| Script | /usr/local/bin/deploy-listener.py |
| Systemd unit | deploy-listener.service |
| Deploy map | /etc/deploy-listener/deploy-map.json |
| Environment file | /etc/deploy-listener/deploy-listener.env |
| Service user home | /var/lib/deploy |
Service User
- User:
deploy - Groups:
docker,git - Home directory:
/var/lib/deploy
The deploy user has Docker socket access through the docker group and repository access through the git group.
Network Binding
- Port: 50500
- Bind address: 0.0.0.0
- Firewall: UFW blocks external access to port 50500. Only Docker's internal 10.0.0.0/8 range is allowed. Caddy reaches the listener at
10.0.12.1:50500(the proxy network gateway).
Deploy Map
Location: /etc/deploy-listener/deploy-map.json
Format — a JSON object mapping owner/repo to the absolute path of the compose directory:
{
"darren/compose-bookstack": "/srv/git/compose-bookstack",
"darren/compose-linkstack": "/srv/git/compose-linkstack",
"darren/my-app": "/srv/git/my-app"
}
Add a new entry to this file for every application that should be auto-deployed on push.
Environment File
Location: /etc/deploy-listener/deploy-listener.env
WEBHOOK_SECRET=<the-shared-secret>
LISTEN_PORT=50500
The WEBHOOK_SECRET value must match the secret configured in each Gitea/Forgejo webhook.
Request Validation & Behavior
- HMAC-SHA256 validation: The listener reads the
X-Gitea-SignatureorX-Forgejo-Signatureheader and validates the request body against theWEBHOOK_SECRETusing HMAC-SHA256. Requests that fail validation are rejected. - Branch filter: Only pushes to
mainormaster(checked via thereffield) trigger a deploy. All other branches are ignored. - Deploy map lookup: The
repository.full_namefield (e.g.,darren/my-app) is looked up in the deploy map. If not found, the request is ignored. - Deploy sequence: On a valid push, the listener executes:
cd /srv/git/my-app git pull docker compose pull docker compose up -d - Concurrency control: A file lock prevents concurrent deploys. If a deploy is already running, the incoming request is queued or rejected.
Health Check
Verify the listener is running:
curl https://deploy.lavender.spl.tech/health
A successful response confirms the listener is reachable through Caddy and functioning.
Systemd Management
# Check status
sudo systemctl status deploy-listener
# Restart
sudo systemctl restart deploy-listener
# View logs
sudo journalctl -u deploy-listener -f
2. Caddy Reverse Proxy
Overview
Caddy serves as the TLS-terminating reverse proxy for all applications on mew. It automatically provisions and renews certificates via Let's Encrypt.
Filesystem Locations
| Item | Path |
|---|---|
| Caddyfile | /data/docker/caddy/Caddyfile |
| Compose file | /data/docker/caddy/docker-compose.yaml |
| Container name | caddy |
| Image | caddy:2-alpine |
Network
- Network name:
proxy - Type: external Docker network
- Subnet: 10.0.12.0/24
- Gateway: 10.0.12.1
- All application containers MUST join the
proxynetwork for Caddy to reach them by container name.
TLS
- Method: Automatic via Let's Encrypt
- Email:
postmaster@lavender-daydream.com - No manual certificate management required. Caddy handles provisioning, renewal, and OCSP stapling automatically.
Deploy Endpoint
The deploy listener is exposed externally through Caddy:
deploy.lavender.spl.tech → 10.0.12.1:50500
This routes through the proxy network gateway to the host-bound deploy listener.
Reloading the Caddyfile
Standard reload (when Caddyfile content changed but inode is the same):
docker exec caddy caddy reload --config /etc/caddy/Caddyfile
Full restart (required when the Caddyfile inode changed, e.g., after replacing the file rather than editing in-place):
cd /data/docker/caddy && docker compose restart caddy
Always check whether the file was edited in-place or replaced. If replaced, you MUST restart rather than reload.
Site Block Format
Follow this exact format when adding new site blocks to the Caddyfile:
# === App Name ===
domain.example.com {
encode zstd gzip
reverse_proxy container_name:port
}
- Place the comment header (
# === App Name ===) above each block for readability. - Always include
encode zstd gzipfor compression. - Use the container name (not IP) in the
reverse_proxydirective — Caddy resolves container names on the proxy network.
3. Gitea API
Connection Details
| Item | Value |
|---|---|
| Internal URL (from mew host) | http://10.0.12.5:3000 |
| External URL | https://git.lavender-daydream.com |
| API base path | /api/v1 |
| Token location | ~/.claude/secrets/gitea.json |
Authentication
Include the token as a header on every API request:
Authorization: token {GITEA_TOKEN}
Key Endpoints
Check if a repo exists
GET /api/v1/repos/{owner}/{repo}
- 200: Repo exists (response includes repo details).
- 404: Repo does not exist.
Create a new repo
POST /api/v1/user/repos
Content-Type: application/json
{
"name": "my-app",
"private": false,
"auto_init": false
}
Set auto_init to false when pushing an existing local repo. Set to true if you want Gitea to create an initial commit.
Add a webhook
POST /api/v1/repos/{owner}/{repo}/hooks
Content-Type: application/json
{
"type": "gitea",
"active": true,
"branch_filter": "main master",
"config": {
"url": "https://deploy.lavender.spl.tech/webhook",
"content_type": "json",
"secret": "<WEBHOOK_SECRET>"
},
"events": ["push"]
}
The secret in the webhook config MUST match the WEBHOOK_SECRET in /etc/deploy-listener/deploy-listener.env.
List repos
GET /api/v1/repos/search?limit=50
Returns up to 50 repositories. Use page parameter for pagination.
4. Forgejo API
Connection Details
| Item | Value |
|---|---|
| Container name | forgejo |
| Internal port | 3000 |
| External URL | https://forgejo.lavender-daydream.com |
| SSH port | 2223 |
API Compatibility
Forgejo is a fork of Gitea. The API format, endpoints, authentication, and request/response structures are identical to those documented in the Gitea section above. Use the same patterns — just substitute the Forgejo base URL.
SSH Access
git remote add forgejo ssh://git@forgejo.lavender-daydream.com:2223/owner/repo.git
5. Cloudflare DNS
Token & Zone Configuration
Location: ~/.claude/secrets/cloudflare.json
Format:
{
"CLOUDFLARE_API_TOKEN": "your-api-token-here",
"zones": {
"lavender-daydream.com": "zone_id_for_lavender_daydream",
"spl.tech": "zone_id_for_spl_tech"
}
}
Authentication
Include the token as a Bearer header:
Authorization: Bearer {CLOUDFLARE_API_TOKEN}
Create an A Record
POST https://api.cloudflare.com/client/v4/zones/{zone_id}/dns_records
Content-Type: application/json
{
"type": "A",
"name": "{subdomain}",
"content": "155.94.170.136",
"ttl": 1,
"proxied": false
}
name: The subdomain portion (e.g.,myappformyapp.lavender-daydream.com, or the full FQDN).content: Always155.94.170.136(mew's public IP).ttl:1means automatic TTL.proxied: Set tofalseso Caddy handles TLS directly. Setting totruewould route through Cloudflare's proxy and interfere with Let's Encrypt.
Choosing the Zone
Pick the zone based on the desired domain suffix:
*.lavender-daydream.com→ use thelavender-daydream.comzone ID*.spl.tech→ use thespl.techzone ID
6. Docker Networking
The proxy Network
| Property | Value |
|---|---|
| Name | proxy |
| Subnet | 10.0.12.0/24 |
| Gateway | 10.0.12.1 |
| Type | External (created once, referenced by all stacks) |
Requirements
- Every application container that Caddy must reach MUST join the
proxynetwork. - Caddy resolves container names to IPs on this network — use container names (not IPs) in
reverse_proxydirectives. - The network is created externally (not by any single compose file). If it does not exist, create it:
docker network create --subnet=10.0.12.0/24 --gateway=10.0.12.1 proxy
Compose Configuration
Every compose file that needs Caddy access must include:
networks:
proxy:
name: proxy
external: true
And each service that Caddy proxies to must list proxy in its networks key:
services:
app:
# ...
networks:
- proxy
If the stack also has internal-only services (e.g., a database), create an additional internal network:
networks:
proxy:
name: proxy
external: true
internal:
driver: bridge
7. Compose Stack Locations
Core Infrastructure Stacks
Location: /data/docker/
These are foundational services that support the entire server:
| Directory | Service |
|---|---|
/data/docker/caddy/ |
Caddy reverse proxy |
/data/docker/gitea/ |
Gitea git forge |
/data/docker/forgejo/ |
Forgejo git forge |
/data/docker/email/ |
Email services |
/data/docker/website/ |
Main website |
/data/docker/linkstack-berlyn/ |
Berlyn's linkstack |
Application Stacks
Location: /srv/git/
These are deployed applications managed by the deploy listener:
| Directory | Application |
|---|---|
/srv/git/compose-bookstack/ |
BookStack wiki |
/srv/git/compose-linkstack/ |
LinkStack |
/srv/git/compose-portainer/ |
Portainer |
/srv/git/compose-wishthis/ |
WishThis |
/srv/git/compose-anythingllm/ |
AnythingLLM |
Ownership & Permissions
- Owner:
root:git - Permissions:
2775(setgid) - The setgid bit ensures new files and directories inherit the
gitgroup, so bothrootand members of thegitgroup (includingdeployanddarren) can read/write.
Standard Stack Contents
Each compose stack directory should contain:
| File | Purpose |
|---|---|
docker-compose.yaml |
Service definitions |
.env |
Environment variables (secrets, config) |
Makefile |
Convenience targets (make up, make down, make logs) |
README.md |
Stack documentation |