Reverse Proxy and SSL
Purpose and Scope
This page explains how to reason about HTTPS when Cal.diy is deployed behind a reverse proxy, load balancer, or cloud edge that terminates TLS before traffic reaches the application container. A reverse proxy is the front-facing component that accepts browser traffic, presents the public certificate, and forwards requests to the Cal.diy service. TLS termination means the proxy handles HTTPS externally while the upstream application may receive plain HTTP internally. That arrangement is common, but it makes URL configuration and trust boundaries more important than a simple localhost install.
Sources: apps/docs/content/troubleshooting.mdx, apps/docs/content/docker.mdx
Cal.diy’s self-hosting docs connect SSL problems to two practical concerns: the public web URL that the app believes it is serving, and the network path that server-side code uses when calling back into the application. The troubleshooting guide calls out redirects to localhost:3000 after deployment as a URL and redirect issue caused by missing production-domain settings. The Docker guide separately notes SSL edge termination behind a load balancer and warns that disabling TLS rejection should only be done when the operator trusts the services directing traffic to Cal.diy.
Sources: apps/docs/content/troubleshooting.mdx, apps/docs/content/docker.mdx
Relevant Source Files
apps/docs/content/troubleshooting.mdx— contains the reader-facing troubleshooting sections for URL and redirect issues, includingNEXT_PUBLIC_WEBAPP_URL,NEXTAUTH_URL, Docker build-time implications, and related deployment symptoms.apps/docs/content/docker.mdx— documents Docker startup, build-time versus runtime variables, update flow, the SSL edge-termination note, and theCLIENT_FETCH_ERRORcontext tied to base URL and container DNS access.apps/docs/content/deployments/gcp.mdx— shows a cloud VM deployment path where port 80 is opened and the Docker container is published on port 80, which is the common upstream target when HTTPS is handled by an external proxy or cloud load balancer.
Mental Model: Public HTTPS, Internal HTTP, and Canonical URLs
When a browser visits a production Cal.diy instance, users should see a single canonical HTTPS origin such as https://cal.yourdomain.com. That origin must match the URL used for authentication callbacks, internal links, integration callbacks, and browser-visible assets. If the app still thinks the canonical origin is http://localhost:3000, users can be sent back to a development URL after login or see links that do not work from the public internet. The troubleshooting docs name NEXT_PUBLIC_WEBAPP_URL and NEXTAUTH_URL as the variables that tell Cal.diy and NextAuth where the app is hosted.
Sources: apps/docs/content/troubleshooting.mdx
The proxy layer does not automatically fix an incorrect application URL. A load balancer can accept HTTPS and forward to a container over HTTP, but Cal.diy still needs the public URL configured without a trailing slash. The docs also note that NEXTAUTH_URL is optional if NEXT_PUBLIC_WEBAPP_URL is set, because NextAuth can infer the base URL from the incoming request Host header when it is not explicitly configured. In practice, the safest self-hosted setup is to set the canonical public web app URL deliberately and keep proxy host forwarding consistent with it.
Sources: apps/docs/content/troubleshooting.mdx
Configuration Reference
Use this compact reference when reviewing a reverse-proxied deployment. The values here are not a full environment file; they are the fields most likely to affect HTTPS behavior, redirects, and Docker rebuild expectations.
| Name | Where it matters | Reverse-proxy implication |
|---|---|---|
NEXT_PUBLIC_WEBAPP_URL | Web app public URL and browser-visible configuration | Set to the external HTTPS origin, for example https://cal.yourdomain.com; do not include a trailing slash. In Docker, this is a build-time variable and changing it requires rebuilding the image. |
NEXTAUTH_URL | Authentication callback base URL | Set to the same production origin when explicitly configured. The docs state it can be omitted if NEXT_PUBLIC_WEBAPP_URL is set and NextAuth can infer from the request host. |
NODE_TLS_REJECT_UNAUTHORIZED=0 | Docker troubleshooting for SSL edge termination | Only consider this behind a trusted SSL-terminating load balancer when requests would otherwise be rejected. The Docker docs explicitly warn to use it only when you know what you are doing and trust the services or load balancers directing traffic. |
WEBAPP_URL | Docker-side server callback behavior noted in CLIENT_FETCH_ERROR troubleshooting | Ensure container-side services can resolve and reach the base URL they use. A public DNS name that works in a browser may not be reachable from inside the container network. |
# Public origin visible to users and callbacks
NEXT_PUBLIC_WEBAPP_URL=https://cal.yourdomain.com
NEXTAUTH_URL=https://cal.yourdomain.com
# Only for trusted SSL edge-termination cases where you understand the risk
NODE_TLS_REJECT_UNAUTHORIZED=0Sources: apps/docs/content/troubleshooting.mdx, apps/docs/content/docker.mdx
Docker and Build-Time Behavior
Docker changes the timing of URL fixes. The troubleshooting docs state that NEXT_PUBLIC_WEBAPP_URL must be set before building the image because it is a build-time variable inlined by Next.js. The Docker documentation repeats this distinction by listing NEXT_PUBLIC_WEBAPP_URL under build-time variables, alongside other public build configuration. If you correct the URL in .env but continue running an already-built image, browser-visible behavior can remain wrong because the previous value was compiled into the web app.
Sources: apps/docs/content/troubleshooting.mdx, apps/docs/content/docker.mdx
A reliable update loop is therefore: stop the stack, update environment variables, rebuild or pull an image that was built with the desired public URL, and restart the services. The Docker guide’s update flow stops the stack with docker compose down, pulls changes with docker compose pull, and starts again with docker compose up -d. If you maintain a custom image for production, apply the same sequencing but make sure the build step receives the production NEXT_PUBLIC_WEBAPP_URL before the image is created.
Sources: apps/docs/content/docker.mdx
docker compose down
# Update .env first; rebuild if NEXT_PUBLIC_WEBAPP_URL changed at build time
docker compose pull
docker compose up -dThe Docker troubleshooting section also mentions CLIENT_FETCH_ERROR, explaining that it may come from the default auth callback using WEBAPP_URL as a base URL while the container does not necessarily have access to the same DNS as the operator’s browser. This is especially relevant behind reverse proxies: the public hostname may resolve externally to the load balancer, but the container network may require an internal service name, a reachable proxy address, or DNS rules that allow the container to call the same public URL.
Sources: apps/docs/content/docker.mdx
Cloud Edge and Port Mapping Flow
The GCP deployment guide demonstrates a minimal cloud VM pattern: create a virtual machine, allow traffic on port 80, install Docker, pull calcom/cal.diy, and run the container with docker run -d -p 80:80 calcom/cal.diy. That guide accesses the application over HTTP. In a reverse-proxy architecture, this port-80 service is typically the upstream target, while HTTPS is added at the cloud load balancer, VM reverse proxy, or another edge component that owns the certificate.
Sources: apps/docs/content/deployments/gcp.mdx
That split is operationally normal, but it creates a contract between layers. The public edge must send the correct host to Cal.diy, Cal.diy must be configured with the public HTTPS URL, and internal services must be able to reach whatever URL they are configured to call. If only the external browser path is tested, login and callback failures can appear later because server-side requests experience a different DNS and TLS environment. Treat browser access, authentication redirect flow, and container-to-web-app reachability as separate checks.
Sources: apps/docs/content/troubleshooting.mdx, apps/docs/content/docker.mdx, apps/docs/content/deployments/gcp.mdx
Troubleshooting Workflow
Start with the visible symptom. If login redirects or internal links point to http://localhost:3000, fix NEXT_PUBLIC_WEBAPP_URL and, when used, NEXTAUTH_URL so they match the production HTTPS origin exactly and have no trailing slash. If this is Docker, rebuild or replace the image when the public URL changed because the value is inlined during the Next.js build. After restarting, test a fresh private browser session so stale cookies and cached redirects do not hide the current configuration.
Sources: apps/docs/content/troubleshooting.mdx, apps/docs/content/docker.mdx
Next, check the proxy trust and TLS path. If Cal.diy is behind a trusted load balancer that terminates SSL and forwards traffic internally, the Docker docs say NODE_TLS_REJECT_UNAUTHORIZED=0 may be needed to prevent requests from being rejected, but the same note cautions that this should only be done when the operator understands the risk and trusts the services directing traffic. Do not use that setting as a generic fix for certificate problems; prefer a correct certificate chain and correct upstream routing whenever possible.
Sources: apps/docs/content/docker.mdx
Finally, validate reachability from inside the deployment, not only from a laptop browser. The CLIENT_FETCH_ERROR note is a reminder that a container may not have access to the same DNS as an external user. If a callback base URL resolves to a load balancer that cannot hairpin back into the same private network, the browser may load Cal.diy while server-side auth callbacks fail. In that case, adjust DNS, proxy routing, or service URL settings so the configured base URL is reachable from the relevant runtime context.
Sources: apps/docs/content/docker.mdx
Next Steps
After fixing reverse-proxy and SSL behavior, review the broader environment and deployment pages before changing unrelated settings. The URL variables on this page overlap with deployment-specific setup, Docker image rebuild behavior, and troubleshooting for API or database services. If the instance is new, confirm the first-run setup wizard still opens at the public HTTPS origin. If the instance is already in use, schedule URL and proxy changes together with a Docker restart window so authentication callbacks, integration callbacks, and browser links move consistently to the same canonical origin.
Related pages: environment-and-urls, docker-configuration, docker-updates-and-troubleshooting, deploy-gcp, troubleshooting-api-database