---
title: HTTPS & Domain Lock
description: Set up HTTPS with Caddy and lock your Pinchy instance to a single domain — secure cookies, HSTS, and origin checks are enabled automatically.
---

import { Aside, Steps } from "@astrojs/starlight/components";

Out of the box Pinchy accepts requests on any host over plain HTTP. That's fine for local development but unsafe in production. This guide walks you through two steps that secure your instance: **HTTPS** via a reverse proxy (so all traffic is encrypted) and **Domain Lock** (so Pinchy only responds to your chosen domain).

## Set up HTTPS with Caddy

HTTPS encrypts all traffic between browsers and your server. Pinchy doesn't handle TLS itself — a reverse proxy sits in front of it and takes care of certificates. We recommend [Caddy](https://caddyserver.com/) because it automatically gets and renews Let's Encrypt certificates with zero configuration.

<Aside type="tip">
  If you deployed via the [Hetzner](/guides/deploy-hetzner/) or
  [DigitalOcean](/guides/deploy-digitalocean/) guides, Caddy is already
  installed — skip to [editing the Caddyfile](#configure-your-domain).
</Aside>

### Install Caddy

```bash
apt-get install -y caddy
```

### Configure your domain

Make sure your domain's DNS A record points to your server's IP address. Then edit the Caddy configuration:

```bash
nano /etc/caddy/Caddyfile
```

Replace the contents with (use your actual domain):

```
pinchy.example.com {
    reverse_proxy localhost:7777
}
```

Save the file (Ctrl+O, Enter, Ctrl+X in nano) and restart Caddy:

```bash
systemctl restart caddy
```

Caddy automatically provisions Let's Encrypt certificates for your domain. After a few seconds, visit `https://pinchy.example.com` — you should see Pinchy with a valid HTTPS certificate.

<Aside type="caution">
  Do **not** set `PINCHY_PORT=0.0.0.0:7777` in your `.env`. Docker manages its
  own iptables rules and bypasses host firewall tools like UFW — a `0.0.0.0`
  binding exposes Pinchy to the internet regardless of your firewall. Keep the
  default `127.0.0.1:7777` binding so only Caddy can reach Pinchy.
</Aside>

### Set the public auth URL

For production domains, set `BETTER_AUTH_URL` to the same HTTPS origin users visit. Better Auth uses it when it needs to build absolute links, including password resets, OAuth callbacks, and Telegram pairing redirects.

Edit your Pinchy `.env` file:

```bash
nano /opt/pinchy/.env
```

Add your public URL, including `https://` and no trailing slash:

```bash
BETTER_AUTH_URL=https://pinchy.example.com
```

<Aside type="caution">
  This setting only takes effect if your `docker-compose.yml` passes
  `BETTER_AUTH_URL` through to the `pinchy` service. Older compose files may
  need to be updated, or you can add the variable via
  `docker-compose.override.yml`.
</Aside>

Restart the Pinchy container so Docker Compose passes the updated value through:

```bash
cd /opt/pinchy
docker compose up -d pinchy
```

<details>
<summary>What's a reverse proxy and why do I need one?</summary>

A reverse proxy sits between the internet and Pinchy. When someone visits your domain:

1. Their browser connects to **Caddy** on port 443 (HTTPS)
2. Caddy handles the SSL certificate and encryption
3. Caddy forwards the request to **Pinchy** on port 7777 (internal only)
4. Pinchy sends the response back through Caddy

This way, Pinchy never needs to deal with SSL certificates directly.

</details>

See the [Hardening Guide](/guides/hardening/#reverse-proxy-with-tls) for nginx examples and advanced configuration.

## What domain lock does

Once HTTPS is working, the next step is locking Pinchy to that domain. Domain Lock pins your instance to exactly one HTTPS domain and turns on the rest of the production security profile as a package:

- Only requests whose `Host` header matches the locked domain reach the app. Everything else gets a `403 Access Denied` page.
- Cookies are issued with `Secure` and `SameSite=Lax`.
- HSTS is advertised so modern browsers refuse to connect over plain HTTP.
- Origin checks are enforced on state-changing requests.

You don't have to configure any of these individually — locking the domain flips them all on.

## Lock the domain

<Steps>

1. Sign in as an admin on the HTTPS domain you want to lock to
2. Go to **Settings → Security**
3. The page shows your current host and whether the connection is HTTPS
4. Click **Lock to this domain**
5. Pinchy restarts briefly to apply the new security profile — the page will reload automatically

</Steps>

From this point on, only the locked domain can reach the app. Use the same hostname for the lock and for `BETTER_AUTH_URL`; the lock controls which host Pinchy accepts, while `BETTER_AUTH_URL` controls the absolute URLs Better Auth generates.

<Aside type="note">
  Domain Lock and `BETTER_AUTH_URL` are related but separate. Domain Lock
  rejects requests for the wrong host. `BETTER_AUTH_URL` tells Better Auth which
  public HTTPS origin to use when it builds callback and redirect URLs. In
  production, set `BETTER_AUTH_URL=https://pinchy.example.com` in your `.env`
  and restart Pinchy.
</Aside>

<Aside type="caution">
  If Domain Lock is configured but `BETTER_AUTH_URL` is unset, Pinchy logs a
  warning on every startup — Better Auth cannot derive the base URL from the
  Domain Lock value, so password reset and email verification links would point
  to the wrong host. Set `BETTER_AUTH_URL` to your locked domain and restart.
</Aside>

## Remove the lock

If the business needs change (different domain, temporary maintenance URL, etc.):

1. Go to **Settings → Security**
2. Click **Remove domain lock**
3. Confirm — the restart happens the same way as on lock

## Recover from a lockout

If HTTPS goes down (expired certificate, proxy misconfiguration, DNS change) you can find yourself unable to reach the admin UI at all. Pinchy includes a last-resort CLI to clear the lock from inside the container:

```bash
docker exec pinchy pnpm domain:reset
```

The command needs shell access to the Pinchy container, so protect it like any other production admin credential. After it runs, Pinchy accepts any host again and you can reach the UI on the non-HTTPS or fallback URL to diagnose the underlying problem.

## What about local development?

Don't use domain lock in development. It is designed for a single deployed instance on a known domain, not for `localhost` or Docker dev stacks. The Security tab is still visible but it refuses to lock when the current connection is not HTTPS.
