====== BookWyrm with Caddy (Custom Stack) ====== This guide covers adding BookWyrm to an existing Docker Compose stack where Caddy is the reverse proxy. Unlike the official BookWyrm docker-compose, this approach does not use certbot and adds a standalone nginx container for static files. See [[hosting:docker:bookwyrm_cloudflare|BookWyrm with Cloudflare Tunnel]] if you are using the official BookWyrm docker-compose instead. ===== Traffic flow ===== ``` Browser -- HTTPS --> Caddy -- HTTP --> nginx:80 --> /static/ served directly --> everything else --> Gunicorn:8000 ``` Caddy must point at nginx, not at Gunicorn directly. If Caddy points at Gunicorn, static file requests return HTML with the wrong MIME type and the site has no styles. ===== Environment variables ===== Add to your ''.env'' file: ``` BOOKS_SECRET_KEY= BOOKS_POSTGRES_PASSWORD= BOOKS_REDIS_ACTIVITY_PASSWORD= BOOKS_REDIS_BROKER_PASSWORD= RESEND_API_KEY= ``` Generate a secret key with: ```bash python3 -c "import secrets; print(secrets.token_hex(50))" ``` ===== nginx config ===== Create ''nginx-books/nginx.conf'' alongside your ''docker-compose.yml'': ```nginx server { listen 80; client_max_body_size 10M; resolver 127.0.0.11 valid=30s; location /static/ { alias /app/static/; } location /images/ { alias /app/images/; } location / { set $upstream bookwyrm-web:8000; proxy_pass http://$upstream; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto https; proxy_redirect off; } } ``` The ''resolver 127.0.0.11'' line and the ''set $upstream'' variable are required. Without them, nginx tries to resolve ''bookwyrm-web'' at startup before that container exists and crashes with ''host not found in upstream''. Using a variable forces nginx to resolve the name at request time via Docker's internal DNS. The service name ''bookwyrm-web'' must match exactly what your Gunicorn service is named in ''docker-compose.yml''. ===== docker-compose.yml services ===== ```yaml bookwyrm-db: image: postgres:16-alpine container_name: bookwyrm-db restart: unless-stopped networks: [your_net] healthcheck: test: ["CMD-SHELL", "pg_isready -U bookwyrm"] interval: 10s timeout: 5s retries: 5 environment: POSTGRES_USER: bookwyrm POSTGRES_PASSWORD: ${BOOKS_POSTGRES_PASSWORD} POSTGRES_DB: bookwyrm volumes: - bookwyrm_postgres:/var/lib/postgresql/data bookwyrm-redis-activity: image: redis:7-alpine container_name: bookwyrm-redis-activity restart: unless-stopped networks: [your_net] command: redis-server --requirepass ${BOOKS_REDIS_ACTIVITY_PASSWORD} volumes: - bookwyrm_redis_activity:/data bookwyrm-redis-broker: image: redis:7-alpine container_name: bookwyrm-redis-broker restart: unless-stopped networks: [your_net] command: redis-server --requirepass ${BOOKS_REDIS_BROKER_PASSWORD} volumes: - bookwyrm_redis_broker:/data bookwyrm-web: image: ghcr.io/bookwyrm-social/bookwyrm:latest container_name: bookwyrm-web restart: unless-stopped networks: [your_net] command: gunicorn bookwyrm.wsgi:application --bind 0.0.0.0:8000 --workers 3 depends_on: bookwyrm-db: condition: service_healthy environment: DOMAIN: books.example.com EMAIL: admin@example.com SECRET_KEY: ${BOOKS_SECRET_KEY} PORT: "8000" USE_HTTPS: "true" ALLOWED_HOSTS: books.example.com POSTGRES_HOST: bookwyrm-db POSTGRES_PORT: "5432" POSTGRES_USER: bookwyrm POSTGRES_PASSWORD: ${BOOKS_POSTGRES_PASSWORD} POSTGRES_DB: bookwyrm REDIS_ACTIVITY_HOST: bookwyrm-redis-activity REDIS_ACTIVITY_PORT: "6379" REDIS_ACTIVITY_PASSWORD: ${BOOKS_REDIS_ACTIVITY_PASSWORD} REDIS_BROKER_HOST: bookwyrm-redis-broker REDIS_BROKER_PORT: "6379" REDIS_BROKER_PASSWORD: ${BOOKS_REDIS_BROKER_PASSWORD} EMAIL_HOST: smtp.resend.com EMAIL_PORT: "587" EMAIL_HOST_USER: resend EMAIL_HOST_PASSWORD: ${RESEND_API_KEY} EMAIL_USE_TLS: "true" DEFAULT_FROM_EMAIL: noreply@example.com volumes: - bookwyrm_static:/app/static - bookwyrm_media:/app/images bookwyrm-nginx: image: nginx:alpine container_name: bookwyrm-nginx restart: unless-stopped networks: [your_net] depends_on: - bookwyrm-web volumes: - ./nginx-books/nginx.conf:/etc/nginx/conf.d/default.conf:ro - bookwyrm_static:/app/static:ro - bookwyrm_media:/app/images:ro bookwyrm-celery-worker: image: ghcr.io/bookwyrm-social/bookwyrm:latest container_name: bookwyrm-celery-worker restart: unless-stopped networks: [your_net] command: celery -A celerywyrm worker -l INFO depends_on: - bookwyrm-web environment: # same env vars as bookwyrm-web above bookwyrm-celery-beat: image: ghcr.io/bookwyrm-social/bookwyrm:latest container_name: bookwyrm-celery-beat restart: unless-stopped networks: [your_net] command: celery -A celerywyrm beat -l INFO --scheduler django_celery_beat.schedulers:DatabaseScheduler depends_on: - bookwyrm-web environment: # same env vars as bookwyrm-web above -- EMAIL_HOST is required even for workers ``` Add named volumes to your ''volumes:'' section: ```yaml volumes: bookwyrm_postgres: bookwyrm_redis_activity: bookwyrm_redis_broker: bookwyrm_static: bookwyrm_media: ``` ===== Caddyfile ===== Point Caddy at nginx, not at Gunicorn: ``` books.example.com { reverse_proxy bookwyrm-nginx:80 { header_up Host {host} } header { X-Content-Type-Options nosniff X-Frame-Options SAMEORIGIN Referrer-Policy strict-origin-when-cross-origin } } ``` ===== Troubleshooting ===== ==== Static files still returning HTML after adding nginx ==== Verify that Caddy's live running config actually points to nginx. ''caddy reload'' can silently leave the old config in place. Check with: ```bash docker exec curl -s http://localhost:2019/config/ ``` If the output shows the old Gunicorn upstream, restart the Caddy container fully: ```bash docker compose up -d --force-recreate caddy ``` ==== host not found in upstream at nginx startup ==== If nginx crashes immediately with ''host not found in upstream "bookwyrm-web:8000"'', the nginx config is using a static ''upstream'' block instead of the ''resolver'' approach. See the nginx config section above for the correct config using ''resolver 127.0.0.11'' and ''set $upstream''. ==== nginx config is read-only warning ==== The warning ''can not modify /etc/nginx/conf.d/default.conf (read-only file system?)'' is harmless. The config is mounted as '':ro'' intentionally. ===== See Also ===== * [[hosting:docker:bookwyrm|BookWyrm overview]] * [[hosting:docker:bookwyrm_cloudflare|BookWyrm with Cloudflare Tunnel]] * [[hosting:caddy:start|Caddy Reverse Proxy]] Last updated: 2026-06-21 * [[hosting:docker:start|Return to Docker]]