16 KB

Dockerized ejabberd XMPP server with Biboumi IRC gateway


This setup packs a ejabberd XMPP Server with a Biboumi IRC Gateway in a docker-compose.yml file.

The whole service lives in a single folder to ease backup and migration. All persistent data is stored in ./data/. All configurations are made in ejabberd.yml and in a .env file. For consistent backups, a cronjob one-liner, running in the ejabberd container, dumps daily the ejabberd DB into ./data/backup/.

It is assumed that on the docker host we're already running

  1. a jwilder/nginx-proxy reverse proxy
  2. together with jrcs/letsencrypt-nginx-proxy-companion to obtain letsencrypt TLS certificates,
  3. as well as watchtower to keep images up-to-date.

Used docker images

  • This setup uses ejabberd/ecs from the ProcessOne ejabberd developers. The underlying Dockerfile builds the server from source and installs it into a Alpine image. The server runs as unprivileged user (uid 9000). This image doesn't come with many parameters or helper scripts, so all configurations are made in the ejabberd.yml configuration file. rroemhild/docker-ejabberd was not used as it didn't run reliably for me in July 2018. Still it is worth to take a look at, as it uses parameters and helper scripts to learn from.
  • Biboumi runs from louiz/biboumi. The image is provided by the Biboumi developer himself and only contains the binary as it is based on FROM scratch. All parameters are set using environment variables defined in docker-compose.yml. The server runs under a unprivileged user (uid 1000).

Open Ports

Marked [x] ports will be opened on the Docker host, so they should not be used by another service on the same host.

  • 5222:5222 Client 2 Server — bare minimum to speak with clients
  • 5223:5223 XMPP over TLS is actually deprecated, instead STARTTLS on port 5222 should be used. It is active for backward compatibility or could be moved to port 443 to ease usage behind firewalls.
  • 5269:5269 Server 2 Server — only needed when we want to speak with users from other servers
  • 5280:5280 Web admin (disabled), BOSH and WebSocket
  • 5443:5443 HTTP Upload
  • 113:8113 Identd so IRC servers can differentiate between users
  • 443!8080 Integrated webserver to serve static content, such as a javascript xmpp client. The port is exposed via the reverse proxy.
  • 5347:5347 Only used for internal unencrypted communication with the Biboumi (IRC gateway) component
  • ----:4560 XMLRPC — API, disabled

BOSH (previously known as 'HTTP binding' or "http-bind") is a technology to use XMPP over HTTP. This allows XMPP applications to run in web pages, but also any other HTTP-only environment such as behind restrictive firewalls.

Used volumes

ejabber runs under user ejabberd with uid/guid 9000. All data is stored in /home/ejabberd/ within the container.

  • ./data/conf/ejabberd.yml — main configuration file
  • .data/database/ — Erlang DB used to store all application data
  • .data/backup/ — place to to store DB dumps
  • .data/uploads/ — files transferred between users
  • .data/cron/ — job to daily dump the DB
  • ./opt/docker/proxy/data/certs/ — certificates and key material (to be changed as per the local docker host setup)
  • .data/www/ — static web content
  • ./data/biboumi/database/ — sqlite DB for Biboumi. The Biboumi configuration is defined via environment variables in docker-compose.yml.

DNS Setup

To allow service discovery, following DNS records have to be created.

im                              3600 IN A     
*.im                            3600 IN CNAME           in TTL class SRV priority weight port target  3600 IN SRV    5 0 5222 3600 IN SRV   10 0 5223  3600 IN SRV    5 0 5269

To check the records:

