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.
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.
Zaktualizowano: 2 marca 2026
Software
localhost:4000
Requirements
sudo
access on the server
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.
Tymeslot listens on port 4000 over plain HTTP. A reverse proxy sits in front of it and handles three things:
Caddy obtains and renews Let's Encrypt certificates on its own, with no configuration and no renewal jobs to schedule.
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.
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.
ss -tlnp | grep :80
to check.
Use this if Nginx is already installed on your server or required by your infrastructure team.
apt install nginx certbot python3-certbot-nginx
You should see both
nginx
and certbot
listed as newly installed packages.
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;
}
}
# 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.
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.
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.
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.
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";
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.
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.
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.
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.
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.
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.
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.
https://tymeslot.yourdomain.com
in your browser. The address bar should show a padlock icon.
http://tymeslot.yourdomain.com
and check that it redirects to the HTTPS version.
https://tymeslot.yourdomain.com/healthcheck
in a browser. It should return a short JSON report with "status": "ok".
Deploy Tymeslot with Docker Compose on any VPS or home server. Includes reverse proxy setup, file upload volumes, and WebSocket configuration for LiveView.
Install Tymeslot on Cloudron in minutes. PostgreSQL, SSL certificates, email relay, and automatic updates are all provisioned and managed for you.
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.