IRC announce bot + qBittorrent + Flood, hardened and shell-less, non-root, read-only rootfs
710
An IRC announce bot, qBittorrent and Flood in a single hardened container.
Announce channels are how a lot of trackers publish new releases in real time. Polling an RSS feed means waiting out the poll interval; sitting on the channel means acting the moment the line appears. irc2torrent connects, listens, filters against your regexes, fetches the .torrent, and hands it to qBittorrent — usually within a second of the announce.
The runtime is built on Docker Hardened Images: no shell, no busybox, no package manager, running as uid 1000 with a read-only root filesystem.
Prefer rTorrent?
irc2torrent/flood_rtorrent_irc2torrent is the same bot and the same Flood with rakshasa rTorrent instead, carrying a fix for a long-standing bug that makes rTorrent read 4–100× more from disk than it uploads. Pick this image if you would rather configure a client through a web UI than an.rtorrent.rc.
docker run -d --name qbittorrent \
--read-only --security-opt no-new-privileges:true \
--tmpfs /tmp --stop-timeout 30 \
-v qbt-config:/config \
-v /srv/downloads:/data \
-p 3000:3000 -p 8080:8080 -p 50000:50000 -p 50000:50000/udp \
irc2torrent/flood_qbittorrent_irc2torrent:latest
qBittorrent's WebUI is on http://localhost:8080, Flood on http://localhost:3000. For Flood, choose qBittorrent → http://127.0.0.1:8080.
The bot needs no configuration to reach qBittorrent. The image ships a config that bypasses authentication for connections from inside the container, and writes an options.toml already pointing at it.
That bypass is keyed on the peer address, so a request arriving through the published port comes from the Docker bridge rather than 127.0.0.1 and still meets the login form. Set a WebUI password anyway if you expose 8080 beyond the host — qBittorrent prints a temporary one to the log on first start until you do.
--stop-timeout 30 matters. Docker defaults to 10 seconds and then SIGKILLs everything, which costs qBittorrent its fastresume data — and a qBittorrent that lost its fastresume rechecks the whole library on the next start, which on a full seedbox is hours of disk I/O.
On first run the bot writes default configs into /config/.config/ and exits. Edit them and start the container again.
Two files, both created with working defaults on first run: irc.toml for the connection, options.toml for everything else.
# Releases you want. A line is taken if it matches ANY of these...
regex_for_downloads_match = [
"Some Release.*2160p.*",
"Another Release.*S02.*1080p.*WEB.*"
]
# ...and dropped if it matches ANY of these. Reject wins over match.
regex_for_downloads_reject_match = [
"(?i).*NORDIC.*",
"(?i).*GERMAN.*"
]
# How to pull the release name and torrent id out of an announce line.
# The two named captures are required; the rest depends on your network.
regex_for_announce_match = '''.*Name:'(?P<name>.*)' uploaded by.*https://tracker.example.org/torrent/(?P<id>\d+)'''
# The tracker. The key is a label of your choosing and is what gets logged.
[platform.YourTracker]
download_url_template = "https://tracker.example.org/rss/download/{id}/{key}/{file}"
rss_key = "XXXXXXXXXXXXXXXXXXXX"
torrent_dir = "/data/.torrents"
[[clients]]
[clients.qBittorrent]
url = "http://127.0.0.1:8080"
username = "" # leave empty where auth is bypassed for localhost
password = ""
save_path and category are optional; empty means whatever qBittorrent is already configured to use. The bot only logs in when the server asks it to, so an install with authentication bypassed for localhost needs no credentials at all.
Set torrent_dir to a path under /data or /config. The root filesystem is read-only, so it has to be somewhere writable — /data/.torrents is the natural choice for this image.
There is no tracker-specific code anywhere. A network is an announce regex plus a download_url_template — {id}, {name}, {file} and {key} are the placeholders. Adding a network is configuration, never code.
Three things worth knowing:
.* do nothing. "2160p" and ".*2160p.*" behave identically.(?i) for case rather than listing variants. One (?i).*NORDIC.* covers NORDiC, Nordic and the rest.regex_for_announce_match is the one you have to write yourself, since every network announces differently. Watch the channel by hand for a minute first.{name} and {id} come verbatim from an IRC message, so every substituted value is percent-encoded and the host is re-checked before the request is made. A placeholder in the host or port is rejected at startup — otherwise a crafted release name could point the request, and your rss_key, at someone else's server.
Most of options.toml reloads live, no restart: the filters, the tracker block, and all notification settings. platform, clients, [telegram] and [slack] are read once at startup and log a line saying so rather than pretending to have applied.
| Path | Purpose |
|---|---|
/config | All persistent state — qBittorrent profile, Flood database, bot config. Back this up. |
/config/.config/options.toml | Filters, tracker, client, notifications. |
/config/.config/irc.toml | IRC connection settings. |
/config/qBittorrent/config/qBittorrent.conf | qBittorrent's own config. Seeded on first start and never overwritten afterwards. |
/data | Torrent data. |
| Port | Purpose |
|---|---|
3000 | Flood web UI |
8080 | qBittorrent WebUI |
50000 tcp+udp | Peer traffic |
Everything has a working default; you normally need to set none of them.
| Variable | Default | Purpose |
|---|---|---|
IRC2TORRENT_SUPERVISE | 1 | Run as container init. Unset to run the bot alone. |
IRC2TORRENT_SHUTDOWN_GRACE | 8 | Seconds children get after SIGTERM before SIGKILL. Below docker stop's 10s default on purpose. Raise this and --stop-timeout together. |
IRC2TORRENT_RAW_CHILD_LOGS | 0 | Child output is captured and prefixed [qbittorrent] / [flood]. Set 1 for plain inheritance, keeping Flood's output as machine-parseable JSON. |
QBITTORRENT_WEBUI_PORT | 8080 | WebUI port, and the port the supervisor waits on before starting Flood. |
QBITTORRENT_PROFILE | /config | Passed as --profile. |
FLOOD_OPTION_AUTH | (unset) | default or none. Set default if Flood is reachable beyond localhost. |
FLOOD_OPTION_PORT | 3000 | Flood web UI port. |
PUID / PGID | 1000 | Ownership of /config and /data. |
Set IRC2TORRENT_SYSLOG and everything the bot logs goes to a collector as well as to docker logs — no shipper, no sidecar, no file to tail. Messages are RFC 3164, which QNAP QuLog Center, rsyslog, syslog-ng and Synology all accept.
-e IRC2TORRENT_SYSLOG=udp://192.168.1.10:514
-e IRC2TORRENT_SYSLOG_HOSTNAME=irc2torrent
| Variable | Default | Purpose |
|---|---|---|
IRC2TORRENT_SYSLOG | (off) | udp://host[:port], tcp://host[:port], unix, or unix:/path. Bare host[:port] means UDP. Port defaults to 514. IPv6 needs brackets. |
IRC2TORRENT_SYSLOG_TAG | irc2torrent | Program name in the header — what collectors group by. |
IRC2TORRENT_SYSLOG_LEVEL | info | Threshold for this sink alone. |
IRC2TORRENT_SYSLOG_FACILITY | daemon | daemon, user, local0–local7, … |
IRC2TORRENT_SYSLOG_HOSTNAME | (system hostname) | Defaults to the short container id, which changes on every recreate — set it if you group by host. |
IRC2TORRENT_SYSLOG_CHILD_LOGS | 0 | Also relay qBittorrent and Flood output to the collector. |
UDP is the default deliberately: the sink is synchronous, so a TCP target that stops answering can stall the bot. A target that cannot be opened is logged once and then ignored — never fatal.
Optional, both off by default.
Notifications go to email, ntfy, IRC private message, Telegram or Slack, on events like a torrent added, a download finished, disk running low, or a daily summary. Two settings keep an announce channel from becoming a firehose: digest_seconds (default 300) buffers events into one message with repeats collapsed into a count, and max_per_hour (default 20) is a hard ceiling that reports how many it suppressed. As of 0.17.0 a notification held for an absent owner expires after hold_seconds (default 15 minutes) instead of arriving as a weekend-long backlog of things that already resolved.
Commands over IRC let you query and control the bot from a private message. This exposes a control interface, so it is off by default. A nickname is not a credential — on a network without enforced registration anyone can take yours the moment you drop off — so require_identified (on by default) asks services who the sender is and refuses unless they are logged in to an account matching your configured name.
One container, no s6-overlay. irc2torrent runs as PID 1, starts and reaps qBittorrent and Flood, installs its own SIGTERM/SIGINT handlers, and waits for the WebUI port to answer before starting Flood, so Flood's first connection succeeds.
Qt loads its TLS backend with dlopen, so it appears in no ldd listing. Miss it and qBittorrent starts, seeds, and silently never announces to an HTTPS tracker — the build copies the whole plugin tree and asserts the TLS backend landed.
irc2torrent parses untrusted input from a network it does not control, so it is built to be contained: non-root, read-only root filesystem, no shell or package manager in the runtime image, no-new-privileges.
A full security audit preceded 0.2.0 — a high-severity arbitrary file write reachable from a crafted IRC announce, three remotely triggerable panics, and dependency work that took cargo audit from 18 vulnerabilities to 0. All of it is fixed as of 0.2.0; run that or newer. cargo audit gates CI on every build.
latest — the most recent release0.17.0, 0.16.0, … — pinned versionsThis ships no tracker configuration and no announce patterns for any particular network. What you connect to, and whether you are entitled to what you download, is yours to sort out.
Source, full documentation and issues: https://github.com/irc2torrent/irc2torrent
Licensed under MIT. qBittorrent is GPL-2.0-or-later, Flood is GPL-3.0; each retains its own license.
Content type
Image
Digest
sha256:2f04f6617…
Size
98.4 MB
Last updated
26 days ago
docker pull irc2torrent/flood_qbittorrent_irc2torrent