# 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.