Installation & Configuration

  1. Prepare data folders with proper permissions.

    mkdir -p $inst_dir/data/conf \
             $inst_dir/data/database \
             $inst_dir/data/backup \
             $inst_dir/data/upload \
             $inst_dir/data/cron \
             $inst_dir/data/www \
    && chown -R 9000:9000 $inst_dir/data/
    mkdir -p $inst_dir/data/biboumi/database
    chown -R 1000:1000 $inst_dir/data/biboumi/
  2. Obtain a vanilla ejabberd.yml configuration file. We'll briefly spawn a volatile container and copy the file to the host.

    docker run -d --rm --name ejabberd ejabberd/ecs
    docker cp ejabberd:/home/ejabberd/conf/ejabberd.yml $inst_dir/data/conf/
    docker kill ejabberd
  3. Configure the server / tune ejabberd.yml
    # Set the hostname in .env to be used in docker-compose.yml
    echo "HOSTNAME=$hostname" > $inst_dir/.env
    # Set hostname and disable localhost configuration as we have no certificate for it
    sed -i \
      -e "s/^hosts:/hosts:\\n  - \"$hostname\"/" \
      -e '/^  - "localhost"/s/^/# /' \
    ## Disable web admin, api and oauth as not used and to reduce attack surface
    sed -i \
      -e '/web_admin: true/s/^/# /' \
      -e '/\/api/s/^/# /' \
      -e '/\/oauth/s/^/# /' \
    ## Enable TLS for BOSH and WebSockets
    sed -i \
      -e '/\/bosh/a\    tls: true' \
    ## Logs: Rotate at 1MB, don't keep old versions
    # The server is started in foreground and therefore logs are collected by docker,
    # so we actually don't need logs at all and can rotate rapidly without keeping old version
    sed -i \
      -e '/log_rotate_size/s/10485760/1048576/' \
      -e '/log_rotate_count/s/1/0/' \
    ## Point to letsencrypt certificates from jrcs/letsencrypt-nginx-proxy-companion,
    ## for details, see:
    sed -i \
      -e '/^certfiles/a\  - \"/home/ejabberd/ssl/*.pem\"' \
      -e '/server.pem/s/^/# /' \
    ## Enforce TLS connection for c2s
    sed -i \
      -e '/    ## starttls_required: true/a\    starttls_required: true' \
    ## Enable XEP-0313 Message Archive Management,
    ## for details, see:
    sed -i \
      -e '/  ## mod_mam/a\  mod_mam: {}' \
    ## Enable OMEMO (XEP-0384),
    ## for details, see:
    sed -i \
      -e '/OMEMO/,+3 s/^/#/' \
    ## Enable Roster Versioning (XEP-0237)
    sed -i \
      -e '/^  mod_roster: {}/a\  mod_roster:\n    versioning: true' \
      -e '/^  mod_roster: {}/s/^/#/' \
    ## Enable SOCKS5 Bytestreams Proxy (XEP-0065)
    sed -i -e '/mod_proxy65/s/## //' $config_file
    ## Enable legacy XMPP via TLS (XEP-0368)
    cat > $heredoc <<'EOF'
        port: 5223
        ip: "::"
        module: ejabberd_c2s
        tls: true
        max_stanza_size: 65536
        shaper: c2s_shaper
        access: c2s
    sed -i "/^listen:$/r $heredoc" $config_file
    rm $heredoc
    ## Enable HTTP File Upload Listener(XEP-0363)
    cat > $heredoc <<'EOF'
        port: 5443
        ip: "::"
        module: ejabberd_http
          "": mod_http_upload
        tls: true
    sed -i "/^listen:$/r $heredoc" $config_file
    rm $heredoc
    ## Enable HTTP File Upload Module
    ## with quota the keep files max 3 days and max size of 5MB per file
    cat > $heredoc <<'EOF'
        put_url: "https://@HOST@:5443/upload"
        thumbnail: false # otherwise needs ejabberd to be compiled with libgd support
        max_size: 524288 # 5MB
        max_days: 3
    sed -i "/^modules:$/r $heredoc" $config_file
    rm $heredoc
    ## Add component for the IRC gateway
    cat > $heredoc <<EOF
        port: 5347
        ip: "::"
        module: ejabberd_service
        access: all
            password: "secret"
    sed -i "/^listen:$/r $heredoc" $config_file
    rm $heredoc
    ## Enable simple fileserver
    cat > $heredoc <<'EOF'
        port: 8080
        ip: "::"
        module: ejabberd_http
          "/": mod_http_fileserver
    sed -i "/^listen:$/r $heredoc" $config_file
    rm $heredocmkdir -p $inst_dir/data/biboumi/database
    chown -R 1000:1000 $inst_dir/data/biboumi/
    sed -i \
      -e '/## mod_http_fileserver:/s/## //' \
      -e '/mod_http_fileserver:/a\    default_content_type: "text/html"' \
      -e '/mod_http_fileserver:/a\    docroot: "/var/www/"' \
      -e '/mod_http_fileserver:/a\    directory_indices:\n      - "index.html"' \
    ## Disable legacy SSL protocols and ciphers
    sed -i \
      -e '/c2s_protocol_options:/ac2s_protocol_options:\n  - "no_sslv2"\n  - "no_sslv3"\n  - "no_tlsv1"\n  - "no_tlsv1_1"\n' \
      -e '/s2s_protocol_options:/as2s_protocol_options:\n  - "no_sslv2"\n  - "no_sslv3"\n' \
      -e '/^    port: 5443/a\    protocol_options:\n      - "no_sslv2"\n      - "no_sslv3"\n      - "no_tlsv1"\n      - "no_tlsv1_1"\n    ciphers: "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA:ECDHE-ECDSA-AES128-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA256"\n' \
      -e '/^    port: 5280/a\    protocol_options:\n      - "no_sslv2"\n      - "no_sslv3"\n      - "no_tlsv1"\n      - "no_tlsv1_1"\n    ciphers: "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA:ECDHE-ECDSA-AES128-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA256"\n' \
  4. Setup the cronjob for dumping daily the DB

    cat >$inst_dir/data/cron/  <<'EOF'
    /home/ejabberd/bin/ejabberdctl backup /home/ejabberd/backup/ejabberd.backup
    chmod a+x $inst_dir/data/cron/
  5. Start the container. During the first startup the databases will be created.

    docker-compose up -d
  6. Create users

    ejabberd comes by default with no users. Privileges and ACLs for users are set in ejabberd.yml. In the default configuration file admin@localhost is set as privileged user already.

    To create new users, run from the docker host: docker exec -it ejabberd /home/ejabberd/bin/ejabberdctl register <user> <domain> <password>

    We'll need to create at least a user for the IRC gateway.

    pass=`apg -q -n1 -m12`
    docker exec -it ejabberd /home/ejabberd/bin/ejabberdctl register $user $hostname $pass
    Further self-registration or LDAP authentication can be enabled in `ejabberd.yml`.
  7. (Optional) Run the XMPP compliance tester

    docker run --rm -it --name=xmpptest openjdk:alpine \
    /bin/sh -c "wget $dl_url ; java -jar $jar_file $user@$domain $pass" \
    | tee $domain.txt
  8. (Optional) Setup a static website data/www/ can be used to serve a static site, e.g. for presenting compliance test results, a web xmpp client or client setup instructions

    cd $inst_dir/data/www
    git clone --depth=1
    mv converse.js webchat
    cp webchat/fullscreen.html webchat/index.html
    sed -i \
      -e '/analytics/d' \
      -e '/piwik/d' \
      -e "/bosh_service_url/s/$hostname:5280/" \
      -e "/bosh_service_url/s/http-bind/bosh/" \


