# Video Ware Deployment Guide This guide covers deploying Video Ware in production using Docker. Video Ware can be deployed in two ways: 1. **Monolithic Container** - Single container with all services (simplest deployment) 2. **Docker Compose** - Separate containers for each service (better for scaling) ## Quick Start ### Prerequisites - Docker 20.10+ and Docker Compose 2.0+ - Access to the container registry where images are published - Required environment variables (see Configuration section) ## Deployment Options ### Option 1: Monolithic Container The monolithic container includes all services (PocketBase, Next.js webapp, Worker, and Nginx) in a single image. This is the simplest deployment option. #### Pull and Run ```bash # Pull the latest image docker pull ghcr.io/make-ware/video-ware:latest # Run the container docker run -d \ --name video-ware \ -p 8888:80 \ -e POCKETBASE_ADMIN_EMAIL=admin@example.com \ -e POCKETBASE_ADMIN_PASSWORD=your-secure-password \ -v ./data:/data \ ghcr.io/make-ware/video-ware:latest ``` The application will be available at: - **Web Application**: http://localhost:8888 - **PocketBase API**: http://localhost:8888/api/ - **PocketBase Admin**: http://localhost:8888/_/ #### Using a Specific Version ```bash # Pull a specific version docker pull ghcr.io/make-ware/video-ware:1.0.0 # Run with version tag docker run -d \ --name video-ware \ -p 8888:80 \ -e POCKETBASE_ADMIN_EMAIL=admin@example.com \ -e POCKETBASE_ADMIN_PASSWORD=your-secure-password \ -v ./data:/data \ ghcr.io/make-ware/video-ware:1.0.0 ``` ### Option 2: Docker Compose (Microservices) Docker Compose deploys each service in its own container, allowing for better scaling and resource management. #### Setup 1. **Create an `.env` file** in the `docker/` directory: ```bash cd docker cat > .env <.amazonaws.com`) | | `STORAGE_S3_FORCE_PATH_STYLE` | `true` to use path-style addressing (required for Garage/MinIO-style stores; default `false`) | #### Application URLs | Variable | Default | Description | |----------|---------|-------------| | `NEXT_PUBLIC_POCKETBASE_URL` | `http://localhost:8090` | Public URL for PocketBase (used by browser) | > [!IMPORTANT] > **`NEXT_PUBLIC_*` is build-time only.** Next.js inlines any `NEXT_PUBLIC_`-prefixed variable into the browser bundle at `docker build` time (see the `ARG`/`ENV NEXT_PUBLIC_POCKETBASE_URL` step in the `builder` stage of the Dockerfile). Setting `NEXT_PUBLIC_POCKETBASE_URL` at **runtime** (via `-e`, `envFrom`, `config.env`, etc.) does **not** change what the browser uses — only the SSR/server-side reads and the worker pick up the runtime value. To change the client value, pass it as `--build-arg NEXT_PUBLIC_POCKETBASE_URL=...` when building the image. > > This is currently harmless for the default same-origin gateway deployment: the image bakes `"/"`, and the gateway proxies `/api` + `/_/` to PocketBase on the same origin, so the relative URL resolves correctly regardless of the runtime value. It only becomes a problem if you serve PocketBase from a **separate hostname**, in which case you must rebuild the image with the correct build-arg. ### Example: Monolithic Container with Full Configuration ```bash docker run -d \ --name video-ware \ -p 8888:80 \ -e POCKETBASE_ADMIN_EMAIL=admin@example.com \ -e POCKETBASE_ADMIN_PASSWORD=secure-password-123 \ -e LOG_LEVEL=warn \ -e WORKER_MAX_RETRIES=5 \ -e GRACEFUL_SHUTDOWN_TIMEOUT=60 \ -v ./data:/data \ ghcr.io/make-ware/video-ware:latest ``` ## Persistent Data You can persist data using one of two methods depending on your needs. ### Option 1: Simple (Single Volume) Map a single host directory (e.g., `./data`) to `/data` in the container. This requires the least configuration and keeps all data in one place. **Internal Structure:** - `/data/pb_data` — PocketBase database (`data.db`) and uploaded files - `/data/storage` — media storage, **shared** by the worker (processing) and the webapp (SSR uploads) - `/data/redis` — Redis append-only file (AOF) for the BullMQ queue (**monolith only**; the bundled Redis persists here) - `/data/tmp` — scratch space for in-flight processing (S3 download staging, ffmpeg temp files). Contents are disposable; stale entries are reaped by the weekly cleanup task. Deliberately on the volume so multi-gigabyte intermediates never land in the container's writable layer (which on hosts like Unraid lives inside the size-capped `docker.img`) **Docker Run:** ```bash docker run -d \ -v ./data:/data \ # ... other options ``` **Docker Compose:** ```yaml services: pocketbase: volumes: - ./data:/data worker: volumes: - ./data:/data ``` ### Option 2: Granular (Separate Volumes) Map individual subdirectories for better control (e.g., keeping the database on fast SSD storage while keeping large video files on cheaper HDD storage). **Docker Run:** ```bash docker run -d \ -v ./data/db:/data/pb_data \ -v ./data/storage:/data/storage \ # ... other options ``` **Docker Compose:** ```yaml services: pocketbase: volumes: - pb_data:/data/pb_data worker: volumes: - storage:/data/storage volumes: pb_data: storage: ``` ### Data directory & permissions All container processes (PocketBase, the worker, the webapp, and the bundled Redis) run as a single unprivileged user, **`nextjs:nodejs` = UID/GID `1001:1001`**. Everything under `/data` must therefore be owned by — or at least writable by — UID `1001`. This is a stable contract; the images do not run their long-lived processes as root. > **Why this matters:** if `/data` (or a file inside it, such as PocketBase's > `data.db`) is owned by `root` or another UID, PocketBase fails on its first > write with `attempt to write a readonly database (8)` (SQLite `SQLITE_READONLY`), > typically while applying the startup snapshot migration. The fix is always to > make `/data` writable by UID `1001`. How ownership is established depends on the mount type: - **Named volumes (recommended for the split images / Compose).** Docker initializes a fresh named volume from the image's `/data`, which the images pre-create owned by `1001`. Ownership is correct automatically — no setup needed. The split `worker` and `pocketbase` images run as UID `1001` directly. - **Bind mounts (`-v ./data:/data`).** A bind mount keeps the **host** directory's ownership, shadowing the image's. For the split images (which run as UID `1001`), chown the host directory once: ```bash sudo chown -R 1001:1001 ./data ``` - **Monolith image.** The monolith starts as root and runs [`start.sh`](./start.sh), which seeds the PocketBase superuser and then re-asserts ownership of `/data` to `1001` **before** handing off to supervisord (which runs every service as `1001`). This self-heals both bind mounts and named volumes with no manual step. #### Unraid The Unraid template bind-mounts the appdata share (e.g. `/mnt/user/appdata/video-ware`) to `/data` and runs the **monolith** as root, so ownership self-heals on startup — no manual chown is required. If you ever see SQLite locking errors (distinct from the readonly error above), point the appdata share at a cache pool / direct disk path rather than the FUSE `/mnt/user` path, which is a known SQLite-on-network-share gotcha. **Scratch space and RAM.** The images pin `TMPDIR=/data/tmp`, so all render and transcode scratch follows the `/data` mount onto real disk. Do **not** add an extra path mapping for the container's `/tmp` pointing at the host's `/tmp` (a common pattern for Plex transcode-to-RAM): Unraid's host root filesystem is a ramdisk, so that mapping turns every intermediate video file into RAM usage. Also note the Unraid dashboard counts the Linux page cache as used memory — heavy render I/O will show a large, temporary "RAM" spike even when scratch is on disk. That cache is reclaimable and is not the container leaking memory. ## Updating ### Monolithic Container ```bash # Pull latest image docker pull ghcr.io/make-ware/video-ware:latest # Stop and remove old container docker stop video-ware docker rm video-ware # Start new container (volumes persist data) docker run -d \ --name video-ware \ -p 8888:80 \ -e POCKETBASE_ADMIN_EMAIL=admin@example.com \ -e POCKETBASE_ADMIN_PASSWORD=your-secure-password \ -v ./data:/data \ ghcr.io/make-ware/video-ware:latest ``` ### Docker Compose ## Health Checks Both deployment options include health checks: ### Monolithic Container The container exposes a health check endpoint at `/health`. You can verify it's running: ```bash curl http://localhost:8888/health ``` ### Docker Compose Each service has its own health check: - PocketBase: `/api/health` - Redis: `redis-cli ping` - Worker: Built into NestJS - Webapp: Built into Next.js View health status: ```bash docker compose ps ``` ## Troubleshooting ### Logs #### Monolithic Container ```bash # View all logs docker logs -f video-ware # View last 100 lines docker logs --tail 100 video-ware ``` #### Docker Compose ```bash # All services docker compose logs -f # Specific service docker compose logs -f webapp docker compose logs -f worker docker compose logs -f pocketbase docker compose logs -f redis ``` ### Common Issues #### Container won't start 1. Check logs: `docker logs video-ware` or `docker compose logs` 2. Verify environment variables are set correctly 3. Ensure ports are not already in use 4. Check disk space: `docker system df` #### Can't access the application 1. Verify the container is running: `docker ps` 2. Check port mappings: `docker port video-ware` or `docker compose ps` 3. Check firewall settings 4. Verify health endpoint: `curl http://localhost:8888/health` #### PocketBase admin not working 1. Ensure `POCKETBASE_ADMIN_PASSWORD` is set to a non-default value 2. Check PocketBase logs: `docker compose logs pocketbase` 3. Access admin UI directly: http://localhost:8090/_/ or http://localhost:8888/_/ #### Worker not processing tasks 1. Check Redis connection: `docker compose logs redis` 2. Verify worker logs: `docker compose logs worker` 3. Check Bull Board dashboard: http://localhost:3002 4. Ensure Redis is healthy: `docker compose ps redis` ### Getting Help - Check the logs for error messages - Review the [Development Guide](../docs/DEVELOPMENT.md) for more details - Open an issue on GitHub with logs and configuration details ## Production Recommendations ### Security - **Change default passwords**: Always set `POCKETBASE_ADMIN_PASSWORD` to a strong, unique password - **Use secrets management**: Store sensitive environment variables in Docker secrets or a secrets manager - **Network isolation**: Use Docker networks to isolate services - **Regular updates**: Keep images up to date with security patches - **Resource limits**: Set CPU and memory limits to prevent resource exhaustion ### Resource Limits ```bash # Monolithic container with limits docker run -d \ --name video-ware \ --memory="4g" \ --cpus="2.0" \ -p 8888:80 \ # ... other options # Docker Compose with limits (add to docker-compose.yml) services: webapp: deploy: resources: limits: cpus: '1' memory: 2G reservations: cpus: '0.5' memory: 1G ``` ### Monitoring - **Container metrics**: Monitor CPU, memory, and network usage - **Log aggregation**: Forward logs to a centralized logging system - **Health checks**: Use health check endpoints for monitoring - **Backup**: Regularly backup PocketBase data volumes ### Backup Backup the PocketBase data volume: ```bash # Monolithic docker run --rm \ -v ./data:/data \ -v $(pwd)/backups:/backup \ alpine tar czf /backup/pb-data-$(date +%Y%m%d).tar.gz -C /data pb_data # Docker Compose docker run --rm \ -v ./data:/data \ -v $(pwd)/backups:/backup \ alpine tar czf /backup/pb-data-$(date +%Y%m%d).tar.gz -C /data pb_data ``` ## Architecture Details ### Monolithic Container The monolithic container uses: - **Supervisor**: Manages all processes - **Nginx**: Reverse proxy (port 80) - **PocketBase**: Backend API (internal port 8090) - **Next.js**: Frontend application (internal port 3000) - **Worker**: Background task processor All services run in a single container, making it ideal for simple deployments. ### Docker Compose Separate containers for: - **Redis**: Task queue backend - **PocketBase**: Backend API and database - **Worker**: Background task processor - **Webapp**: Next.js frontend This architecture allows for independent scaling and better resource management.