This repository contains the Infrastructure-as-Code (IaC) for deploying a self-hosted media stack on an Intel N100 Mini-PC running Debian 13 "Trixie". The architecture emphasizes zero-trust micro-segmentation, atomic hardlinks for media management, and hardware-accelerated transcoding.
This project implements a robust, spec-driven media server solution designed for resilience and efficient media processing. It leverages Docker Compose for service orchestration and Ansible for host provisioning, adhering to strict architectural and security guidelines.
- Operating System: Debian 13 "Trixie" (The Host)
- Hardware: Intel N100 Mini-PC (Intel QuickSync supported)
- Container Engine: Docker
- Orchestration: Docker Compose
- Provisioning: Ansible
- Firewall: UFW (Uncomplicated Firewall)
- Ingress: Cloudflare Tunnel (Zero-Trust)
- Spec-Driven Development: All infrastructure is defined and implemented based on detailed specifications (
CONSTITUTION.md,PRODUCT_SPECIFICATION.md,ARCHITECTURE.md). - Idempotency: All deployment scripts and configurations are idempotent, ensuring consistent state regardless of how many times they are executed.
- Zero Root Execution: Docker containers never run as root, utilizing a dedicated
mediasvcsystem user with UID/GID 5000. - State vs. Compute Isolation: Strict separation between stateless compute (Docker containers) and stateful data (volume mounts). All state lives in
/opt/mediastack/appdata/. - Zero-Trust Micro-segmentation: Network traffic is strictly controlled between services using isolated bridge networks.
- No Port Forwarding: All external access is routed through a secure Cloudflare Tunnel.
The media stack consists of the following services, categorized by their Bounded Contexts:
- Delivery:
- Jellyfin: Media server with Intel QuickSync hardware transcoding.
- Acquisition:
- Radarr: Movie collection manager.
- Sonarr: TV show collection manager.
- Prowlarr: Indexer manager for Usenet/Torrents.
- Recyclarr: Automatically syncs TRaSH Guides quality profiles.
- Processing:
- SABnzbd: Usenet download client (Resource limited to 2 CPUs / 2GB RAM).
- Tdarr: Automated media transcoding (Intel QuickSync on-host).
- On-Demand GPU Workload Offloading: A transient laptop node with an AMD Radeon RX 7600M XT can join the Tdarr cluster over NFSv4 + the Tdarr control plane to offload GPU encoding on demand. The export and control port are strictly scoped to the auto-detected LAN subnet via UFW with zero-trust UID/GID squashing (see Distributed Tdarr Node).
- Media Request & Identity & Access:
- Seerr: UI for media discovery and requests.
- Ingress:
- Cloudflared: Establish secure tunnel for zero-trust external access.
- Dashboards & Maintenance:
- Homepage: Centralized dashboard for all services.
- Watchtower: Automatic Docker image updates.
- Docker Socket Proxy: Secure abstraction for the Docker socket.
The system mandates a single root directory (/opt/mediastack/) for all media data to enable Atomic Hardlinks (instant, zero-space moves).
/opt/mediastack/
├── docker-compose.yml
├── .env
├── appdata/ <-- Config state (Must reside on fast SSD)
│ ├── jellyfin/
│ ├── radarr/
│ └── ...
└── data/ <-- The Media Payload (Resides on High-Capacity Drive)
├── usenet/ <-- SABnzbd active downloads
└── media/
├── movies/ <-- Final destination for Radarr
└── tv/ <-- Final destination for Sonarr
Before running Ansible, the Intel N100 Mini-PC must be manually prepared:
- Install Debian 13 "Trixie": Use a minimal netinst image. Ensure the SSH Server is selected during the "Software selection" step.
- Create Ansible User: Log in as root and create the user that Ansible will use:
useradd -m -s /bin/bash ansible passwd ansible usermod -aG sudo ansible
- Configure SSH Key Access: From your control machine, copy your public SSH key to the host. Password authentication will be disabled by the playbook.
ssh-copy-id ansible@<host-ip>
- Identify Media Drive: Plug in your high-capacity drive and find its device path:
lsblk # Note the path, e.g., /dev/sdb. This is used in the inventory. - Verify Hardware Acceleration: Ensure the Intel GPU node is present:
ls -l /dev/dri/renderD128
Ensure your control machine has Ansible installed along with the required collections:
ansible-galaxy collection install community.general ansible.posix community.docker ansible.utilsPython dependency: the
ansible.utilscollection'sipaddrfilter (used for automatic LAN subnet detection) requires thenetaddrPython package in your Ansible control environment (pip install netaddr, orpipx inject ansible-core netaddrwhen Ansible runs from pipx).
-
Clone the repository:
git clone https://github.com/MelvinLoos/mediacenter.git cd mediacenter -
Inventory: Edit
ansible/inventory/hosts.iniand ensuremedia_drive_devicematches your drive path from step 1.4.Bootstrapping (Day 1 vs. Day 2)
- Day 1 (Bootstrap): The Host does not run Avahi/mDNS yet, so
mediacenter.localcannot resolve. Target its DHCP address directly for the first run, e.g.ansible-playbook -i "192.168.1.150," -u ansible ansible/playbooks/provision_host.yml -K. - Day 2 (Normal Ops): The playbook sets the role-based hostname and installs Avahi. Afterwards, point the inventory at the mDNS name — resilient to DHCP address changes:
[the_host] mediacenter.local ansible_user=ansible media_drive_device=/dev/sdb
- Day 1 (Bootstrap): The Host does not run Avahi/mDNS yet, so
-
Environment Variables: Copy
.env.exampleto.envand set yourTZandCLOUDFLARED_TOKEN.cp .env.example .env
Run the master provision playbook. This handles system updates, user creation, filesystem formatting, security hardening, and starts the media stack.
ansible-playbook -i ansible/inventory/hosts.ini ansible/playbooks/provision_host.yml -KNote on Ingress: By default, the Cloudflare Tunnel container is in the
ingressprofile. To ensure it starts if you run compose manually, usedocker compose --profile ingress up -d. The Ansible playbook handles this automatically.
Once deployed, the following services are available on The Host:
| Service | Port | Bounded Context | Ingress Access via Tunnel |
|---|---|---|---|
| Homepage | 80 | Dashboard | No (Local Only) |
| Seerr | 5055 | Request / Identity | Yes |
| Jellyfin | 8096 | Delivery | Yes |
| Radarr | 7878 | Acquisition | No |
| Sonarr | 8989 | Acquisition | No |
| Prowlarr | 9696 | Indexers | No |
| SABnzbd | 8080 | Processing | No |
| Tdarr Server | 8266 | Processing | No (Local /24 only — laptop GPU node control plane) |
This project uses Molecule with Testinfra to validate the infrastructure against the specifications.
To run tests:
cd mediacenter
molecule testTests verify:
- Non-root execution (
mediasvcownership) - UFW firewall rules (No leakage of internal ports)
- Filesystem hierarchy requirements
- Atomic hardlink capability (shared mount point)
This project is licensed under the MIT License - see the LICENSE file for details.