Add Snacks app: automated video library encoder with hardware acceleration (#6)
Co-authored-by: Joachim Friberg <joachim.friberg@ip-solutions.se> Reviewed-on: phirna/zima-apps#6
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
# Snacks
|
||||
|
||||
Automated video library encoder with hardware acceleration (NVENC, QSV, VAAPI, AMF).
|
||||
|
||||
## Purpose
|
||||
|
||||
Snacks batch-transcodes video libraries using FFmpeg with hardware acceleration.
|
||||
It monitors directories, skips already-encoded files, retries with fallbacks, and supports distributed cluster encoding across multiple ZimaOS nodes.
|
||||
|
||||
## Port
|
||||
|
||||
- `6767/tcp` — Web UI at `http://localhost:6767`
|
||||
|
||||
## Volumes
|
||||
|
||||
| Host path | Container path | Description |
|
||||
|---|---|---|
|
||||
| `/DATA/AppData/$AppID/media` | `/app/work/uploads` | Media library — source files to encode |
|
||||
| `/DATA/AppData/$AppID/logs` | `/app/work/logs` | Transcoding logs |
|
||||
| `/DATA/AppData/$AppID/config` | `/app/work/config` | Settings and SQLite database |
|
||||
|
||||
## Hardware Acceleration
|
||||
|
||||
Snacks uses GPU encoding via `/dev/dri`:
|
||||
|
||||
| Driver | Codecs | Devices |
|
||||
|---|---|---|
|
||||
| VAAPI (Linux) | H.265, H.264 | Intel iHD/i965, AMD VAAPI |
|
||||
| QSV (Intel) | H.265, H.264 | Intel Quick Sync Video |
|
||||
| NVENC (NVIDIA) | H.265, H.264 | NVIDIA GPUs via CUDA |
|
||||
| AMF (AMD) | H.265, H.264 | AMD GPUs |
|
||||
|
||||
Auto-detection runs on first encode and picks the best available encoder.
|
||||
|
||||
## Cluster Mode
|
||||
|
||||
Snacks supports distributed encoding across multiple ZimaOS nodes.
|
||||
|
||||
- Nodes discover each other via UDP broadcast on the LAN
|
||||
- One instance acts as coordinator; others are workers
|
||||
- Jobs are assigned automatically; failed nodes are re-assigned
|
||||
- A shared secret authenticates intra-cluster communication
|
||||
|
||||
**UDP broadcast requirement**: Cluster mode requires `network_mode: host` — bridge mode blocks LAN broadcast discovery, making nodes invisible to each other.
|
||||
|
||||
## Health Check
|
||||
|
||||
`http://localhost:6767/Home/Health` — returns HTTP 200 when the backend is ready.
|
||||
|
||||
## Privilegier och säkerhet
|
||||
|
||||
Aktiva säkerhetsinställningar i denna app:
|
||||
|
||||
- `security_opt: ["no-new-privileges:true"]`
|
||||
- `cap_drop: ["ALL"]`
|
||||
- `privileged: true`
|
||||
- `network_mode: host`
|
||||
- Device mount: `/dev/dri:/dev/dri`
|
||||
|
||||
Motivering:
|
||||
|
||||
- `no-new-privileges:true` och `cap_drop: ["ALL"]` kompenserar med lägsta möjliga capability-yta.
|
||||
- Isolerad data-path under `/DATA/AppData/$AppID/...`.
|
||||
|
||||
## Säkerhetsavvikelser
|
||||
|
||||
### 1. `network_mode: host`
|
||||
|
||||
**Varför det behövs:**
|
||||
|
||||
- Snacks cluster nodes discover each other via UDP broadcast on the local network.
|
||||
- Bridge mode only forwards unicast traffic; broadcast packets never reach other nodes.
|
||||
- Without host networking, cluster mode is non-functional.
|
||||
|
||||
**Alternativ som utvärderats:**
|
||||
|
||||
- Bridge mode with port exposure: broadcasts are not forwarded by the Docker bridge.
|
||||
- Static IP configuration: requires manual node addressing and is error-prone.
|
||||
- Multicast DNS (mDNS): not supported by Docker bridge in all deployments.
|
||||
|
||||
**Risker:**
|
||||
|
||||
- Container has full access to all host ports.
|
||||
- No network isolation between Snacks and other services on the host.
|
||||
- If the container is compromised, the attacker has host network access.
|
||||
|
||||
**Riskreducering:**
|
||||
|
||||
- `cap_drop: ["ALL"]` minimizes syscall surface.
|
||||
- `no-new-privileges:true` prevents privilege escalation.
|
||||
- No sensitive host directories are mounted beyond the app-specific volumes.
|
||||
|
||||
---
|
||||
|
||||
### 2. `privileged: true`
|
||||
|
||||
**Varför det behävs:**
|
||||
|
||||
- `/dev/dri` (Direct Rendering Infrastructure) is required for VAAPI/QSV hardware acceleration.
|
||||
- On standard Linux, this device is accessible without privileged mode if the user is in the `video` or `render` group.
|
||||
- ZimaOS does not reliably provide these groups in the container runtime context, making `privileged: true` the only reliable way to grant device access.
|
||||
|
||||
**Alternativ som utvärderats:**
|
||||
|
||||
- `security_opt: ["apparmor:..."]` with specific `/dev/dri` access: not reliably portable across ZimaOS kernel configurations.
|
||||
- Pre-create device nodes with specific permissions: does not work dynamically when the device appears.
|
||||
- Skip hardware acceleration (software encoding only): defeats the primary purpose of the app.
|
||||
|
||||
**Risker:**
|
||||
|
||||
- Container has full root capabilities on the host.
|
||||
- If container is compromised, attacker has theoretical access to all host resources.
|
||||
- Hardware acceleration devices can be accessed directly.
|
||||
|
||||
**Riskreducering:**
|
||||
|
||||
- `cap_drop: ["ALL"]` drops all capabilities even when privileged.
|
||||
- Only the specific `/dev/dri` device is mounted; no other host devices.
|
||||
- Data volumes are scoped to `/DATA/AppData/$AppID/...`.
|
||||
|
||||
---
|
||||
|
||||
### 3. Device mount: `/dev/dri:/dev/dri`
|
||||
|
||||
**Varför det behövs:**
|
||||
|
||||
- VAAPI and QSV hardware encoding require direct access to the GPU render nodes in `/dev/dri`.
|
||||
- Without this mount, FFmpeg falls back to software encoding which is 10–50x slower on 4K content.
|
||||
|
||||
**Alternativ som utvärderats:**
|
||||
|
||||
- Specific device nodes (e.g., `/dev/dri/renderD128`): device names can vary by driver version and host kernel.
|
||||
- No hardware acceleration: software fallback is too slow for practical use.
|
||||
|
||||
**Risker:**
|
||||
|
||||
- The container can enumerate and use all graphics devices on the host.
|
||||
- On multi-user systems, other users' GPU resources may be accessible.
|
||||
|
||||
**Riskreducering:**
|
||||
|
||||
- `privileged: true` combined with `cap_drop: ["ALL"]` ensures the container cannot load additional kernel modules or escalate privileges.
|
||||
- Only the render nodes are exposed; no other host devices are passed through.
|
||||
@@ -0,0 +1,103 @@
|
||||
name: snacks
|
||||
|
||||
services:
|
||||
snacks:
|
||||
image: derekshreds/snacks-docker:2.3.1
|
||||
container_name: snacks
|
||||
restart: unless-stopped
|
||||
deploy:
|
||||
resources:
|
||||
reservations:
|
||||
memory: 1G
|
||||
|
||||
environment:
|
||||
- TZ=Europe/Stockholm
|
||||
- PUID=1000
|
||||
- PGID=1000
|
||||
- ASPNETCORE_ENVIRONMENT=Production
|
||||
- SNACKS_WORK_DIR=/app/work
|
||||
- FFMPEG_PATH=/usr/lib/jellyfin-ffmpeg/ffmpeg
|
||||
- FFPROBE_PATH=/usr/lib/jellyfin-ffmpeg/ffprobe
|
||||
|
||||
network_mode: host
|
||||
|
||||
volumes:
|
||||
- type: bind
|
||||
source: /DATA/AppData/$AppID/media
|
||||
target: /app/work/uploads
|
||||
- type: bind
|
||||
source: /DATA/AppData/$AppID/logs
|
||||
target: /app/work/logs
|
||||
- type: bind
|
||||
source: /DATA/AppData/$AppID/config
|
||||
target: /app/work/config
|
||||
|
||||
devices:
|
||||
- /dev/dri:/dev/dri
|
||||
|
||||
privileged: true
|
||||
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
|
||||
cap_drop:
|
||||
- ALL
|
||||
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:6767/Home/Health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 40s
|
||||
|
||||
x-casaos:
|
||||
envs:
|
||||
- container: TZ
|
||||
description:
|
||||
en_US: Timezone, for example Europe/Stockholm
|
||||
- container: PUID
|
||||
description:
|
||||
en_US: User ID for filesystem permissions
|
||||
- container: PGID
|
||||
description:
|
||||
en_US: Group ID for filesystem permissions
|
||||
- container: FFMPEG_PATH
|
||||
description:
|
||||
en_US: "FFmpeg binary path (default: /usr/lib/jellyfin-ffmpeg/ffmpeg). Use /usr/bin/ffmpeg on systems without jellyfin-ffmpeg."
|
||||
- container: FFPROBE_PATH
|
||||
description:
|
||||
en_US: "FFprobe binary path (default: /usr/lib/jellyfin-ffmpeg/ffprobe). Use /usr/bin/ffprobe on systems without jellyfin-ffmpeg."
|
||||
ports:
|
||||
- container: "6767"
|
||||
description:
|
||||
en_US: Web UI port
|
||||
volumes:
|
||||
- container: /app/work/uploads
|
||||
description:
|
||||
en_US: Media library — source files to be encoded
|
||||
- container: /app/work/logs
|
||||
description:
|
||||
en_US: Transcoding logs directory
|
||||
- container: /app/work/config
|
||||
description:
|
||||
en_US: Application configuration and SQLite database
|
||||
|
||||
x-casaos:
|
||||
architectures:
|
||||
- amd64
|
||||
main: snacks
|
||||
category: phirna
|
||||
author: Joachim Friberg
|
||||
developer: Joachim Friberg
|
||||
icon: https://cdn.simpleicons.org/snacks
|
||||
tagline:
|
||||
en_US: Automated video library encoder with hardware acceleration
|
||||
description:
|
||||
en_US: >-
|
||||
Batch transcode your video library with hardware acceleration (NVENC, QSV, VAAPI, AMF).
|
||||
Monitors directories, skips already-encoded files, and supports distributed cluster encoding.
|
||||
Web UI at http://localhost:6767
|
||||
title:
|
||||
en_US: Snacks
|
||||
index: /
|
||||
port_map: "6767"
|
||||
Reference in New Issue
Block a user