I ran my first IRC server on a dedicated Pentium 4 box under my desk in 2003. It had one job and it did it fine — until the hard drive failed and I lost every configuration file, TLS cert, and services database. That was the day I became religious about backups. Twenty years later, the same principle applies but the tools are better. Containers give you reproducible IRC server deployments where the config is versioned, the state is on a volume, and rebuilding the whole thing takes 90 seconds instead of an afternoon.
Why Containerize an IRC Server
IRC daemons are not hard to compile. UnrealIRCd's build process is a configure script and make. InspIRCd is similar. The compilation itself is not the problem. The problems are everything around it: dependency management across OS versions, configuration drift between test and production, TLS certificate renewal plumbing, and the ability to roll back a bad configuration change in seconds instead of hours.
Containers solve these problems cleanly:
- Reproducibility — The Dockerfile defines the exact build environment. A build that works today will work identically next year.
- Isolation — The IRC daemon runs in its own filesystem namespace. A compromised IRCd cannot traverse to the host.
- Portability — Move from a VPS to a dedicated server by copying three files: the Dockerfile, the compose file, and the config volume.
- Rollback — Tag your images. If a new UnrealIRCd version breaks something, roll back to the previous tag in one command.
The overhead is negligible. A containerized UnrealIRCd uses roughly 2-5 MB more RAM than a bare-metal installation. CPU overhead is unmeasurable for a service that spends 99% of its time waiting on I/O. The trade-off is entirely in your favor.
For teams managing multiple services and servers, this kind of infrastructure discipline matters. Organizations like PowerITi, which specialize in IT infrastructure management for small and mid-size businesses, have been pushing this exact approach — containerized, version-controlled service deployments — as a baseline for any production workload.
UnrealIRCd in Docker
There's no official UnrealIRCd Docker image. You build your own. This is actually preferable — you control the compile options, the base image, and the module selection.
# Dockerfile for UnrealIRCd 6.1.x
FROM debian:bookworm-slim AS builder
RUN apt-get update && apt-get install -y \
build-essential \
curl \
libssl-dev \
libcurl4-openssl-dev \
zlib1g-dev \
libpcre2-dev \
libargon2-dev \
libsodium-dev \
pkg-config \
&& rm -rf /var/lib/apt/lists/*
ARG UNREALIRCD_VERSION=6.1.7.1
WORKDIR /usr/src
RUN curl -sSL https://www.unrealircd.org/downloads/unrealircd-${UNREALIRCD_VERSION}.tar.gz \
| tar xz && mv unrealircd-${UNREALIRCD_VERSION} unrealircd
WORKDIR /usr/src/unrealircd
# Non-interactive configure
RUN ./Config \
--with-showlistmodes \
--enable-ssl \
--with-bindir=/opt/unrealircd/bin \
--with-datadir=/opt/unrealircd/data \
--with-confdir=/opt/unrealircd/conf \
--with-modulesdir=/opt/unrealircd/modules \
--with-logdir=/opt/unrealircd/logs \
--with-cachedir=/opt/unrealircd/cache \
--with-tmpdir=/opt/unrealircd/tmp \
--with-pidfile=/opt/unrealircd/data/unrealircd.pid \
--with-nicknamehistorylength=2000 \
--with-permissions=0600 \
--with-fd-setsize=4096 \
&& make -j$(nproc) && make install
# Runtime stage
FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y \
libssl3 \
libcurl4 \
zlib1g \
libpcre2-8-0 \
libargon2-1 \
libsodium23 \
&& rm -rf /var/lib/apt/lists/*
RUN useradd -r -s /bin/false -d /opt/unrealircd ircd
COPY --from=builder /opt/unrealircd /opt/unrealircd
RUN chown -R ircd:ircd /opt/unrealircd
USER ircd
WORKDIR /opt/unrealircd
EXPOSE 6667 6697 6900
CMD ["/opt/unrealircd/bin/unrealircd", "-F"]
The -F flag runs UnrealIRCd in the foreground, which is what Docker expects. Without it, the daemon forks and Docker thinks the process exited.
./Config script is interactive by default. In a Docker build, you must either patch the script to accept environment variables or use a pre-generated config.settings file. The approach above works for most configurations but may need adjustment for unusual module selections. Test the build locally before committing to CI.Build and run:
# Build the image
docker build -t unrealircd:6.1.7.1 .
# Run with config volume
docker run -d \
--name unrealircd \
-p 6667:6667 \
-p 6697:6697 \
-v ./conf:/opt/unrealircd/conf:ro \
-v ./data:/opt/unrealircd/data \
-v ./logs:/opt/unrealircd/logs \
-v ./certs:/opt/unrealircd/certs:ro \
--restart unless-stopped \
unrealircd:6.1.7.1
InspIRCd in Docker
InspIRCd is modular by design. Every feature — from TLS to SASL to channel modes — is a loadable module. This makes the Dockerfile simpler because you compile everything and enable modules through configuration, not compile flags.
# Dockerfile for InspIRCd 4.x
FROM debian:bookworm-slim AS builder
RUN apt-get update && apt-get install -y \
build-essential \
git \
cmake \
pkg-config \
libssl-dev \
libgnutls28-dev \
libmaxminddb-dev \
libpcre2-dev \
libsqlite3-dev \
libldap2-dev \
libtre-dev \
&& rm -rf /var/lib/apt/lists/*
ARG INSPIRCD_VERSION=v4.3.0
WORKDIR /usr/src
RUN git clone --branch ${INSPIRCD_VERSION} --depth 1 \
https://github.com/inspircd/inspircd.git
WORKDIR /usr/src/inspircd/build
RUN cmake .. \
-DCMAKE_INSTALL_PREFIX=/opt/inspircd \
-DINSPIRCD_EXTRA_MODULES="ssl_gnutls;ssl_openssl;ldap;regex_pcre2;geo_maxmind" \
&& cmake --build . -j$(nproc) \
&& cmake --install .
# Runtime stage
FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y \
libssl3 \
libgnutls30 \
libmaxminddb0 \
libpcre2-8-0 \
libsqlite3-0 \
libldap-2.5-0 \
libtre5 \
&& rm -rf /var/lib/apt/lists/*
RUN useradd -r -s /bin/false -d /opt/inspircd ircd
COPY --from=builder /opt/inspircd /opt/inspircd
RUN chown -R ircd:ircd /opt/inspircd
USER ircd
WORKDIR /opt/inspircd
EXPOSE 6667 6697
CMD ["/opt/inspircd/bin/inspircd", "--nofork"]
InspIRCd 4 uses CMake, which is cleaner for Docker builds than the older configure-based system. The --nofork flag serves the same purpose as UnrealIRCd's -F.
Docker Compose for Multi-Service Stacks
A production IRC setup typically involves the IRC daemon, a services package (Anope or Atheme), and sometimes a web frontend or API. Docker Compose ties these together.
# docker-compose.yml — Full IRC stack
version: "3.8"
services:
unrealircd:
build:
context: ./unrealircd
args:
UNREALIRCD_VERSION: "6.1.7.1"
container_name: unrealircd
ports:
- "6667:6667"
- "6697:6697"
- "6900:6900" # Server linking port
volumes:
- ./unrealircd/conf:/opt/unrealircd/conf:ro
- unrealircd-data:/opt/unrealircd/data
- unrealircd-logs:/opt/unrealircd/logs
- ./certs:/opt/unrealircd/certs:ro
networks:
- ircnet
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "echo PING | nc -q1 localhost 6667 | grep -q PONG || exit 1"]
interval: 60s
timeout: 10s
retries: 3
anope:
build:
context: ./anope
container_name: anope
volumes:
- ./anope/conf:/opt/anope/conf:ro
- anope-data:/opt/anope/data
- anope-logs:/opt/anope/logs
networks:
- ircnet
depends_on:
unrealircd:
condition: service_healthy
restart: unless-stopped
# Optional: WebIRC client (The Lounge)
thelounge:
image: thelounge/thelounge:latest
container_name: thelounge
ports:
- "9000:9000"
volumes:
- thelounge-data:/var/opt/thelounge
networks:
- ircnet
restart: unless-stopped
networks:
ircnet:
driver: bridge
volumes:
unrealircd-data:
unrealircd-logs:
anope-data:
anope-logs:
thelounge-data:
The key details: Anope connects to UnrealIRCd over the internal ircnet bridge network, so the services link port (6900) doesn't need to be exposed to the host. The health check ensures Anope only starts after UnrealIRCd is accepting connections. Configuration files are bind-mounted read-only; runtime data goes to named volumes.
docker run --rm -v volume_name:/data -v $(pwd):/backup busybox tar czf /backup/volume.tar.gz /data.TLS Certificates and Let's Encrypt
IRC servers need TLS. Port 6697 is the standard TLS port. Getting certificates from Let's Encrypt works, but the renewal process needs to be wired into the container lifecycle.
# Certbot on the host, shared into the container
# Initial certificate request
sudo certbot certonly --standalone \
-d irc.example.com \
--preferred-challenges http
# Copy certs to IRC server config directory
sudo cp /etc/letsencrypt/live/irc.example.com/fullchain.pem ./certs/
sudo cp /etc/letsencrypt/live/irc.example.com/privkey.pem ./certs/
# UnrealIRCd TLS config snippet (unrealircd.conf)
# Place in conf/ directory, bind-mounted into the container
tls {
certificate "/opt/unrealircd/certs/fullchain.pem";
key "/opt/unrealircd/certs/privkey.pem";
options {
no-client-certificate;
};
ciphers "TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:ECDHE-ECDSA-AES256-GCM-SHA384";
tlsv1;
tlsv1.1;
};
# Renewal hook script — /etc/letsencrypt/renewal-hooks/deploy/irc-reload.sh
#!/bin/bash
DOMAIN="irc.example.com"
CERT_DIR="/path/to/your/irc/certs"
cp /etc/letsencrypt/live/$DOMAIN/fullchain.pem $CERT_DIR/
cp /etc/letsencrypt/live/$DOMAIN/privkey.pem $CERT_DIR/
chmod 600 $CERT_DIR/*.pem
# Signal UnrealIRCd to reload TLS
docker exec unrealircd /opt/unrealircd/bin/unrealircd rehash
# Or for InspIRCd:
# docker exec inspircd /opt/inspircd/bin/inspircd --rehash
The rehash command reloads the configuration — including TLS certificates — without disconnecting any users. Schedule certbot renew in cron and the deploy hook handles the rest automatically.
Podman as an Alternative
Podman is a daemonless container engine that's compatible with Docker's image format and most of its CLI syntax. For IRC servers, Podman has two advantages: it runs rootless by default, and it doesn't require a long-running daemon process.
# Building with Podman (same Dockerfile)
podman build -t unrealircd:6.1.7.1 .
# Running rootless
podman run -d \
--name unrealircd \
-p 6667:6667 \
-p 6697:6697 \
-v ./conf:/opt/unrealircd/conf:ro,Z \
-v unrealircd-data:/opt/unrealircd/data:Z \
-v ./certs:/opt/unrealircd/certs:ro,Z \
unrealircd:6.1.7.1
# Generate systemd unit for auto-start
podman generate systemd --name unrealircd --files --new
mv container-unrealircd.service ~/.config/systemd/user/
systemctl --user daemon-reload
systemctl --user enable container-unrealircd.service
The :Z suffix on volume mounts tells Podman to set the correct SELinux context. If you're on a system without SELinux (Debian, Ubuntu), omit it.
Podman's rootless mode means the IRC daemon runs without any elevated privileges at all — not even the container runtime has root access. This is a meaningful security improvement for a network-facing service like an IRC daemon, which is a direct target for exploitation attempts.
net.ipv4.ip_unprivileged_port_start on the host or use a reverse proxy.For Podman users, podman-compose or podman play kube can handle multi-container setups similar to Docker Compose. The podman-compose tool reads the same docker-compose.yml format, though compatibility is not 100% — test before relying on it for production.
Whether you pick Docker or Podman, the pattern is the same: build the image from a Dockerfile, mount config and data as volumes, expose the ports, and let the container runtime handle process lifecycle. It took me twenty years to get from "bare-metal Pentium 4 under the desk" to "containerized IRC stack deployed in 90 seconds." The IRC daemon hasn't changed that much. Everything around it has.
FAQ
Will containerizing my IRC server add latency?
No measurable latency. Docker's bridge networking adds microseconds of overhead per packet. IRC is a low-throughput, latency-tolerant protocol — even a busy server handles thousands of messages per second, which is trivial for container networking. The bottleneck in any IRC deployment is the upstream network link, not the container runtime.
Can I link two containerized IRC servers together?
Yes. If both servers are on the same host, use a shared Docker network. If they're on different hosts, expose the linking port (typically 6900 or a custom port) and configure the link block in each server's config to point to the other host's public IP. TLS on the link is strongly recommended — use a separate certificate or the same Let's Encrypt cert if the hostname matches.
How do I handle IRC server logs in Docker?
Mount a named volume or bind mount for the log directory. Alternatively, configure the IRC daemon to log to stdout/stderr and use Docker's built-in log driver (docker logs unrealircd). For production, use the json-file log driver with rotation: --log-opt max-size=10m --log-opt max-file=3. Avoid the default unlimited logging — IRC servers generate a lot of log output over time.
Should I use Alpine or Debian as the base image?
Debian (bookworm-slim) for IRC daemons. Alpine uses musl libc, which causes subtle issues with DNS resolution, locale handling, and some C library calls that IRC daemons rely on. The size difference (40 MB vs 80 MB for the runtime image) is not worth the debugging time when something breaks on musl. I've been bitten by this twice with UnrealIRCd and once with Anope.