Custom Domain & HTTPS
A fresh install answers on http://localhost. This guide moves it to your own domain over HTTPS — for example https://consent.example.com.
Do this before you add sites. Every consent script Conzent generates has your domain baked into it. Changing the domain later works, but you must regenerate the scripts (step 5) and update the embed snippet on every website already using them (step 6).
1. Point DNS at your server
Create an A record for the hostname you want, pointing at your server's public IP:
consent.example.com. A 203.0.113.10
Confirm it resolves with dig +short consent.example.com before continuing. A subdomain of a domain you already own — consent., cmp., privacy. — is the usual choice.
2. Set APP_URL
APP_URL is the single source of truth for every absolute URL Conzent produces: consent script endpoints, the embed snippet shown in the dashboard, password-reset links, and OAuth callbacks.
Edit .env in your install directory:
APP_URL=https://consent.example.com
Use the exact public URL, with the scheme and no trailing slash. If you are terminating TLS — and you should — that means https://, even though the container itself still speaks plain HTTP internally. Setting https:// also switches the "remember me" cookie to Secure, so it is never sent over plain HTTP.
New installs can do steps 1 and 2 in one shot:
curl -sSL https://getconzent.com/install | sh -s -- --domain consent.example.com
3. Terminate TLS
The bundled nginx container serves plain HTTP on ${APP_PORT:-80}. It does not obtain or serve certificates.
Option A — Caddy in front. Caddy fetches and renews Let's Encrypt certificates automatically. Free up port 80 by setting APP_PORT=8080 in .env, then create docker-compose.override.yml:
services:
caddy:
image: caddy:latest
ports:
- "80:80"
- "443:443"
volumes:
- ./docker/caddy/Caddyfile:/etc/caddy/Caddyfile:ro
- caddy-data:/data
- caddy-config:/config
depends_on:
- nginx
restart: unless-stopped
volumes:
caddy-data:
caddy-config:
With docker/caddy/Caddyfile:
consent.example.com {
reverse_proxy nginx:80
}
Run docker compose up -d. Caddy issues the certificate on the first request.
Option B — an existing nginx or Apache. Set APP_PORT=8080 in .env, then proxy to it:
server {
listen 443 ssl http2;
server_name consent.example.com;
ssl_certificate /etc/letsencrypt/live/consent.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/consent.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;
}
}
X-Forwarded-For matters beyond tidiness: Conzent records the visitor IP on every consent log entry and uses it for geo-targeting. Without the header, every consent is attributed to your proxy.
Option C — Cloudflare. Proxy the record and set SSL/TLS mode to Full. Keep origin TLS on; "Flexible" leaves the hop between Cloudflare and your server unencrypted. Set CLOUDFLARE_ZONE_ID and CLOUDFLARE_API_TOKEN in .env to have Conzent purge the edge cache automatically when scripts change.
Put stack changes in docker-compose.override.yml, never in docker-compose.yml — updates reset tracked files, and the override file is left alone.
4. Restart and confirm
docker compose up -d
docker compose exec app php bin/oci health
Load https://consent.example.com. You should get the login page over a valid certificate.
5. Regenerate the consent scripts
This is the step that is easy to miss and produces the strangest symptoms if you do.
Each site's script lives at public/sites_data/{site_key}/script.js and contains absolute URLs built from APP_URL at generation time — the API endpoint it posts consent to, the CSS it pulls, the logo paths. Changing APP_URL does not rewrite scripts that already exist. Until you regenerate, banners on your customers' sites keep calling the old host: consent is recorded against the wrong origin, or fails outright once the old address stops answering.
docker compose exec app php bin/oci scripts:regenerate
6. Update the embed snippet on your websites
The snippet shown in the dashboard now points at the new domain:
<script async src="https://consent.example.com/c/consent.js" data-key="YOUR_SITE_KEY"></script>
If you are moving a live install, keep the old hostname resolving and proxying to the new one until you have swapped every embed. The loader is the entry point for the whole banner — a broken src means no consent banner at all.
7. Verify end to end
curl -sSI https://consent.example.com/c/consent.js | head -1
curl -s https://consent.example.com/sites_data/YOUR_SITE_KEY/version.json
Then load a page that embeds the banner, accept consent, and confirm the entry appears under Consent Logs. That single round trip exercises DNS, TLS, the loader, the generated script, and the API path together.
Troubleshooting
| Symptom | Cause |
|---|---|
| Banner does not appear | The embed still points at the old domain, or scripts were never regenerated |
| Consent logs stopped after the move | The generated script is posting to the old API path — run scripts:regenerate |
| Every consent log shows the same IP | The proxy is not forwarding X-Forwarded-For |
Reset emails link to localhost | APP_URL not updated, or containers not restarted after editing .env |
| Mixed-content warnings | APP_URL is http:// while the site is served over HTTPS |
| Certificate never issues | Port 80 is still held by another service |
| Compose edits vanished | An update reset tracked files — move them to docker-compose.override.yml |
The full reference lives with the code: docs/custom-domain.md on GitHub.