Skip to content

GitHub tag (latest SemVer) ejabberd Container on GitHub ecs Container on Docker

ejabberd Container Image

ejabberd is an open-source, robust, scalable and extensible realtime platform built using Erlang/OTP, that includes XMPP Server, MQTT Broker and SIP Service.

This document explains how to use the ejabberd container image available in ghcr.io/processone/ejabberd, built using the files in .github/container/. This image is based in Alpine 3.19, includes Erlang/OTP 26.2 and Elixir 1.16.1.

Alternatively, there is also the ecs container image available in docker.io/ejabberd/ecs, built using the docker-ejabberd/ecs repository. Check the differences between ejabberd and ecs images.

If you are using a Windows operating system, check the tutorials mentioned in ejabberd Docs > Docker Image.

Start ejabberd

With default configuration

Start ejabberd in a new container:

docker run --name ejabberd -d -p 5222:5222 ghcr.io/processone/ejabberd

That runs the container as a daemon, using ejabberd default configuration file and XMPP domain localhost.

Stop the running container:

docker stop ejabberd

Restart the stopped ejabberd container:

docker restart ejabberd

Start with Erlang console attached

Start ejabberd with an Erlang console attached using the live command:

docker run --name ejabberd -it -p 5222:5222 ghcr.io/processone/ejabberd live

That uses the default configuration file and XMPP domain localhost.

Start with your configuration and database

Pass a configuration file as a volume and share the local directory to store database:

mkdir database
chown ejabberd database

cp ejabberd.yml.example ejabberd.yml

docker run --name ejabberd -it \
  -v $(pwd)/ejabberd.yml:/opt/ejabberd/conf/ejabberd.yml \
  -v $(pwd)/database:/opt/ejabberd/database \
  -p 5222:5222 ghcr.io/processone/ejabberd live

Notice that ejabberd runs in the container with an account named ejabberd, and the volumes you mount must grant proper rights to that account.

Next steps

Register the administrator account

The default ejabberd configuration does not grant admin privileges to any account, you may want to register a new account in ejabberd and grant it admin rights.

Register an account using the ejabberdctl script:

docker exec -it ejabberd ejabberdctl register admin localhost passw0rd

Then edit conf/ejabberd.yml and add the ACL as explained in ejabberd Docs: Administration Account

Check ejabberd log files

Check the content of the log files inside the container, even if you do not put it on a shared persistent drive:

docker exec -it ejabberd tail -f logs/ejabberd.log

Inspect the container files

The container uses Alpine Linux. Start a shell inside the container:

docker exec -it ejabberd sh

Open ejabberd debug console

Open an interactive debug Erlang console attached to a running ejabberd in a running container:

docker exec -it ejabberd ejabberdctl debug

CAPTCHA

ejabberd includes two example CAPTCHA scripts. If you want to use any of them, first install some additional required libraries:

docker exec --user root ejabberd apk add imagemagick ghostscript-fonts bash

Now update your ejabberd configuration file, for example:

docker exec -it ejabberd vi conf/ejabberd.yml

and add this option:

captcha_cmd: /opt/ejabberd-22.04/lib/captcha.sh

Finally, reload the configuration file or restart the container:

docker exec ejabberd ejabberdctl reload_config

If the CAPTCHA image is not visible, there may be a problem generating it (the ejabberd log file may show some error message); or the image URL may not be correctly detected by ejabberd, in that case you can set the correct URL manually, for example:

captcha_url: https://localhost:5443/captcha

For more details about CAPTCHA options, please check the CAPTCHA documentation section.

Advanced Container Configuration

Ports

This container image exposes the ports:

  • 5222: The default port for XMPP clients.
  • 5269: For XMPP federation. Only needed if you want to communicate with users on other servers.
  • 5280: For admin interface.
  • 5443: With encryption, used for admin interface, API, CAPTCHA, OAuth, Websockets and XMPP BOSH.
  • 1883: Used for MQTT
  • 4369-4399: EPMD and Erlang connectivity, used for ejabberdctl and clustering
  • 5210: Erlang connectivity when ERL_DIST_PORT is set, alternative to EPMD

Volumes

ejabberd produces two types of data: log files and database spool files (Mnesia). This is the kind of data you probably want to store on a persistent or local drive (at least the database).

The volumes you may want to map:

  • /opt/ejabberd/conf/: Directory containing configuration and certificates
  • /opt/ejabberd/database/: Directory containing Mnesia database. You should back up or export the content of the directory to persistent storage (host storage, local storage, any storage plugin)
  • /opt/ejabberd/logs/: Directory containing log files
  • /opt/ejabberd/upload/: Directory containing uploaded files. This should also be backed up.

