Ombi-Discord-Router

Ombi-Discord-Router

Docker app from MediaOps' Repository

Overview

Companion service for MediaOps that receives Ombi webhook notifications and routes media request lifecycle updates into managed Discord Forum threads.

Ombi Discord Router addon

This optional companion service adapts Ombi request notifications into a Discord Forum workflow that MediaOps can maintain as a searchable request history.

The router is intentionally separate from the main MediaOps bot. It owns the Discord webhook credential, while MediaOps only needs the webhook's non-secret ID plus the Forum/tag IDs used for status synchronization.

What it does

For supported Ombi movie and TV request notifications, the router:

  • creates one Discord Forum post per active media request lifecycle;
  • applies one media-type tag and one request-status tag when the post is created;
  • stores the Discord thread ID in a small persistent JSON index;
  • posts only meaningful forward lifecycle changes into the existing thread;
  • ignores duplicate same-state notifications and stale/backward transitions;
  • keeps terminal request states terminal within the same Ombi request lifecycle;
  • removes active correlation when Ombi reports RequestDeleted while leaving Discord history intact;
  • recognizes a new Ombi request ID for the same provider item as a new request lifecycle;
  • recovers from a deleted/missing Discord Forum thread by removing the stale correlation and recreating the thread from the current event;
  • exposes /health for container monitoring;
  • emits sanitized technical event logs without titles, requester names, provider IDs, request IDs, or webhook credentials;
  • avoids logging the Discord webhook URL/token when Discord returns an error.

MediaOps then performs the privileged Discord-side lifecycle management, including status-tag synchronization and closing/locking terminal request posts.

Published image

ghcr.io/miakkia/mediaops-ombi-discord-router:latest
ghcr.io/miakkia/mediaops-ombi-discord-router:dev
ghcr.io/miakkia/mediaops-ombi-discord-router:sha-<commit>

latest follows main; development branches publish dev; repository release tags publish matching semantic-version tags.

Requirements

  • Ombi with webhook notifications enabled;
  • one Discord Forum channel;
  • a Discord webhook created for that Forum;
  • Forum tags for Movie, Series, Requested, Processing, Available, Failed, and Denied;
  • optional Forum tag for webhook diagnostics, such as Test;
  • persistent storage for /data that is writable by UID/GID 1000:1000 when using the hardened example Compose configuration;
  • network reachability from Ombi to the router.

The visible tag names are administrator choices. Configuration uses Discord IDs.

Configuration

Copy .env.example to .env and fill in your own values. Never commit the real .env file.

TZ=America/Toronto
MEDIA_REQUESTS_WEBHOOK=
MEDIA_REQUESTS_WEBHOOK_NAME=Media Request Herald
MEDIA_TAG_REQUESTED=
MEDIA_TAG_PROCESSING=
MEDIA_TAG_AVAILABLE=
MEDIA_TAG_FAILED=
MEDIA_TAG_DENIED=
MEDIA_TAG_MOVIE=
MEDIA_TAG_SERIES=
MEDIA_TAG_TEST=
ROUTER_DATA_DIR=/data
ROUTER_DATA_HOST_DIR=./data
MEDIAOPS_NETWORK=mediaops-backend

MEDIA_REQUESTS_WEBHOOK is a secret because the Discord webhook URL contains its credential. Do not paste it into issues, screenshots, logs, examples, or MediaOps configuration.

MediaOps itself should receive only the webhook ID through MEDIA_REQUESTS_WEBHOOK_ID, not this secret URL.

Persistent data and permissions

The router persists its request/thread index under /data and uses an atomic temporary-file write before replacing media-threads.json. The hardened Compose example runs the container as UID/GID 1000:1000, so the host directory mounted at /data must be writable by that identity.

A typical host mapping is:

volumes:
  - /path/on/host/ombi-discord-router/data:/data

Keep ROUTER_DATA_DIR=/data; host filesystem paths do not belong in ROUTER_DATA_DIR.

On a shell-capable Docker host, prepare the directory before starting the router:

mkdir -p /path/on/host/ombi-discord-router/data
chown -R 1000:1000 /path/on/host/ombi-discord-router/data
chmod -R 750 /path/on/host/ombi-discord-router/data

Portainer/NAS deployments without SSH can perform the same one-time ownership change with a temporary privileged/root utility container that bind-mounts the host directory, runs chown -R 1000:1000 and chmod -R 750, then is removed. Do not make the router itself privileged and do not use world-writable 777 permissions as a workaround.

If permissions are wrong, Ombi delivery may reach the router but return HTTP 502 with a log similar to:

PermissionError: [Errno 13] Permission denied: '/data/media-threads.tmp'

That error means network delivery is working; correct the host bind-mount ownership/permissions and restart the router.

