Custom Domains
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 domain | Remote Access | |
|---|---|---|
| The name | Yours, registered and paid for by you | Assigned by Cardinal under connect.cardinalapps.host |
| DNS | You point a record at your host | Managed by Cardinal |
| TLS certificate | Yours to obtain and renew | Issued and renewed by Cardinal, delivered to your server |
| Reachability | Your host is already on the internet | Works from behind NAT, with automatic port forwarding and a relay when nothing else gets through |
| Cost | Whatever your host and domain cost | Included 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
Arecord (andAAAAif 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
24900in 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:
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:
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
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:
- Set the ingress target port to
24900. - Add your custom domain in the platform's dashboard, and create the DNS records it asks for.
- Let the platform issue and renew the certificate.
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.
| Variable | Why |
|---|---|
SECURE_COOKIES=true | Your 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_SECRET | Session 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_PORT | The 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:
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?