Initial commit: _deploy_app skill
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
This commit is contained in:
431
references/infrastructure.md
Normal file
431
references/infrastructure.md
Normal file
@@ -0,0 +1,431 @@
|
||||
# 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=<the-shared-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": "<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
|
||||
|
||||
```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 |
|
||||
Reference in New Issue
Block a user