All these files are owned by ejabberd user inside the container.

It's possible to install additional ejabberd modules using volumes, this comment explains how to install an additional module using docker-compose.

Commands on start

The ejabberdctl script reads the CTL_ON_CREATE environment variable the first time the container is started, and reads CTL_ON_START every time the container is started. Those variables can contain one ejabberdctl command, or several commands separated with the blankspace and ; characters.

By default failure of any of commands executed that way would abort start, this can be disabled by prefixing commands with !

Example usage (or check the full example):

    environment:
      - CTL_ON_CREATE=! register admin localhost asd
      - CTL_ON_START=stats registeredusers ;
                     check_password admin localhost asd ;
                     status

Macros in environment

ejabberd reads EJABBERD_MACRO_* environment variables and uses them to define the * macros, overwriting the corresponding macro definition if it was set in the configuration file. This is supported since ejabberd 24.12.

For example, if you configure this in ejabberd.yml:

acl:
  admin:
    user: ADMINJID

now you can define the admin account JID using an environment variable:

    environment:
      - EJABBERD_MACRO_ADMINJID=admin@localhost

Check the full example for other example.

Clustering

When setting several containers to form a cluster of ejabberd nodes, each one must have a different Erlang Node Name and the same Erlang Cookie.

For this you can either:

  • edit conf/ejabberdctl.cfg and set variables ERLANG_NODE and ERLANG_COOKIE
  • set the environment variables ERLANG_NODE_ARG and ERLANG_COOKIE

Example to connect a local ejabberdctl to a containerized ejabberd:

  1. When creating the container, export port 5210, and set ERLANG_COOKIE:
    docker run --name ejabberd -it \
      -e ERLANG_COOKIE=`cat $HOME/.erlang.cookie` \
      -p 5210:5210 -p 5222:5222 \
      ghcr.io/processone/ejabberd
    
  2. Set ERL_DIST_PORT=5210 in ejabberdctl.cfg of container and local ejabberd
  3. Restart the container
  4. Now use ejabberdctl in your local ejabberd deployment

To connect using a local ejabberd script:

ERL_DIST_PORT=5210 _build/dev/rel/ejabberd/bin/ejabberd ping

Example using environment variables (see full example docker-compose.yml):

    environment:
      - ERLANG_NODE_ARG=ejabberd@node7
      - ERLANG_COOKIE=dummycookie123

Build a Container Image

This container image includes ejabberd as a standalone OTP release built using Elixir. That OTP release is configured with:

  • mix.exs: Customize ejabberd release
  • vars.config: ejabberd compilation configuration options
  • config/runtime.exs: Customize ejabberd paths
  • ejabberd.yml.template: ejabberd default config file

Direct build

Build ejabberd Community Server container image from ejabberd master git repository:

docker buildx build \
    -t personal/ejabberd \
    -f .github/container/Dockerfile \
    .

Podman build

To build the image using Podman, please notice:

  • EXPOSE 4369-4399 port range is not supported, remove that in Dockerfile
  • It mentions that healthcheck is not supported by the Open Container Initiative image format
  • to start with command live, you may want to add environment variable EJABBERD_BYPASS_WARNINGS=true
podman build \
    -t ejabberd \
    -f .github/container/Dockerfile \
    .

podman run --name eja1 -d -p 5222:5222 localhost/ejabberd

podman exec eja1 ejabberdctl status

podman exec -it eja1 sh

podman stop eja1

podman run --name eja1 -it -e EJABBERD_BYPASS_WARNINGS=true -p 5222:5222 localhost/ejabberd live

Package build for arm64

By default, .github/container/Dockerfile builds this container by directly compiling ejabberd, it is a fast and direct method. However, a problem with QEMU prevents building the container in QEMU using Erlang/OTP 25 for the arm64 architecture.

Providing --build-arg METHOD=package is an alternate method to build the container used by the Github Actions workflow that provides amd64 and arm64 container images. It first builds an ejabberd binary package, and later installs it in the image. That method avoids using QEMU, so it can build arm64 container images, but is extremely slow the first time it's used, and consequently not recommended for general use.

In this case, to build the ejabberd container image for arm64 architecture:

docker buildx build \
    --build-arg METHOD=package \
    --platform linux/arm64 \
    -t personal/ejabberd:$VERSION \
    -f .github/container/Dockerfile \
    .

