Back to Overview

Reverse Proxy Setup

Run Tymeslot behind Caddy or Nginx with automatic HTTPS. Production deployments need it to sign in, and OAuth integrations need it for their callback URLs.

Luka Breitig, Software Engineer & AI Developer
Luka Breitig

Aktualisiert am 2. März 2026

📋 Before You Begin

Software

  • Caddy 2.x (recommended), or
  • Nginx 1.18+ with Certbot
  • Tymeslot running and reachable at localhost:4000

Requirements

  • Root or sudo access on the server
  • A domain name with its DNS A record pointing to this server's IP address
  • Ports 80 and 443 open in your firewall

Outcome: By the end of this guide, Tymeslot will be reachable over HTTPS at your domain, with WebSocket connections passed through so pages update live.

🔀 Why a Reverse Proxy?

Tymeslot listens on port 4000 over plain HTTP. A reverse proxy sits in front of it and handles three things:

  • HTTPS termination with free Let's Encrypt certificates from Caddy or Certbot. Without HTTPS nobody can sign in: production session cookies are marked Secure, so browsers send them only over HTTPS.
  • WebSocket proxying so the browser can keep its WebSocket connection open. Phoenix LiveView uses it to push updates to the page; without it, the page loads but nothing on it responds.
  • OAuth compatibility for providers that accept only HTTPS callback URLs. Google and Microsoft reject plain HTTP callbacks for anything other than localhost.

1 Option A: Caddy (Recommended)

Caddy obtains and renews Let's Encrypt certificates on its own, with no configuration and no renewal jobs to schedule.

Install Caddy

Run the following on your server (Ubuntu/Debian):

apt install -y debian-keyring debian-archive-keyring apt-transport-https
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' \
  | gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' \
  | tee /etc/apt/sources.list.d/caddy-stable.list
apt update && apt install caddy

You should see Setting up caddy in the output, and the Caddy service is enabled automatically.

Create the Caddyfile

Open /etc/caddy/Caddyfile in your editor and replace its contents with:

tymeslot.yourdomain.com {
    reverse_proxy localhost:4000
    encode gzip
}

Reload Caddy to apply the configuration:

sudo systemctl reload caddy

Within about 30 seconds, Caddy contacts Let's Encrypt, obtains a certificate and starts serving your domain over HTTPS. The first request may take a moment while the certificate is issued.

Port 80 Must Be Free

Caddy uses port 80 for the ACME HTTP-01 challenge, which proves you own the domain before a certificate is issued. If another process (such as Apache or an old Nginx install) holds port 80, Caddy cannot obtain a certificate, and the error appears only in its logs. Run ss -tlnp | grep :80 to check.

2 Option B: Nginx + Certbot (Alternative)

Use this if Nginx is already installed on your server or required by your infrastructure team.

Install Nginx and Certbot

apt install nginx certbot python3-certbot-nginx

You should see both nginx and certbot listed as newly installed packages.

Create the site configuration

Create the file /etc/nginx/sites-available/tymeslot with the following contents. The Upgrade and Connection headers in the location block are essential: they tell Nginx to forward WebSocket upgrade requests to Tymeslot rather than dropping them:

server {
    listen 80;
    server_name tymeslot.yourdomain.com;
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    server_name tymeslot.yourdomain.com;

    ssl_certificate /etc/letsencrypt/live/tymeslot.yourdomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/tymeslot.yourdomain.com/privkey.pem;

    location / {
        proxy_pass http://localhost:4000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        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;
        proxy_read_timeout 86400;
    }
}

Enable the site and obtain a certificate

# Symlink the config into sites-enabled
ln -s /etc/nginx/sites-available/tymeslot /etc/nginx/sites-enabled/

# Obtain a Let's Encrypt certificate (Certbot edits the config automatically)
sudo certbot --nginx -d tymeslot.yourdomain.com

# Test the config and reload
nginx -t && systemctl reload nginx

After certbot completes, you should see Congratulations! and the path to your new certificate. The nginx -t command should return syntax is ok and test is successful.

WebSocket Headers Are Not Optional

Phoenix LiveView maintains a persistent WebSocket connection between the browser and the server. If the Upgrade and Connection "upgrade" headers are missing, the page loads but its live parts (available times, booking forms, search) stop responding. It is the most common Nginx misconfiguration with Phoenix applications.

If you get a 502 Bad Gateway

Nginx cannot reach Tymeslot on port 4000, usually because the application is not running. Check its logs:

docker-compose logs tymeslot

Look for startup errors or a missing database connection in the output.

3 Update Environment Variables

Now that Tymeslot is behind HTTPS, update your .env file so the application generates correct URLs for emails, OAuth callbacks and links. https is already the default for URL_SCHEME; setting it makes the choice explicit:

URL_SCHEME=https
PHX_HOST=tymeslot.yourdomain.com

Recreate the container so it picks up the change:

docker-compose down && docker-compose up -d

The containers should start cleanly. Check docker-compose logs tymeslot if the application does not come up within 15 seconds.

Why These Variables Matter

PHX_HOST is the host in every absolute URL Tymeslot builds: calendar invites, email links and OAuth redirect URIs. It also decides which origin may open a WebSocket connection. If it does not match the domain your reverse proxy serves, OAuth logins fail, email links point to the wrong host, and live updates are refused.

⚡ WebSocket Support Explained