Networking

Ombi and router on the same Docker host/network

Prefer a user-defined Docker network so Ombi can reach the router by container name instead of a changing container IP.

docker network create mediaops-backend

Connect Ombi to the same network, then configure Ombi's webhook destination as:

http://ombi-discord-router:8080/ombi

compose.example.yaml expects the network to already exist and defaults to mediaops-backend. Set MEDIAOPS_NETWORK if you use a different external network name.

Ombi and router on different LAN hosts

If Ombi and the router are intentionally on different trusted LAN hosts, they cannot use a shared Docker network. Publish the router port only to the trusted LAN as required by your platform and point Ombi at the router host, for example:

http://ROUTER_LAN_IP:8080/ombi

Verify reachability first with:

http://ROUTER_LAN_IP:8080/health

A successful response reports status: ok, version: 1.9, mode: discord-forum, and the /data/media-threads.json index path.

Do not port-forward router port 8080 from the Internet. /ombi is designed for a private Docker/LAN trust boundary.

Compose deployment

cp .env.example .env
# edit .env with your values

docker compose -f compose.example.yaml pull
docker compose -f compose.example.yaml up -d

The example Compose file uses the published GHCR image and deliberately keeps least-privilege defaults: non-root execution, read-only root filesystem, dropped Linux capabilities, no-new-privileges, PID/memory limits, and a dedicated persistent /data mount.

For development or reproducible local testing:

docker build -t local/ombi-discord-router:test .

Unraid

A generic Unraid v2 template is included at:

templates/ombi-discord-router.xml

The template points to the published GHCR image, masks the webhook credential, predefines /data, includes all required Forum tag variables, and applies the same hardened runtime options used during validation.

Before installing the router template, create an external Docker network such as mediaops-backend and attach Ombi to it. Keep the router name ombi-discord-router or update the Ombi webhook URL if you choose a different container name.

Health check

Expected response from GET /health:

{
  "status": "ok",
  "version": "1.9",
  "mode": "discord-forum",
  "index": "/data/media-threads.json"
}

The packaged example does not publish port 8080 to the host by default. Check /health from inside the container or another container on the shared Docker network. Publish the port only when a separate trusted LAN host such as Ombi must reach the router.

Ombi notification behavior

The router accepts POST /ombi JSON notifications from Ombi. Movie and TV request lifecycle messages are correlated using provider/request identity and recorded in /data/media-threads.json.

A normal lifecycle is represented as one Forum post with meaningful forward updates:

Requested -> Processing -> Available

Failed and Denied are also terminal states supported by MediaOps.

Ombi may emit multiple notifications for one logical state change, and those notifications can arrive in a surprising order. The router therefore treats lifecycle delivery as idempotent:

  • a notification that resolves to the same status already stored is ignored;
  • a notification that would move the stored request backward is ignored;
  • a terminal request cannot be rewritten by later non-terminal events from the same request lifecycle;
  • request/provider metadata may still be refreshed silently when a duplicate or stale event contains useful identifiers;
  • if the same provider item later arrives with a different Ombi request ID, the router treats it as a new request lifecycle and creates a fresh Forum thread;
  • if the stored Discord thread no longer exists and Discord returns Unknown Channel, the stale correlation is removed and the current lifecycle is recreated instead of remaining stuck on a dead thread ID.

This prevents sequences such as RequestApproved -> NewRequest -> RequestApproved from producing three near-identical Forum messages or regressing a request from Processing back to Requested, while still allowing a previously Available, Failed, Denied, or deleted item to be requested again later.

Request deletion and re-requesting

RequestDeleted does not delete Discord history. Instead, the router removes the active /data/media-threads.json correlation for that request. A later request for the same media can therefore create a new Forum thread without being mistaken for a duplicate of the deleted request.

The same lifecycle separation applies when a previous request reached Available, Failed, or Denied: a later Ombi request with a new request ID starts a fresh active lifecycle while the old Discord thread remains historical.

Ombi administrator requests

Ombi's notification behavior depends on who creates the request. In particular, Ombi does not emit the normal NewRequest notification for a request created by an Ombi Admin account because the administrator is considered to already know that the request was made. API, Power User, and normal-user requests can emit the normal request notification flow.

This is an Ombi-side behavior, not a router permission decision. The router cannot create a Requested Forum post for an event it never receives. A later notification such as RequestApproved, RequestAvailable, Failed, or Denied can still create the Forum post if no earlier post exists.

Administrators who require every request to appear in the Forum from its first state should avoid relying on Ombi Admin-origin NewRequest notifications as the sole source of truth. A future MediaOps-side request/API synchronization path may cover that case without weakening the router trust boundary.

Ombi webhook test

