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
8.4 KiB
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.
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
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]
Notes
- Use
expose(notports) to keep the port internal to Docker networks only. - Set
container_nameto a unique, descriptive name — Caddy uses this name in itsreverse_proxydirective. - 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.
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
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=1ensures log output appears immediately indocker compose logs.- For production, consider adding
--workers 4to 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.
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
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_filesfallback to/index.htmlsupports 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.
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
imageandexposeport with whatever the application requires. - Check the image documentation for required environment variables and volume mount paths.
- Persist application data by mounting a local
./datadirectory. - 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.
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
internalnetwork — it is not reachable from Caddy or any other container outside this stack. - The app is on both
proxy(so Caddy can reach it) andinternal(so it can reach the database). depends_onwithcondition: service_healthyensures 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:Note the hostname is the database container name (DATABASE_URL=postgresql://myapp:secretpassword@myapp-db:5432/myappmyapp-db), notlocalhost.
6. App with Environment File
Pattern for managing configuration through .env files with a .env.example template checked into version control.
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).
# 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_filedirective in compose loads all variables from.envinto the container environment. - Variables defined in
env_fileare available both to the containerized application and to compose variable interpolation (${VAR}syntax in the compose file). - Always provide a
.env.examplewith 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:
-
Always include the proxy network if Caddy needs to reach the container:
networks: proxy: name: proxy external: true -
Use
expose, notports: Keep ports internal to Docker networks. Never bind to the host unless absolutely necessary. -
Set
container_nameexplicitly: Caddy resolves containers by name. Avoid auto-generated names. -
Set
restart: unless-stopped: Containers restart automatically after crashes or server reboots, but stay stopped if manually stopped. -
Use
env_filefor secrets: Do not hardcode secrets in the compose file. -
Use health checks for databases and critical dependencies to ensure proper startup ordering.
-
Persist data with named volumes or bind mounts: Never rely on container-internal storage for important data.