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:
Darren Neese
2026-03-25 21:12:30 -04:00
commit 994332a3f0
11 changed files with 3006 additions and 0 deletions

View File

@@ -0,0 +1,351 @@
# Docker Compose Patterns Reference
Reusable `docker-compose.yaml` templates for common application types deployed on mew. Every template includes the external `proxy` network required for Caddy reverse proxying.
---
## 1. Node.js / Express with Dockerfile Build
Build a Node.js app from a local Dockerfile. The container exposes an internal port that Caddy proxies to.
```yaml
version: "3.8"
services:
app:
build:
context: .
dockerfile: Dockerfile
container_name: myapp
restart: unless-stopped
expose:
- "3000"
environment:
- NODE_ENV=production
- PORT=3000
env_file:
- .env
networks:
- proxy
networks:
proxy:
name: proxy
external: true
```
### Companion Dockerfile
```dockerfile
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]
```
### Notes
- Use `expose` (not `ports`) to keep the port internal to Docker networks only.
- Set `container_name` to a unique, descriptive name — Caddy uses this name in its `reverse_proxy` directive.
- The app listens on port 3000 inside the container. Caddy reaches it via `myapp:3000`.
---
## 2. Python / FastAPI with Dockerfile Build
Build a Python FastAPI app from a local Dockerfile. Uses Uvicorn as the ASGI server.
```yaml
version: "3.8"
services:
app:
build:
context: .
dockerfile: Dockerfile
container_name: myapi
restart: unless-stopped
expose:
- "8000"
environment:
- PYTHONUNBUFFERED=1
env_file:
- .env
networks:
- proxy
networks:
proxy:
name: proxy
external: true
```
### Companion Dockerfile
```dockerfile
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
```
### Notes
- `PYTHONUNBUFFERED=1` ensures log output appears immediately in `docker compose logs`.
- For production, consider adding `--workers 4` to the Uvicorn command or switching to Gunicorn with Uvicorn workers.
- Caddy reaches this via `myapi:8000`.
---
## 3. Static Site (nginx)
Serve pre-built static files (HTML, CSS, JS) via nginx.
```yaml
version: "3.8"
services:
app:
image: nginx:alpine
container_name: mysite
restart: unless-stopped
expose:
- "80"
volumes:
- ./dist:/usr/share/nginx/html:ro
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
networks:
- proxy
networks:
proxy:
name: proxy
external: true
```
### Companion nginx.conf
```nginx
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
# Cache static assets
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2?)$ {
expires 30d;
add_header Cache-Control "public, immutable";
}
}
```
### Notes
- Mount the build output directory (e.g., `./dist`) into the nginx html root.
- The `try_files` fallback to `/index.html` supports client-side routing (React Router, Vue Router, etc.).
- Mount the nginx config as read-only (`:ro`).
- Caddy reaches this via `mysite:80`.
---
## 4. Pre-built Image Only
Pull and run a published Docker image with no local build. Suitable for off-the-shelf applications like wikis, dashboards, and link pages.
```yaml
version: "3.8"
services:
app:
image: lscr.io/linuxserver/bookstack:latest
container_name: bookstack
restart: unless-stopped
expose:
- "6875"
env_file:
- .env
volumes:
- ./data:/config
networks:
- proxy
networks:
proxy:
name: proxy
external: true
```
### Notes
- Replace the `image` and `expose` port with whatever the application requires.
- Check the image documentation for required environment variables and volume mount paths.
- Persist application data by mounting a local `./data` directory.
- Caddy reaches this via `bookstack:6875`.
---
## 5. App with PostgreSQL Database
A two-service stack with an application and a PostgreSQL database. The database is on an internal-only network. The app joins both the internal and proxy networks.
```yaml
version: "3.8"
services:
app:
build:
context: .
dockerfile: Dockerfile
container_name: myapp
restart: unless-stopped
expose:
- "3000"
env_file:
- .env
depends_on:
db:
condition: service_healthy
networks:
- proxy
- internal
db:
image: postgres:16-alpine
container_name: myapp-db
restart: unless-stopped
environment:
POSTGRES_DB: ${POSTGRES_DB:-myapp}
POSTGRES_USER: ${POSTGRES_USER:-myapp}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD in .env}
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-myapp}"]
interval: 10s
timeout: 5s
retries: 5
networks:
- internal
volumes:
pgdata:
networks:
proxy:
name: proxy
external: true
internal:
driver: bridge
```
### Notes
- The database is **only** on the `internal` network — it is not reachable from Caddy or any other container outside this stack.
- The app is on **both** `proxy` (so Caddy can reach it) and `internal` (so it can reach the database).
- `depends_on` with `condition: service_healthy` ensures the app waits for PostgreSQL to be ready before starting.
- The `${POSTGRES_PASSWORD:?...}` syntax causes compose to fail with an error if the variable is not set, preventing accidental deploys with no database password.
- Use a named volume (`pgdata`) for database persistence.
- In the app's `.env`, set the database URL:
```
DATABASE_URL=postgresql://myapp:secretpassword@myapp-db:5432/myapp
```
Note the hostname is the database container name (`myapp-db`), not `localhost`.
---
## 6. App with Environment File
Pattern for managing configuration through `.env` files with a `.env.example` template checked into version control.
```yaml
version: "3.8"
services:
app:
build:
context: .
dockerfile: Dockerfile
container_name: myapp
restart: unless-stopped
expose:
- "3000"
env_file:
- .env
networks:
- proxy
networks:
proxy:
name: proxy
external: true
```
### Companion .env.example
Check this file into version control as a template. The actual `.env` file contains secrets and is listed in `.gitignore` on public repos only (on private Gitea repos, `.env` is committed per project conventions).
```env
# Application
NODE_ENV=production
PORT=3000
APP_URL=https://myapp.lavender-daydream.com
# Database (if applicable)
DATABASE_URL=postgresql://user:password@myapp-db:5432/myapp
# Secrets
SESSION_SECRET=generate-a-random-string-here
API_KEY=your-api-key-here
# Email (Mailgun)
MAILGUN_API_KEY=
MAILGUN_DOMAIN=
MAILGUN_FROM=noreply@lavender-daydream.com
# Deploy listener webhook secret (must match /etc/deploy-listener/deploy-listener.env)
WEBHOOK_SECRET=must-match-deploy-listener
```
### Notes
- The `env_file` directive in compose loads all variables from `.env` into the container environment.
- Variables defined in `env_file` are available both to the containerized application and to compose variable interpolation (`${VAR}` syntax in the compose file).
- Always provide a `.env.example` with placeholder values and comments explaining each variable.
- For the deploy listener to work, the repo's webhook secret must match the value in `/etc/deploy-listener/deploy-listener.env`.
---
## Universal Compose Conventions
These conventions apply to ALL stacks on mew:
1. **Always include the proxy network** if Caddy needs to reach the container:
```yaml
networks:
proxy:
name: proxy
external: true
```
2. **Use `expose`, not `ports`**: Keep ports internal to Docker networks. Never bind to the host unless absolutely necessary.
3. **Set `container_name`** explicitly: Caddy resolves containers by name. Avoid auto-generated names.
4. **Set `restart: unless-stopped`**: Containers restart automatically after crashes or server reboots, but stay stopped if manually stopped.
5. **Use `env_file` for secrets**: Do not hardcode secrets in the compose file.
6. **Use health checks** for databases and critical dependencies to ensure proper startup ordering.
7. **Persist data with named volumes or bind mounts**: Never rely on container-internal storage for important data.