When MEDIA_TAG_TEST is configured, Ombi's built-in Send test action creates a dedicated Ombi Webhook Test Forum post carrying only the configured Test tag. It is not written to media-threads.json and is not treated as a Movie/Series lifecycle entry.

Operational logging

The router logs a narrow technical summary for each accepted Ombi event, for example:

OMBI EVENT: notificationType=RequestApproved type=Movie result=created
OMBI EVENT: notificationType=NewRequest type=Movie result=ignored reason=duplicate-status
OMBI EVENT: notificationType=RequestDeleted type=Movie result=removed reason=request-deleted

These lines are intentionally limited to notification type, media type, result, and a bounded reason. They do not include titles, requester identities, request/provider IDs, overview text, or webhook credentials.

Security notes

Treat Ombi payloads and Discord responses as untrusted integration data. Keep the service private to the Docker/LAN network and do not expose /ombi publicly unless you add an appropriate authenticated reverse-proxy boundary.

The router never needs the MediaOps Discord bot token. MediaOps never needs the router's webhook token. Keeping those credentials separate reduces the impact of either component being misconfigured or compromised.

The router intentionally sanitizes Discord delivery errors so the webhook URL/token is not included in application logs.

For the MediaOps-side trust boundary and required Discord permissions, see ../../docs/REQUEST_FORUM.md and ../../docs/SECURITY_MODEL.md.

Runtime files

The only persistent application-owned state is the request/thread index:

/data/media-threads.json

Do not commit runtime state or a populated .env file.

Install Ombi-Discord-Router on Unraid in a few clicks.

Find Ombi-Discord-Router in Community Apps on your Unraid server, review the template, and click Install. Unraid handles the Docker app or plugin setup from the published template.

Open the Apps tab on your Unraid server Search Community Apps for Ombi-Discord-Router Review the template variables and paths Click Install

Requirements

Requires Ombi, a Discord Forum webhook, configured Forum tag IDs, and an existing user-defined Docker network shared with Ombi. MediaOps is required for live Forum tag synchronization and terminal thread locking.

Related apps

Explore more like this

Explore all

Details

Repository
ghcr.io/miakkia/mediaops-ombi-discord-router:latest
Last Updated2026-08-28
First Seen2026-08-28

Runtime arguments

Network
mediaops-backend
Shell
sh
Privileged
false
Extra Params
--user 1000:1000 --read-only --tmpfs /tmp --cap-drop ALL --security-opt no-new-privileges:true --memory 256m --pids-limit 64

Template configuration

Router DataPathrw

Persistent Ombi Discord Router request/thread index.

Target
/data
Default
/mnt/user/appdata/ombi-discord-router/data
TimezoneVariable

IANA timezone used by the router for log timestamps.

Target
TZ
Default
America/Toronto
Media Requests WebhookVariable

Full Discord Forum webhook URL. Secret: the URL contains a Discord credential and must not be published.

Target
MEDIA_REQUESTS_WEBHOOK
Webhook Display NameVariable

Display name used by the Discord webhook when posting request lifecycle updates.

Target
MEDIA_REQUESTS_WEBHOOK_NAME
Default
Media Request Herald
Requested Status Tag IDVariable

Discord Forum tag ID applied to newly submitted media requests that are waiting to be processed.

Target
MEDIA_TAG_REQUESTED
Processing Status Tag IDVariable

Discord Forum tag ID used when a media request has been approved or is currently being processed.

Target
MEDIA_TAG_PROCESSING
Available Status Tag IDVariable

Discord Forum tag ID used when the requested media is available on the configured media server. This is a terminal request state.

Target
MEDIA_TAG_AVAILABLE
Failed Status Tag IDVariable

Discord Forum tag ID used when processing or acquiring the requested media has failed. This is a terminal request state.

Target
MEDIA_TAG_FAILED
Denied Status Tag IDVariable

Discord Forum tag ID used when a media request has been denied or rejected. This is a terminal request state.

Target
MEDIA_TAG_DENIED
Movie Media Tag IDVariable

Discord Forum tag ID identifying a request as a movie. This media-type tag is preserved throughout the request lifecycle.

Target
MEDIA_TAG_MOVIE
Series Media Tag IDVariable

Discord Forum tag ID identifying a request as a TV series. This media-type tag is preserved throughout the request lifecycle.

Target
MEDIA_TAG_SERIES
Test Tag IDVariable

Optional Discord Forum tag ID used only by Ombi's built-in Webhook Send test action. Test posts carry only this tag, are not indexed as media requests, and are ignored by the MediaOps request lifecycle.

Target
MEDIA_TAG_TEST
Router Data DirectoryVariable

Container path for the persistent request/thread index. Leave at /data for the packaged deployment.

Target
ROUTER_DATA_DIR
Default
/data