Composer Examples

Minimal Example

This is the barely minimal file to get a usable ejabberd.

If using Docker, write this docker-compose.yml file and start it with docker-compose up:

services:
  main:
    image: ghcr.io/processone/ejabberd
    container_name: ejabberd
    ports:
      - "5222:5222"
      - "5269:5269"
      - "5280:5280"
      - "5443:5443"

If using Podman, write this minimal.yml file and start it with podman kube play minimal.yml:

apiVersion: v1

kind: Pod

metadata:
  name: ejabberd

spec:
  containers:

  - name: ejabberd
    image: ghcr.io/processone/ejabberd
    ports:
    - containerPort: 5222
      hostPort: 5222
    - containerPort: 5269
      hostPort: 5269
    - containerPort: 5280
      hostPort: 5280
    - containerPort: 5443
      hostPort: 5443

Customized Example

This example shows the usage of several customizations: it uses a local configuration file, defines a configuration macro using an environment variable, stores the mnesia database in a local path, registers an account when it's created, and checks the number of registered accounts every time it's started.

Download or copy the ejabberd configuration file:

wget https://raw.githubusercontent.com/processone/ejabberd/master/ejabberd.yml.example
mv ejabberd.yml.example ejabberd.yml

Use a macro in ejabberd.yml to set the served vhost, with localhost as default value:

define_macro:
  XMPPHOST: localhost

hosts:
  - XMPPHOST

Create the database directory and allow the container access to it:

mkdir database
sudo chown 9000:9000 database

If using Docker, write this docker-compose.yml file and start it with docker-compose up:

version: '3.7'

services:

  main:
    image: ghcr.io/processone/ejabberd
    container_name: ejabberd
    environment:
      - EJABBERD_MACRO_XMPPHOST=example.com
      - CTL_ON_CREATE=register admin example.com asd
      - CTL_ON_START=registered_users example.com ;
                     status
    ports:
      - "5222:5222"
      - "5269:5269"
      - "5280:5280"
      - "5443:5443"
    volumes:
      - ./ejabberd.yml:/opt/ejabberd/conf/ejabberd.yml:ro
      - ./database:/opt/ejabberd/database

If using Podman, write this custom.yml file and start it with podman kube play custom.yml:

apiVersion: v1

kind: Pod

metadata:
  name: ejabberd

spec:
  containers:

  - name: ejabberd
    image: ghcr.io/processone/ejabberd
    env:
    - name: CTL_ON_CREATE
      value: register admin example.com asd
    - name: CTL_ON_START
      value: registered_users example.com ;
             status
    ports:
    - containerPort: 5222
      hostPort: 5222
    - containerPort: 5269
      hostPort: 5269
    - containerPort: 5280
      hostPort: 5280
    - containerPort: 5443
      hostPort: 5443
    volumeMounts:
    - mountPath: /opt/ejabberd/conf/ejabberd.yml
      name: config
      readOnly: true
    - mountPath: /opt/ejabberd/database
      name: db

  volumes:
  - name: config
    hostPath:
      path: ./ejabberd.yml
      type: File
  - name: db
    hostPath:
      path: ./database
      type: DirectoryOrCreate

Clustering Example

In this example, the main container is created first. Once it is fully started and healthy, a second container is created, and once ejabberd is started in it, it joins the first one.

An account is registered in the first node when created (and we ignore errors that can happen when doing that - for example when account already exists), and it should exist in the second node after join.

Notice that in this example the main container does not have access to the exterior; the replica exports the ports and can be accessed.

If using Docker, write this docker-compose.yml file and start it with docker-compose up:

version: '3.7'

services:

  main:
    image: ghcr.io/processone/ejabberd
    container_name: ejabberd
    environment:
      - ERLANG_NODE_ARG=ejabberd@main
      - ERLANG_COOKIE=dummycookie123
      - CTL_ON_CREATE=! register admin localhost asd

  replica:
    image: ghcr.io/processone/ejabberd
    container_name: replica
    depends_on:
      main:
        condition: service_healthy
    environment:
      - ERLANG_NODE_ARG=ejabberd@replica
      - ERLANG_COOKIE=dummycookie123
      - CTL_ON_CREATE=join_cluster ejabberd@main
      - CTL_ON_START=registered_users localhost ;
                     status
    ports:
      - "5222:5222"
      - "5269:5269"
      - "5280:5280"
      - "5443:5443"

If using Podman, write this cluster.yml file and start it with podman kube play cluster.yml:

apiVersion: v1

