How I Got qwebirc Running in Docker (2025-proof, no Apache, works with Cloudflare Tunnel)
qwebirc is awesome, but it’s Python 2 + Twisted era software. On modern hosts that means:
- you need Debian’s archived packages (buster is EOL),
- you must run it non-root,
- and you have to start it in a way that keeps the container alive.
Here’s exactly what I did to make it work, end-to-end.
1) Build a Python-2/Twisted image from the Debian archive
Create ~/qwebirc-docker/Dockerfile
:
FROM python:2.7-slim
# buster is EOL → point apt to the archive, then install prebuilt Py2 deps
RUN sed -i 's|deb.debian.org|archive.debian.org|g' /etc/apt/sources.list \
&& sed -i 's|security.debian.org|archive.debian.org|g' /etc/apt/sources.list \
&& apt-get -o Acquire::Check-Valid-Until=false update \
&& apt-get install -y --no-install-recommends \
git ca-certificates adduser \
python-twisted python-twisted-web \
python-zope.interface python-constantly python-automat \
python-hyperlink python-incremental python-openssl \
python-autobahn python-simplejson \
&& rm -rf /var/lib/apt/lists/*
# get qwebirc
WORKDIR /opt
RUN git clone https://github.com/qwebirc/qwebirc.git
# run as non-root so qwebirc doesn't refuse to start
RUN adduser --system --home /opt/qwebirc --ingroup nogroup qwebirc \
&& chown -R qwebirc:nogroup /opt/qwebirc
USER qwebirc
WORKDIR /opt/qwebirc
EXPOSE 9090
DockerfileBuild it:
cd ~/qwebirc-docker
sudo docker build --no-cache -t qwebirc .
Nix2) First boot (debug mode) to prime Twisted & confirm it runs
Start a long-running shell so the container stays up:
sudo docker rm -f qwebirc 2>/dev/null || true
sudo docker run -d --name qwebirc -p 9090:9090 qwebirc sleep infinity
NixPrime Twisted’s dropin cache (harmless but fixes odd starts-and-stops):
sudo docker exec -u 0 qwebirc /bin/sh -lc "/usr/bin/twistd --help || true"
NixCreate a default config and run in the foreground once (see any errors live):
sudo docker exec -it qwebirc /bin/sh -lc '\
cp -n /opt/qwebirc/config.py.example /opt/qwebirc/config.py; \
/usr/bin/python /opt/qwebirc/compile.py || true; \
exec /usr/bin/python /opt/qwebirc/run.py 0.0.0.0 9090'
NixYou’ll see a Java/minify warning — that’s fine. Open http://<docker-host-ip>:9090
to confirm the UI.
Ctrl+C
to stop the foreground run.
3) Stable detached run (the bit that finally sticks)
By default qwebirc can exit after compile. Run it in the background and keep the container alive:
sudo docker rm -f qwebirc 2>/dev/null || true
sudo docker run -d --name qwebirc -p 9090:9090 qwebirc /bin/sh -lc '
cd /opt/qwebirc
[ -f config.py ] || cp config.py.example config.py
/usr/bin/python compile.py || true
/usr/bin/python run.py 0.0.0.0 9090 &
tail -f /dev/null
'
NixCheck:
sudo docker ps --filter name=qwebirc
sudo docker logs qwebirc
curl -I http://<docker-host-ip>:9090/
Nix4) Editing config (three easy ways)
A) Quick copy in/out
sudo docker cp qwebirc:/opt/qwebirc/config.py ~/qwebirc-docker/config.py
nano ~/qwebirc-docker/config.py
sudo docker cp ~/qwebirc-docker/config.py qwebirc:/opt/qwebirc/config.py
sudo docker restart qwebirc
NixB) Bind-mount (best for ongoing edits)
mkdir -p ~/qwebirc-docker/config
sudo docker run --rm -v ~/qwebirc-docker/config:/mnt qwebirc \
/bin/sh -lc 'cp -n /opt/qwebirc/config.py.example /mnt/config.py'
# edit on host
nano ~/qwebirc-docker/config/config.py
# run with the mounted config
sudo docker rm -f qwebirc 2>/dev/null || true
sudo docker run -d --name qwebirc -p 9090:9090 \
-v ~/qwebirc-docker/config/config.py:/opt/qwebirc/config.py:ro \
qwebirc /bin/sh -lc '
cd /opt/qwebirc
/usr/bin/python compile.py || true
/usr/bin/python run.py 0.0.0.0 9090 &
tail -f /dev/null
'
NixC) Edit inside the container
sudo docker exec -it qwebirc /bin/sh
# (install nano temporarily if you really want)
# sudo docker exec -u 0 qwebirc sh -lc 'apt-get -o Acquire::Check-Valid-Until=false update && apt-get install -y nano'
nano /opt/qwebirc/config.py
sudo docker restart qwebirc
NixWhat to set in config.py
: your IRC host/ports (6667/6697), network name/branding, optional WEBIRC password to get real client IPs on the IRCd, and the admin engine allowlist.
And there we have it! qwebirc running in a docker container like a boss!


Troubleshooting gotchas (the ones that bit me)
- ZoPe/Twisted “missing”: Use
/usr/bin/python
(system Python) because apt installs Py2 modules under/usr/lib/python2.7/dist-packages
. - “Refusing to run as root”: run as a non-root user (the Dockerfile creates
qwebirc
). Alternatively--user 1000:1000
. - Container exits right away: start
run.py
in the background and keep PID1 alive withtail -f /dev/null
(or use a tiny entrypoint script withexec …
). - Entry script “exec format error”: your script probably had CRLF line endings; fix with
sed -i 's/\r$//' entrypoint.sh
. - Debian apt 404s: buster is EOL. Use
archive.debian.org
+-o Acquire::Check-Valid-Until=false
. - Java warnings: harmless. They only affect JS/CSS minification during
compile.py
.
Final one-liner I’m using to run it
sudo docker rm -f qwebirc 2>/dev/null || true
sudo docker run -d --name qwebirc -p 9090:9090 \
-v ~/qwebirc-docker/config/config.py:/opt/qwebirc/config.py:ro \
qwebirc /bin/sh -lc '
cd /opt/qwebirc
/usr/bin/python compile.py || true
/usr/bin/python run.py 0.0.0.0 9090 &
tail -f /dev/null
'
NixThis keeps it rock-solid, easy to edit, and ready to sit behind your Cloudflare Tunnel.