EncodeHarbor

EncodeHarbor

Docker app from marcvandecoevering's Repository

Overview

Automatically compress videos with FFmpeg. Watch folders, choose quality profiles and working hours, and track progress in a responsive web interface. Files are always delivered: compressed when smaller, otherwise unchanged. Input cleanup is optional and disabled by default.

EncodeHarbor

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

Use separate input, output, work and config folders. Change the cache path to your actual pool name. Output storage must support hard links. The web interface has no login; use a trusted network or your own access proxy.

Related apps

Details

Repository
ghcr.io/marcvandecoevering/encodeharbor:latest
Last Updated2026-11-02
First Seen2026-11-02

Runtime 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

Web interfacePorttcp

Choose an available host port.

Target
8080
Default
8080
Value
8080
Input videosPathrw

Source videos. Writable for optional cleanup; deletion is disabled by default.

Target
/input
Default
/mnt/user/MediaToEncode
Value
/mnt/user/MediaToEncode
Output videosPathrw

Smaller compressed videos or unchanged originals. Must support hard links; keep separate from input.

Target
/output
Default
/mnt/user/EncodedMedia
Value
/mnt/user/EncodedMedia
ConfigurationPathrw

Persistent queue and settings. Use persistent local storage.

Target
/config
Default
/mnt/user/appdata/encodeharbor
Value
/mnt/user/appdata/encodeharbor
Working storagePathrw

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
Filesystem user IDVariable

Non-root numeric user ID for the application and FFmpeg.

Target
PUID
Default
99
Value
99
Filesystem group IDVariable

Numeric group ID.

Target
PGID
Default
100
Value
100