kind: Pod

metadata:
  name: cluster

spec:
  containers:

  - name: first
    image: ghcr.io/processone/ejabberd
    env:
    - name: ERLANG_NODE_ARG
      value: main@cluster
    - name: ERLANG_COOKIE
      value: dummycookie123
    - name: CTL_ON_CREATE
      value: register admin localhost asd
    - name: CTL_ON_START
      value: stats registeredusers ;
             status
    - name: EJABBERD_MACRO_PORT_C2S
      value: 6222
    - name: EJABBERD_MACRO_PORT_C2S_TLS
      value: 6223
    - name: EJABBERD_MACRO_PORT_S2S
      value: 6269
    - name: EJABBERD_MACRO_PORT_HTTP_TLS
      value: 6443
    - name: EJABBERD_MACRO_PORT_HTTP
      value: 6280
    - name: EJABBERD_MACRO_PORT_MQTT
      value: 6883
    - name: EJABBERD_MACRO_PORT_PROXY65
      value: 6777
    volumeMounts:
    - mountPath: /opt/ejabberd/conf/ejabberd.yml
      name: config
      readOnly: true

  - name: second
    image: ghcr.io/processone/ejabberd
    env:
    - name: ERLANG_NODE_ARG
      value: replica@cluster
    - name: ERLANG_COOKIE
      value: dummycookie123
    - name: CTL_ON_CREATE
      value: join_cluster main@cluster ;
             started ;
             list_cluster
    - name: CTL_ON_START
      value: stats registeredusers ;
             check_password admin localhost asd ;
             status
    ports:
    - containerPort: 5222
      hostPort: 5222
    - containerPort: 5280
      hostPort: 5280
    volumeMounts:
    - mountPath: /opt/ejabberd/conf/ejabberd.yml
      name: config
      readOnly: true

  volumes:
  - name: config
    hostPath:
      path: ./conf/ejabberd.yml
      type: File

Your configuration file should use those macros to allow each ejabberd node use different listening port numbers:

diff --git a/ejabberd.yml.example b/ejabberd.yml.example
index 39e423a64..6e875b48f 100644
--- a/ejabberd.yml.example
+++ b/ejabberd.yml.example
@@ -24,9 +24,19 @@ loglevel: info
 #  - /etc/letsencrypt/live/domain.tld/fullchain.pem
 #  - /etc/letsencrypt/live/domain.tld/privkey.pem

+define_macro:
+  PORT_C2S: 5222
+  PORT_C2S_TLS: 5223
+  PORT_S2S: 5269
+  PORT_HTTP_TLS: 5443
+  PORT_HTTP: 5280
+  PORT_STUN: 5478
+  PORT_MQTT: 1883
+  PORT_PROXY65: 7777
+
 listen:
   -
-    port: 5222
+    port: PORT_C2S
     ip: "::"
     module: ejabberd_c2s
     max_stanza_size: 262144
@@ -34,7 +44,7 @@ listen:
     access: c2s
     starttls_required: true
   -
-    port: 5223
+    port: PORT_C2S_TLS
     ip: "::"
     module: ejabberd_c2s
     max_stanza_size: 262144
@@ -42,13 +52,13 @@ listen:
     access: c2s
     tls: true
   -
-    port: 5269
+    port: PORT_S2S
     ip: "::"
     module: ejabberd_s2s_in
     max_stanza_size: 524288
     shaper: s2s_shaper
   -
-    port: 5443
+    port: PORT_HTTP_TLS
     ip: "::"
     module: ejabberd_http
     tls: true
@@ -60,14 +70,14 @@ listen:
       /upload: mod_http_upload
       /ws: ejabberd_http_ws
   -
-    port: 5280
+    port: PORT_HTTP
     ip: "::"
     module: ejabberd_http
     request_handlers:
       /admin: ejabberd_web_admin
       /.well-known/acme-challenge: ejabberd_acme
   -
-    port: 5478
+    port: PORT_STUN
     ip: "::"
     transport: udp
     module: ejabberd_stun
@@ -77,7 +87,7 @@ listen:
     ## The server's public IPv6 address:
     # turn_ipv6_address: "2001:db8::3"
   -
-    port: 1883
+    port: PORT_MQTT
     ip: "::"
     module: mod_mqtt
     backlog: 1000
@@ -207,6 +217,7 @@ modules:
   mod_proxy65:
     access: local
     max_connections: 5
+    port: PORT_PROXY65
   mod_pubsub:
     access_createnode: pubsub_createnode
     plugins: