Skip to main content

Custom Domains

briiianClaude
Authored by briiian and Claude · Last updated Written with AI by reading the source code, and edited by hand
self-hosted

Some self-hosted setups allow you to use a custom domain name with your Cardinal Media Server. This type of setup is sometimes also called Bring Your Own Domain (BYOD). If you run your Media Server on a host that is already on the public internet like a VPS, a dedicated server, or a container platform, then you can serve it at a domain you own, with a certificate you control, and end up with something like https://my-private-cardinal.com.

This is a DIY setup that does not use Cardinal's built-in Remote Access features. No account, no subscription, or any Cardinal other cloud service is needed as long as you bring your own domain, DNS, SSL certificate, and of course your Cardinal Media Server.

How this differs from Remote Access

Both get you an HTTPS address that works from anywhere. The difference is who provides the pieces:

Custom domainRemote Access
The nameYours, registered and paid for by youAssigned by Cardinal under connect.cardinalapps.host
DNSYou point a record at your hostManaged by Cardinal
TLS certificateYours to obtain and renewIssued and renewed by Cardinal, delivered to your server
ReachabilityYour host is already on the internetWorks from behind NAT, with automatic port forwarding and a relay when nothing else gets through
CostWhatever your host and domain costIncluded with your Cardinal subscription

The two are not alternatives, and running one does not interfere with the other. A server with Remote Access enabled keeps its Cardinal-assigned address while also answering on your domain.

The rule of thumb: a custom domain suits a server that is already publicly reachable and an operator comfortable maintaining a proxy and certificates. Remote Access suits everything else, and in particular a server at home behind a router, which a custom domain alone cannot solve.

What you need

  • A host reachable from the internet — a VPS, a dedicated server, or a container platform. A home connection behind NAT is not a good fit for this guide; use Remote Access there instead.
  • A domain, with a DNS A record (and AAAA if you have IPv6) pointing at the host.
  • Ports 80 and 443 open to the internet, for your proxy.
  • Your Media Server's port, which is 24900 in the published container image.

Terminating TLS

Something in front of the Media Server has to hold the certificate and answer HTTPS. That is either your platform's ingress or a reverse proxy you run yourself.

Either way, one detail matters more than the rest:

Send plain HTTP to the Media Server

Your proxy's connection to the Media Server should be plain http:// on port 24900, not HTTPS.

The Media Server answers both protocols on that one port and decides per connection which it received. Plain connections go to the app. TLS connections go to the Remote Access listener, which answers with Cardinal's certificate for a Cardinal hostname — not for yours. A proxy that dials it over HTTPS is talking to the wrong thing.

Keep that hop private. When the proxy is on the same host, publish the port to loopback only, so nothing but the proxy can reach it:

ports:
- 127.0.0.1:24900:24900

AI GeneratedUse with caution.

Fig. 2 — publishing the Media Server's port to the proxy alone

Caddy

The shortest path. Caddy obtains and renews the certificate on its own:

Caddyfile
media.example.com {
reverse_proxy 127.0.0.1:24900 {
flush_interval -1
}
}

AI GeneratedUse with caution.

Fig. 3 — a complete Caddy configuration

flush_interval -1 turns off response buffering, which the Media Server's live event stream needs. Caddy already does this for event streams on its own; setting it explicitly costs nothing and documents the requirement.

nginx

/etc/nginx/sites-available/cardinal
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name media.example.com;

ssl_certificate /etc/letsencrypt/live/media.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/media.example.com/privkey.pem;

location / {
proxy_pass http://127.0.0.1:24900;
proxy_http_version 1.1;
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;
}

# The live event stream. Buffering here would hold events until the buffer
# filled, which is never — the connection is meant to stay open.
location /events/subscribe {
proxy_pass http://127.0.0.1:24900;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 24h;
}
}

AI GeneratedUse with caution.

Fig. 4 — nginx, with the event stream exempted from buffering

nginx buffers proxied responses by default, and that one exemption is the difference between an app that updates live and one that appears frozen.

Traefik

With Traefik discovering containers through Docker labels, no separate file is needed:

labels:
- traefik.enable=true
- traefik.http.routers.cardinal.rule=Host(`media.example.com`)
- traefik.http.routers.cardinal.tls.certresolver=letsencrypt
- traefik.http.services.cardinal.loadbalancer.server.port=24900

AI GeneratedUse with caution.

Fig. 5 — Traefik labels on the Media Server service

Traefik streams responses without buffering, so the event stream needs no special handling.

Platform ingress

On a container platform — Azure Container Apps, Fly.io, Railway, Render and the like — the platform is the proxy. There is no configuration file, and the pattern is the same everywhere:

  1. Set the ingress target port to 24900.
  2. Add your custom domain in the platform's dashboard, and create the DNS records it asks for.
  3. Let the platform issue and renew the certificate.
Give the container persistent storage

The Media Server keeps its database, its identity, and its session signing secret in /config. Container platforms give you an ephemeral filesystem by default, which means every deployment starts a brand new server — empty library, new identity, everyone signed out.

Mount a persistent volume at /config before you put anything real on it. If your platform cannot, set SIGNING_SECRET explicitly so that sessions at least survive a restart.

Settings that matter behind a proxy

The Media Server needs very little adjustment, but these are worth setting deliberately. All of them are environment variables.

VariableWhy
SECURE_COOKIES=trueYour users reach the server over HTTPS, so its cookies should be issued with the stricter settings that HTTPS allows. Leave it off if you are still testing over plain HTTP, or you will not be able to sign in.
SIGNING_SECRETSession tokens are signed with it. One is generated and stored in /config if you do not provide it — fine with a persistent volume, and a problem without one, since a regenerated secret signs everyone out. Set it explicitly on any host with an ephemeral filesystem.
CARDINAL_MEDIA_SERVER_PORTThe port the server listens on, 24900 in the container image. Change it only if you have a reason to, and point your proxy at whatever you choose.

Two habits are worth more than any setting here. First, do not publish the Media Server's own port to the internet once a proxy is in front of it — bind it to loopback, or to a private network the proxy shares. Second, a server on a public domain is found by strangers within days, so go through the Deployment guide before you point a domain at it, in particular disabling the guest account.

A complete example

Media Server and Caddy, on one host:

docker-compose.yaml
services:
cardinal-media-server:
platform: linux/x86_64
image: cardinalapps/media-server:stable
ports:
- 127.0.0.1:24900:24900
environment:
- SECURE_COOKIES=true
volumes:
- cardinal-media-server-data:/config
- /srv/media/music:/music
- /srv/media/photos:/photos
- /srv/media/tv:/tv
- /srv/media/movies:/movies
restart: unless-stopped

caddy:
image: caddy:2
ports:
- 80:80
- 443:443
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy-data:/data
network_mode: host
restart: unless-stopped

volumes:
cardinal-media-server-data:
caddy-data:

AI GeneratedUse with caution.

Fig. 6 — a Media Server published at your own domain

Caddy runs on the host network here so it can reach the Media Server on loopback. On a bridge network instead, drop network_mode, publish the Media Server's port to the compose network rather than to 127.0.0.1, and address it by service name: reverse_proxy cardinal-media-server:24900.

Accounts

Nothing about accounts changes when you serve the server from your own domain. Local accounts behave exactly as they do on any other deployment, the guest account is available if you want it, and roles and capabilities apply the same way.

Was this article helpful?