Get a shell in the container

docker exec -it ejabberd sh

Control the server

docker exec -it ejabberd /home/ejabberd/bin/ejabberdctl \

  • registered_users <host> List all registered users in HOST
  • unregister <user> <host> List all registered users in HOST
  • modules_available List the contributed modules available to install
  • modules_installed List the contributed modules already installed
  • mnesia Get details about the database
  • reload_config Reload ejabberd configuration file into memory (this will not start new servers)
  • connected_users list connected users with their resources
  • backup /home/ejabberd/backup/ejabberd.backup Backup database
  • install_fallback /home/ejabberd/backup/ejabberd.backup restores the db and makes it active after the next restart
  • help lists available commands

Biboumi documentation

Check if the IRC server certificate is valid



  • Jingle? Jingle Video? WebRTC? STUN/TURN server to find ports for video calls, e.g. for Jingle ICE?
  • Jabber transport for Skype, e.g. spectrum
  • mirror vhost using mod_echo (this requires moving general configs in a vhost)

Notes and pitfalls

The Node and DB name changes with the hostname

Docker's randomly generated hostnames causes ejabberd to calculate different unique node and DB name. To prevent a new DB beeing created with each container restart, use docker run --hostname <hostname> or hostname: and domainname: in the docker-compose.yml.

/opt/docker/proxy/data/certs/<hostname> should be readable by ejabberd user.