# 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: ```json { "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` ```env WEBHOOK_SECRET= LISTEN_PORT=50500 ``` The `WEBHOOK_SECRET` value must match the secret configured in each Gitea/Forgejo webhook. ### Request Validation & Behavior 1. **HMAC-SHA256 validation**: The listener reads the `X-Gitea-Signature` or `X-Forgejo-Signature` header and validates the request body against the `WEBHOOK_SECRET` using HMAC-SHA256. Requests that fail validation are rejected. 2. **Branch filter**: Only pushes to `main` or `master` (checked via the `ref` field) trigger a deploy. All other branches are ignored. 3. **Deploy map lookup**: The `repository.full_name` field (e.g., `darren/my-app`) is looked up in the deploy map. If not found, the request is ignored. 4. **Deploy sequence**: On a valid push, the listener executes: ```bash cd /srv/git/my-app git pull docker compose pull docker compose up -d ``` 5. **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: ```bash curl https://deploy.lavender.spl.tech/health ``` A successful response confirms the listener is reachable through Caddy and functioning. ### Systemd Management ```bash # 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 `proxy` network 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): ```bash 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): ```bash 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 gzip` for compression. - Use the container name (not IP) in the `reverse_proxy` directive — 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": "" }, "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 ```bash 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: ```json { "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., `myapp` for `myapp.lavender-daydream.com`, or the full FQDN). - **`content`**: Always `155.94.170.136` (mew's public IP). - **`ttl`**: `1` means automatic TTL. - **`proxied`**: Set to `false` so Caddy handles TLS directly. Setting to `true` would 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 the `lavender-daydream.com` zone ID - `*.spl.tech` → use the `spl.tech` zone 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 `proxy` network. - Caddy resolves container names to IPs on this network — use container names (not IPs) in `reverse_proxy` directives. - The network is created externally (not by any single compose file). If it does not exist, create it: ```bash 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: ```yaml networks: proxy: name: proxy external: true ``` And each service that Caddy proxies to must list `proxy` in its `networks` key: ```yaml services: app: # ... networks: - proxy ``` If the stack also has internal-only services (e.g., a database), create an additional internal network: ```yaml 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 `git` group, so both `root` and members of the `git` group (including `deploy` and `darren`) 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 |