When a visitor opens Tymeslot, Phoenix LiveView opens a WebSocket connection alongside the page. The server uses it to push updates (booking confirmations, availability changes, form validation) without the browser polling.

The connection starts as an HTTP request with the header Upgrade: websocket. Nginx, by default, treats it as a normal request and drops that header, so you must tell it to pass the header through. Caddy and Traefik do this automatically.

If the page loads but stops updating in real time

Your proxy configuration is missing the WebSocket upgrade headers. The page appears to load correctly but never reflects changes: search results do not update, booking forms do not respond, and a connection error banner may appear.

For Nginx, confirm these three lines are inside your location / block:

proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";

⚖️ Running Behind a Load Balancer or CDN

Behind AWS ALB, Cloudflare or a similar layer, each inner proxy must pass on the headers the outer one adds. Tymeslot reads the visitor's address from X-Forwarded-For or X-Real-IP for rate limiting. It does not read X-Forwarded-Proto: generated URLs take their scheme from URL_SCHEME, however many proxies sit in front.

If you use Cloudflare, go to [SSL/TLS] in the Cloudflare dashboard and set the mode to Full (strict). Using Flexible makes Cloudflare connect to your origin over HTTP while the browser sees HTTPS, which produces redirect loops between Cloudflare and Caddy or Nginx.

Restricting WebSocket Origins

Set WS_ALLOWED_ORIGINS to a comma-separated list of origins (with scheme, such as https://tymeslot.yourdomain.com) to control which sites can open a WebSocket connection to your instance. Set it when visitors reach Tymeslot under more than one hostname. By default it allows PHX_HOST plus localhost:4000. Docker deployments only; Cloudron ignores it.

❓ Frequently Asked Questions

Phoenix LiveView disconnects repeatedly. What's wrong with my proxy config?

LiveView relies on a persistent WebSocket connection. When a proxy drops or mishandles the WebSocket upgrade, the browser keeps retrying and the disconnect banner keeps coming back. For Nginx, confirm all three of these directives are inside your location / block:

proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";

For Caddy, the reverse_proxy directive handles WebSocket upgrades automatically, with no extra configuration.

I get a '502 Bad Gateway' error. What should I check?

A 502 means Nginx or Caddy could not get a response from the upstream address. Usually Tymeslot is not running or is still starting up. Check the container status and logs:

docker-compose ps
docker-compose logs tymeslot

Look for startup errors or a failed database connection in the output. Also confirm that the port in your proxy config matches the port Tymeslot listens on; the default is 4000.

Caddy handles HTTPS automatically. Do I still need to configure anything for SSL?

No. Caddy contacts Let's Encrypt, obtains a certificate for the domain in your Caddyfile and renews it before it expires, with no cron jobs or manual steps.

It needs only two things: ports 80 and 443 open in your firewall, and your domain's DNS A record pointing to the server's IP address. Without both, the ACME challenge fails and no certificate is issued. Run ss -tlnp | grep :80 to confirm nothing else is occupying port 80.

Why do OAuth redirects fail after adding a reverse proxy?

OAuth providers (Google, Microsoft, GitHub) compare the callback URL they receive with the one registered in their developer console. If Tymeslot generates an http:// callback URL when your proxy is serving https://, the comparison fails and the login is rejected.

Tymeslot builds callback URLs from two variables, not from proxy headers. Check both in your .env file, and recreate the container after changing them:

URL_SCHEME=https
PHX_HOST=tymeslot.yourdomain.com

Then confirm the callback URL registered with the provider uses the same host and https://. The proxy_set_header X-Forwarded-Proto $scheme; line in the Nginx example does no harm, but it does not change these URLs.

Can I run Tymeslot behind Cloudflare?

Yes. Set Cloudflare's SSL/TLS mode to Full (strict) in the Cloudflare dashboard. Using Flexible makes Cloudflare connect to your origin over plain HTTP while showing HTTPS to the browser, which creates redirect loops with Caddy and Nginx.

Also make sure WebSockets are enabled for your Cloudflare zone: go to Network in the Cloudflare dashboard and confirm WebSockets is switched on. Without it, Cloudflare's edge drops LiveView connections before they reach your server.

Verify Your Setup

Confirm each of the following before considering the setup complete:
  • HTTPS loads without a certificate warning. Visit https://tymeslot.yourdomain.com in your browser. The address bar should show a padlock icon.
  • HTTP redirects to HTTPS. Visit http://tymeslot.yourdomain.com and check that it redirects to the HTTPS version.
  • A booking page opens and loads fully. Open any public booking link. The page should render completely, with no spinner stuck on screen.
  • Real-time updates work. Log in and open the dashboard calendar. Type in its search box: results that update as you type confirm the WebSocket connection is live.
  • The health endpoint returns 200. Visit https://tymeslot.yourdomain.com/healthcheck in a browser. It should return a short JSON report with "status": "ok".

🔗 Related Articles

Read Docker Self-Hosting

Docker Self-Hosting

Deploy Tymeslot with Docker Compose on any VPS or home server. Includes reverse proxy setup, file upload volumes, and WebSocket configuration for LiveView.

Read Cloudron Deployment

Cloudron Deployment

Install Tymeslot on Cloudron in minutes. PostgreSQL, SSL certificates, email relay, and automatic updates are all provisioned and managed for you.

Read Railway Deployment

Railway Deployment

Deploy Tymeslot to Railway with one click. PostgreSQL is provisioned and connected for you; add a signing secret and your email settings and your instance is live.