WebCalendar

Run WebCalendar Behind a Reverse Proxy with HTTPS (Caddy, Nginx, Traefik)

In the Docker Compose install guide we got WebCalendar running on http://localhost:8080. That’s perfect for a local trial, but you should never expose that port directly to the internet: it’s plain HTTP, so logins and event data travel unencrypted. The fix is to put a reverse proxy in front of the container to terminate TLS and serve everything over HTTPS.

This guide shows three ways to do it — Caddy (the simplest, with automatic certificates), Nginx (the most common), and Traefik (great if you already use it) — plus the one gotcha that trips up almost everyone: making WebCalendar realize it’s being served over HTTPS.

Prerequisites

  • WebCalendar already running in Docker (see the install guide).
  • A domain name (e.g. calendar.example.com) with a DNS A record pointing at your server’s public IP.
  • Ports 80 and 443 open to the internet on that server. Port 80 is needed for the initial Let’s Encrypt HTTP challenge.

Step 1: Stop exposing the app port

The most important change: the WebCalendar container should not publish port 8080 to the world anymore. Instead, the proxy and the app talk to each other over Docker’s internal network, and only the proxy is reachable from outside.

In your docker-compose.yml, remove the ports: block from the webcalendar service. Because it shares a Compose network with the proxy, the proxy can still reach it at webcalendar:80 by service name.


Option A — Caddy (recommended: automatic HTTPS)

Caddy obtains and renews Let’s Encrypt certificates for you automatically, and it sets the correct forwarding headers out of the box. It’s two files.

1. Create a Caddyfile:

calendar.example.com {
    reverse_proxy webcalendar:80
}

2. Add Caddy to your Compose stack (alongside the db and webcalendar services from the install guide):

  caddy:
    image: caddy:2
    container_name: webcalendar-caddy
    depends_on:
      - webcalendar
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy-data:/data
      - caddy-config:/config
    restart: unless-stopped

volumes:
  db-data:
  caddy-data:
  caddy-config:

Bring it up and Caddy will fetch a certificate on first request:

docker compose up -d

Visit https://calendar.example.com — you’re done. Caddy automatically redirects HTTP to HTTPS and passes X-Forwarded-Proto: https to the container.


Option B — Nginx + Let’s Encrypt

If you’re already running Nginx (on the host or as a container), here’s a server block that terminates TLS and proxies to the WebCalendar container. This assumes you’re running Nginx on the host and WebCalendar is published on 127.0.0.1:8080 (bind it to localhost only: - "127.0.0.1:8080:80").

server {
    listen 80;
    server_name calendar.example.com;
    # Allow the ACME challenge, redirect everything else to HTTPS
    location /.well-known/acme-challenge/ { root /var/www/certbot; }
    location / { return 301 https://$host$request_uri; }
}

server {
    listen 443 ssl http2;
    server_name calendar.example.com;

    ssl_certificate     /etc/letsencrypt/live/calendar.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/calendar.example.com/privkey.pem;

    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_set_header Host              $host;
        proxy_set_header X-Real-IP         $remote_addr;
        proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;   # critical: tells WebCalendar it's HTTPS
    }
}

Obtain the certificate with Certbot using the webroot challenge, then reload Nginx:

sudo certbot certonly --webroot -w /var/www/certbot -d calendar.example.com
sudo nginx -t && sudo systemctl reload nginx

Certbot installs a systemd timer that renews automatically. The X-Forwarded-Proto header is the line that matters most here — leave it out and you’ll hit the redirect loop described below.


Option C — Traefik (label-driven)

If you already run Traefik as your edge router with an ACME resolver configured, you don’t need a separate config file — just add labels to the webcalendar service (and, again, drop its public ports:):

    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.webcalendar.rule=Host(`calendar.example.com`)"
      - "traefik.http.routers.webcalendar.entrypoints=websecure"
      - "traefik.http.routers.webcalendar.tls.certresolver=letsencrypt"
      - "traefik.http.services.webcalendar.loadbalancer.server.port=80"

Traefik forwards X-Forwarded-Proto automatically, so HTTPS detection works the same as with Caddy.


The gotcha: making WebCalendar know it’s on HTTPS

When a proxy terminates TLS, the request that reaches the container is plain HTTP. The PHP app inside sees http, not https — and if it builds absolute URLs or issues redirects based on that, you get the two classic symptoms:

  • A redirect loop (“too many redirects”) — the app keeps trying to “upgrade” a request it thinks is insecure.
  • Mixed-content warnings — pages load over HTTPS but reference CSS/JS/links as http://.

There are two levers, and you generally want both:

1. Forward the header (proxy side)

Make sure the proxy sends X-Forwarded-Proto: https. Caddy and Traefik do this automatically; for Nginx it’s the proxy_set_header X-Forwarded-Proto $scheme; line above.

2. Make the container trust it (app side)

The WebCalendar image is built on php:8-apache. Tell its Apache to treat a forwarded https as a real HTTPS request by mounting a one-line config. Create forwarded-proto.conf:

SetEnvIf X-Forwarded-Proto "https" HTTPS=on

And mount it into the webcalendar service:

    volumes:
      - ./forwarded-proto.conf:/etc/apache2/conf-enabled/forwarded-proto.conf:ro

3. Set WebCalendar’s server URL

Finally, log in as admin and set WebCalendar’s server URL to the full https://calendar.example.com/ in Admin → System Settings. This value is stored in the database and ensures generated links, email notifications, and iCal feed URLs all use the correct HTTPS address.


Optional: hardening headers (HSTS)

Once you’ve confirmed HTTPS works everywhere, add HTTP Strict Transport Security so browsers refuse to talk to the site over plain HTTP. In Caddy:

calendar.example.com {
    reverse_proxy webcalendar:80
    header Strict-Transport-Security "max-age=31536000; includeSubDomains"
}

In Nginx, add inside the 443 server block: add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;. Only enable HSTS after HTTPS is fully working — it’s sticky, and browsers will honor it for a year.

Verify it

  • https://calendar.example.com loads with a valid padlock.
  • Visiting http://calendar.example.com redirects to HTTPS.
  • No mixed-content warnings in the browser console, and logging in works without a redirect loop.
  • Port 8080 is not reachable from outside the server.

Next steps

Which proxy are you running in front of WebCalendar? Let me know in the comments if you hit any snags with HTTPS detection.

Leave a Reply

Your email address will not be published. Required fields are marked *