EncodeHarbor
Docker app from marcvandecoevering's Repository
Overview
Readme
View on GitHubEncodeHarbor
A calm little workspace for lighter videos.
EncodeHarbor watches a folder, compresses videos with FFmpeg and places verified MP4 copies in a separate output folder. It runs on your own server, with a responsive web interface and a persistent queue.
Version 1.0.0 · CPU encoding · Linux amd64 · release 1.0
What it does
- Watches nested folders and waits for files to stop changing.
- Encodes one video at a time to H.264 / AAC in an MP4 container.
- Offers High quality, Balanced and Small file profiles.
- Shows progress, file sizes, output reduction and searchable history.
- Pauses between jobs; retries failed and skipped jobs on request.
- Keeps the queue and settings through restarts.
- Validates duration and fully decodes the output before publishing it.
- Retains input by default; optional cleanup removes each source only after verified publication.
- Always delivers output: if compression saves no space, copies the original with its original extension.
- Supports working hours, weekdays and an IANA time zone, including overnight windows.
- Opens directly without an account or password. No telemetry, cloud API or external fonts.
Output reduction compares completed copies to their originals. It is not space already freed from your disks: both versions remain unless input cleanup is enabled.
Quick start from source
Requires Docker Engine / Docker Desktop and Docker Compose.
docker compose up -d --build
Open http://localhost:8080 to access the dashboard directly. Put a video in .runtime/input. It appears after it has been unchanged for at least 30 seconds and two scanner observations. Finished copies are written to .runtime/output.
The supplied Compose file binds to loopback. For access from your LAN, set EH_BIND_ADDRESS=0.0.0.0 in .env. The app has no built-in authentication; access control, if needed, belongs to your network or reverse proxy.
Install on Unraid
Use the Unraid template. The container image is ghcr.io/marcvandecoevering/encodeharbor:latest; pin :1.0.0 for this release. Host and container web ports default to 8080. Choose your four folders before starting the container. The web interface opens directly without a login.
Community Applications catalog publication is managed separately from the GitHub release. Until the listing is available, save the template as /boot/config/plugins/dockerMan/templates-user/my-encodeharbor.xml, then select EncodeHarbor under Docker → Add Container.
Release notes, image SBOM and corresponding source packages are attached to GitHub releases.
Folder layout
| Container folder | Access | Purpose |
|---|---|---|
/config |
Read/write, private | SQLite queue and settings |
/input |
Read/write for optional cleanup | Your source videos, including subfolders |
/output |
Read/write | Verified compressed copies |
/work |
Read/write | Temporary encoding files; fast local storage recommended |
The four folders must be separate and must not contain one another. Do not map two container paths onto the same host folder. Keep /config on local persistent storage rather than an SMB/NFS mount because SQLite WAL needs reliable local locking.
For Unraid, a typical layout is:
/mnt/user/appdata/encodeharbor -> /config
/mnt/user/MediaToEncode -> /input (read/write for optional cleanup)
/mnt/user/EncodedMedia -> /output
/mnt/cache/encodeharbor-work -> /work
Choose the actual cache/pool path present on your server. Input/output can be shares whose storage placement you configure in Unraid. A local installation template is in unraid/encodeharbor-local.xml.
Compose paths can be changed in .env:
EH_INPUT_PATH=/mnt/user/MediaToEncode
EH_OUTPUT_PATH=/mnt/user/EncodedMedia
EH_CONFIG_PATH=/mnt/user/appdata/encodeharbor
EH_WORK_PATH=/mnt/cache/encodeharbor-work
EH_BIND_ADDRESS=0.0.0.0
EH_PORT=8080
PUID=99
PGID=100
How a file moves through the queue
Input → settle → probe → encode → validate → publish to output
optional input cleanup only after verified publication
The scanner checks every three seconds. It identifies a source version by relative path, size and modification time. Re-scanning an unchanged source does not create another job. This is not content-based deduplication across different filenames.
For reliable automated ingestion, write under a non-video extension (for example .part), close the file, then rename it to its final video extension on the input filesystem. A settling timeout cannot prove that a paused upload has finished.
Compressed outputs use MP4; unchanged originals keep their extension. Output filenames include a source fingerprint, such as camera-2c714d8fa62a.mp4. Subfolders are preserved. Output is staged in the destination directory and published using an atomic hard link; existing files are never overwritten. The output filesystem must support hard links (normal local Unraid/Linux filesystems do). An unsupported filesystem fails the job safely.
On interruption, active jobs are requeued and re-encoded from the beginning. A receipt allows recovery if the app stopped after publishing but before committing the final database update. Mount a dedicated /work folder; it must not be shared by multiple instances. Only one app instance may use a given /config folder.
The disk reserve defaults to 2 GiB on both work and output storage. Before encoding, free space must exceed that reserve plus twice the source size. Free space is checked during encoding and again before copying. This is a safety margin, not a filesystem quota or a guarantee against another process filling the disk.
Encoding behavior and limits
| Profile | Codec | CRF | Audio |
|---|---|---|---|
| High quality | H.264, medium preset | 18 | AAC 128 kbit/s |
| Balanced | H.264, medium preset | 23 | AAC 128 kbit/s |
| Small file | H.264, medium preset | 28 | AAC 128 kbit/s |
Compression is lossy. Resolution and frame rate are retained; odd dimensions are padded to even numbers. FFmpeg applies source rotation. Output uses 8-bit YUV420P for compatibility. Lower CRF generally preserves more detail but consumes more space. Savings depend on the input.
- Supported input extensions: MP4, MOV, MKV, AVI, WebM, M4V, MTS and M2TS. Codec support depends on the included FFmpeg build.
- HDR (PQ / HLG) is copied unchanged to output to avoid an unconfigured HDR-to-SDR conversion.
- Only the first non-cover-art video and first audio track are copied into the encode. Extra audio, subtitles, chapters and source metadata are not preserved.
- No GPU encoding, AV1/HEVC output, image compression, media streaming or remote URL ingestion in this release.
- Pause lets the current job finish and prevents the next job from starting. Scanning continues while paused.
- Failed jobs require a retry or a changed source; there is no endless automatic retry loop.
- The admin interface lists filenames and output paths but does not serve the videos.
- The first release has been tested on Linux amd64 in Docker Desktop. Native Unraid installation still needs a release acceptance test.
Working hours and input cleanup
Working hours are disabled by default. Enable them in Settings, select starting weekdays, start/end times and a time zone (default Europe/Amsterdam). Overnight windows belong to their starting day: Monday 22:00–07:00 includes Tuesday morning. Equal start and end times mean the full selected calendar day. The current job finishes when a window closes; scanning continues and the next job waits. Manual pause also prevents new jobs. Daylight saving time follows the selected zone.
Empty input after conversion is disabled by default and applies to newly started jobs. It removes only the individual successfully processed source, never the folder or unrelated files. Both compressed outputs and unchanged-original fallbacks qualify. Before deletion, the output is durably published and hash-verified; the source is checked against its original signature and SHA-256. If output creation fails, input stays intact. If deletion fails (for example on an existing read-only mount), the completed job shows a cleanup warning and a Retry action. Retry uses the verified output rather than encoding again. A persisted receipt also handles a restart after deletion.
The supplied Compose and Unraid templates now mount input read/write so cleanup can work. Existing installations using :ro must change it to :rw, recreate the container and grant the configured PUID/PGID write permission to source directories. Input ownership is never changed automatically. If cleanup remains disabled, a read-only input mount is still supported.
Damaged/unreadable videos or storage failures remain failed jobs with input intact; they are not silently deleted or reported as delivered. Old skipped jobs can be retried to deliver them under the new behavior.
Filesystem permissions and access
The container starts with a small root entrypoint that sets ownership on the three writable mount roots only, then drops to PUID:PGID (default 99:100). It does not recursively change ownership of existing files. Ensure existing folders and config files are accessible to that identity. The web server and FFmpeg run unprivileged. Docker exec defaults to root; use docker exec --user 99:100 ... when checking application permissions.
There are no application accounts, login screens, passwords or authentication cookies. Anyone who can reach the app can use its controls. Cross-site browser mutation requests are still rejected. Legacy password/session files from earlier builds are ignored and are not needed.
Development and tests
docker build -t encodeharbor:1.0.0 .
docker run --rm --entrypoint python \
-v "$PWD/tests:/tests:ro" \
encodeharbor:1.0.0 -m unittest discover -s /tests -v
Tests include real FFmpeg encoding, decode validation, corrupt media, no-overwrite behavior, source preservation, unchanged-original fallback, optional cleanup and crash-safe cleanup retries, working-hour boundaries and time zones, crash receipt recovery, storage reserve, restart recovery, settings persistence, source changes, symlink/traversal rejection and direct access and cross-site request protection.
The backend is Python, Flask, Waitress and SQLite. The frontend is plain HTML/CSS/JavaScript without a build toolchain. FFmpeg is installed inside the container, so the Unraid host needs no FFmpeg plugin.
License
Application code: MIT. FFmpeg and other image components retain their own licenses; see THIRD_PARTY.md.
Requirements
Categories
Related apps
Explore more like this
Explore allDetails
ghcr.io/marcvandecoevering/encodeharbor:latestRuntime arguments
- Web UI
http://[IP]:[PORT:8080]/- Network
bridge- Shell
sh- Privileged
- false
- Extra Params
--cap-drop=ALL --cap-add=CHOWN --cap-add=SETUID --cap-add=SETGID --security-opt=no-new-privileges --stop-timeout=30
Template configuration
Choose an available host port.
- Target
- 8080
- Default
- 8080
- Value
- 8080
Source videos. Writable for optional cleanup; deletion is disabled by default.
- Target
- /input
- Default
- /mnt/user/MediaToEncode
- Value
- /mnt/user/MediaToEncode
Smaller compressed videos or unchanged originals. Must support hard links; keep separate from input.
- Target
- /output
- Default
- /mnt/user/EncodedMedia
- Value
- /mnt/user/EncodedMedia
Persistent queue and settings. Use persistent local storage.
- Target
- /config
- Default
- /mnt/user/appdata/encodeharbor
- Value
- /mnt/user/appdata/encodeharbor
Temporary videos. Change cache to your actual pool name. Must be dedicated to this instance.
- Target
- /work
- Default
- /mnt/cache/encodeharbor-work
- Value
- /mnt/cache/encodeharbor-work
Non-root numeric user ID for the application and FFmpeg.
- Target
- PUID
- Default
- 99
- Value
- 99
Numeric group ID.
- Target
- PGID
- Default
- 100
- Value
- 100