Site Tools


Hotfix release available: 2026-07-14a "Mort". upgrade now! [57.1] (what's this?)
New release available: 2026-07-14 "Mort". upgrade now! [57] (what's this?)
hosting:docker:bookwyrm_caddy

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

  1. → 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=<long random string> BOOKS_POSTGRES_PASSWORD=<password> BOOKS_REDIS_ACTIVITY_PASSWORD=<password> BOOKS_REDIS_BROKER_PASSWORD=<password> RESEND_API_KEY=<your email provider 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 <caddy-container> 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_caddy.txt · Last modified: by 127.0.0.1