Use an AI agent
Have your AI agent handle this for you with these skills.
Install
Update
Install with Proxmox
This guide uses a feature that is new in Proxmox VE 9.1: containers created directly from a Docker-style image. There is no operating system layer between Proxmox and Cardinal. It is not an LXC with Debian inside, and it is not a virtual machine. Proxmox calls these application containers and labels them a technology preview, which means the feature works but may still change between Proxmox releases. For a simpler setup, use Install with Docker inside a virtual machine.
Because the Proxmox feature it relies on is still a preview, the install script and this page will change as Proxmox does. Fetch the page fresh each time rather than working from a saved copy, and expect the commands to differ from one Proxmox release to the next.
This install touches more of Proxmox than a Docker install does: storage, networking, device passthrough, and the container lifecycle. The Use an AI agent box on this page holds a skill that performs the whole installation for you, and a second one that performs updates. They are the quickest route to a working server, and to keeping it current.
Cardinal Media Server can run on Proxmox VE as an application container: an LXC container created directly from the official cardinalapps/media-server image. There is no Docker inside the container and no guest operating system to update. The container is the image, and Proxmox manages it like any other guest.
An install script does the whole setup in one command. It creates the container, gives it a dedicated volume for its configuration and database, binds your media read-only, passes the render device for hardware transcoding when one exists, and verifies that the server answers.
┌───────────────────────────┐ ┌───────────────────────────┐
│ Docker on a VM │ │ Proxmox application │
│ │ │ container │
│ cardinal/media-server │ │ │
│ ───────────────────── │ │ cardinal/media-server │
│ Docker Engine │ │ ───────────────────── │
│ ───────────────────── │ │ LXC (Proxmox) │
│ Guest OS + kernel │ │ ───────────────────── │
│ ───────────────────── │ │ Proxmox host kernel │
│ Proxmox host kernel │ │ │
└───────────────────────────┘ └───────────────────────────┘
fig. 1 — the layers a request passes through
A running Cardinal application container uses around 350 MB of memory and 2.2 GB of disk for the image.
Requirements
- Proxmox VE 9.1 or newer. Application containers from OCI images arrived in Proxmox VE 9 and are marked as a technology preview by Proxmox.
- skopeo on the host, which Proxmox uses to pull images from registries:
apt install skopeo. - A root shell on the Proxmox host. The script runs as root and uses
pct,pvesmandpvesh. - Storage for an 8 GB root disk and a 32 GB configuration volume. Both are thin-provisioned on LVM-thin and ZFS.
- Your media, reachable on the host as directories. See the next section.
Prepare your media
Cardinal reads your media in place, through read-only bind mounts from the Proxmox host into the container. Before installing, make each media folder available on the host:
- Media on a local disk of the host needs nothing more than its path.
- Media on a NAS must be mounted on the host first, for example with an entry in
/etc/fstab:
192.168.1.10:/volume1/Music /mnt/media/music nfs vers=3,ro,nosuid,nodev,noexec,hard,_netdev 0 0
Use one folder per media type. Cardinal turns top-level folders into Libraries, so pick the highest-level folder that holds each type.
The install script checks only that each path exists. It never lists or reads the contents of your media folders.
Install
Run this on the Proxmox host as root, replacing the container ID and the media paths with your own:
bash <(curl -fsSL https://help.cardinalapps.io/scripts/install-cardinal-proxmox.sh) \
--ctid 200 \
--media /mnt/media/music:/music \
--media /mnt/media/movies:/movies
The script prints its plan and waits for confirmation before changing anything:
Cardinal Media Server on Proxmox VE — plan
Container 200 (cardinal-stable-sqlite, unprivileged application container)
Image docker.io/cardinalapps/media-server:stable
Resources 4 cores, 4096 MB RAM, 512 MB swap
Disks 8 GB root + 32 GB /config on local-lvm (config volume backed up)
Network vmbr0, ip=dhcp, onboot=1, firewall=on
GPU /dev/dri/renderD128 (hardware transcoding)
Database SQLite on the /config volume
Mount guard on
Logs /var/log/cardinal/ct-200.log
Media
/mnt/media/music -> /music (read-only)
/mnt/media/movies -> /movies (read-only)
Proceed? [y/N]
It then pulls the image into the template storage, creates and starts the container, and waits for the server to answer on port 24900. The summary at the end includes the First Time Setup URL:
Cardinal Media Server — installed ✔
Container 200 (cardinal-stable-sqlite) at 192.168.1.50
Image docker.io/cardinalapps/media-server:stable
Database SQLite on the /config volume
Media /mnt/media/music -> /music
Media /mnt/media/movies -> /movies
GPU /dev/dri/renderD128
First Time Setup http://192.168.1.50:24900/admin
Open that URL to complete the first-time setup, then index your media.
Every container listens on port 24900 at its own address. Running several Cardinal Media Servers on one host means several containers, each with its own address.
Options
| Option | Default | Description |
|---|---|---|
--ctid <id> | required | Container ID. Must be unused. |
--hostname <name> | cardinal-<channel>-<db> | Hostname of the container. |
--cores <n> | 4 | CPU limit. |
--memory <MB> | 4096 | Memory limit. |
--swap <MB> | 512 | Swap limit. |
--rootfs-size <GB> | 8 | Root disk, which holds the image. |
--config-size <GB> | 32 | The /config volume: database, artwork, thumbnails. |
--storage <name> | local-lvm | Storage for both disks. |
--template-storage <name> | local | Storage that holds pulled images. |
--bridge <name> | vmbr0 | Network bridge. |
--ip dhcp|<cidr> | dhcp | IPv4 address of the container. |
--gateway <ip> | Required with a static --ip. | |
--mac <address> | random | Fixed MAC address, useful for DHCP reservations. |
--onboot 0|1 | 1 | Start the container when the host boots. |
--firewall on|off | auto | Per-container Proxmox firewall. On when the datacenter firewall is enabled. |
--media <host>:<ct> | Bind a host directory read-only. Repeatable. <ct> is /music, /photos, /movies or /tv. | |
--guard-mounts on|off | auto | Refuse to start unless every media path is a mounted filesystem. On when any media path is a mount point. |
--no-gpu | Do not pass the render device even if the host has one. | |
--channel stable|beta | stable | Release channel. |
--db sqlite|postgres | sqlite | Database. See PostgreSQL. |
--env KEY=VALUE | Extra environment variable for the server. Repeatable. | |
--pull | Re-download the image even if it is already cached. | |
--yes | Skip the confirmation prompt. | |
--dry-run | Print the plan and exit. |
Options for PostgreSQL are listed in the next section.
PostgreSQL
With --db postgres the script runs PostgreSQL 16 in a second application container and connects Cardinal to it. The database container has its own data volume and starts before Cardinal when the host boots.
bash <(curl -fsSL https://help.cardinalapps.io/scripts/install-cardinal-proxmox.sh) \
--ctid 200 --db postgres --db-ctid 201 \
--media /mnt/media/music:/music
| Option | Default | Description |
|---|---|---|
--db-ctid <id> | required | Container ID for PostgreSQL. |
--db-hostname <name> | <hostname>-db | Hostname of the PostgreSQL container. |
--db-data-size <GB> | 16 | Its data volume. |
--zone <name> | cardinal | SDN zone for the private network. |
--vnet <name> | cardinal | SDN VNet the database lives on. |
--vnet-subnet <cidr> | 10.20.0.0/24 | Private subnet. Shifted automatically if it overlaps a network the host already knows. |
--postgres-database <name> | cardinal | Database name. |
--postgres-host <host> | Use an existing PostgreSQL server instead. No database container is created. | |
--postgres-port <n> | 5432 | |
--postgres-user <name> | cardinal | |
--postgres-password <secret> | generated | Required with --postgres-host. |
The private network
The database is not placed on your LAN. The script creates a Proxmox SDN zone of type simple and a VNet in it, which is an isolated bridge on the host with no uplink. The PostgreSQL container's only interface is on that VNet. The Cardinal container keeps its LAN interface and gets a second one on the VNet.
LAN (vmbr0) VNet "cardinal" (10.20.0.0/24)
─────┬────────────────── ────────┬────────────────
│ │
┌────┴─────────┐ 10.20.0.10 ┌────────── ─────┴──┐
│ Cardinal ├──────────────┤ PostgreSQL │ 10.20.0.2
│ CT 200 │ │ CT 201 │
└──────────────┘ └──────────────────┘
fig. 2 — the database is reachable only from Cardinal
Nothing outside the host can reach the database, and the generated password is stored only in the two container configurations under /etc/pve/lxc/.
Sharing one PostgreSQL between several Cardinal Media Servers
Pass the ID of the existing PostgreSQL container as --db-ctid and a different --postgres-database. The script reuses the container, reads its credentials from its configuration, and creates the new database on it if it does not exist:
bash <(curl -fsSL https://help.cardinalapps.io/scripts/install-cardinal-proxmox.sh) \
--ctid 210 --channel beta --db postgres --db-ctid 201 --postgres-database cardinal_beta \
--media /mnt/media/music:/music
Bringing your own PostgreSQL
Pass --postgres-host, --postgres-password and, if they differ from the defaults, --postgres-port, --postgres-user and --postgres-database. No database container and no private network are created. The database must exist and the user must own it.
Hardware transcoding
When the host has an Intel or AMD GPU with a render node at /dev/dri/renderD128, the script passes it into the container. Confirm on the host with ls /dev/dri. If the device is missing, check that the GPU's kernel driver is loaded and that the device is not bound to vfio-pci for passthrough to a virtual machine.
Cardinal Media Server does not currently use hardware acceleration for transcoding. Audio is transcoded on the CPU. The device is passed so that it is available to the server when hardware transcoding is supported.
After installation
| Task | Command on the Proxmox host |
|---|---|
| Server log | tail -f /var/log/cardinal/ct-200.log |
| Shell inside the container | pct enter 200 |
| Restart | pct reboot 200 |
| Stop and start | pct stop 200, pct start 200 |
| Firewall rules | /etc/pve/firewall/200.fw |
| Environment variables | /etc/pve/lxc/200.conf, one lxc.environment.runtime: line per variable, then pct reboot 200 |
| Update | See Updating Cardinal |
The server's output goes to /var/log/cardinal/ct-<id>.log on the host and is rotated weekly. There is no SSH server in the container; use pct enter or pct exec.
The per-container firewall allows port 24900 and ICMP in, and everything out. Edit the .fw file or use the container's Firewall panel in the Proxmox web interface to change that.
Two behaviours are provided by a hookscript the install attaches to the container, stored under the template storage's snippets directory:
- Mount guard. If any media path is not a mounted filesystem when the container starts, the start is refused. This prevents the server from seeing empty libraries when a NAS is unavailable. Start the container again once the mount is back.
- Crash restart. If the server process exits unexpectedly, the container is restarted after five seconds, at most five times in ten minutes. A normal
pct stoporpct shutdowndoes not trigger a restart.
The install also orders guest startup after network filesystems, so containers do not start before NAS mounts at boot.
Updating Cardinal
The image is the container's root disk, so an update replaces that disk and keeps everything else: the /config volume, media mounts, network, device, firewall and hookscript. The script does it in one command:
bash <(curl -fsSL https://help.cardinalapps.io/scripts/install-cardinal-proxmox.sh) --update 200
It pulls the latest image for the container's release channel, builds the new root from it, swaps it in, restarts the container once, and waits for the server to answer. If the container already runs the latest build, it says so and changes nothing.
The previous root is kept until the next update, so a bad build can be undone with one command:
bash <(curl -fsSL https://help.cardinalapps.io/scripts/install-cardinal-proxmox.sh) --rollback 200
Rollback restores the previous root and configuration and restarts the container once. Only the last update can be undone.
Both commands print a plan and ask for confirmation. Add --yes to skip the prompt, for example from a scheduled job:
# /etc/cron.weekly/cardinal-update
#!/bin/sh
bash <(curl -fsSL https://help.cardinalapps.io/scripts/install-cardinal-proxmox.sh) --update 200 --yes
Before updating, check the release notes for your new version. Not every release has notes.
Backups
The configuration volume is created with the backup flag, so a Proxmox backup job that includes the container captures the database, artwork and settings. The root disk is included too and can be excluded without loss, since it is only the image.
Add the container to a backup job under Datacenter → Backup in the Proxmox web interface.
Secure Backups inside Cardinal works independently of Proxmox backups.
Uninstall
pct stop 200
pct destroy 200 --purge
rm /etc/pve/firewall/200.fw /var/lib/vz/snippets/cardinal-ct-200.sh /var/log/cardinal/ct-200.log* /var/lib/cardinal/ct-200.*
--purge removes both volumes, including the configuration volume. The PostgreSQL container, if any, is separate and is removed the same way. The SDN zone and VNet stay in place; delete them under Datacenter → SDN once no container uses them.
Troubleshooting
| Symptom | Cause and fix |
|---|---|
refusing to start CT with is not mounted in the task log | The mount guard found a media path that is not mounted. Mount it on the host, then start the container. |
| The container keeps restarting | The server is crashing. Read /var/log/cardinal/ct-<id>.log. After five crashes in ten minutes the restarts stop. |
did not get an IPv4 address | No DHCP answer on the bridge within two minutes. Check the bridge and the DHCP server, or use --ip with a static address and --gateway. |
| The server does not answer on port 24900 | Read the log. On the PostgreSQL path, confirm the database container is running with pct status <db ctid>. |
This Proxmox VE version cannot run application containers | Proxmox VE is older than 9.1. Upgrade, or use Install with Docker in a virtual machine. |
Install 'skopeo' to pull OCI images | Run apt install skopeo on the host. |
ID <n> is already in use | Pick another --ctid. IDs are shared between containers and virtual machines. |
Was this article helpful?