This directory runs Ethora Core on any Docker host with nothing else installed: an API, a web chat and admin panel, an XMPP server and file storage, behind Caddy with Let's Encrypt certificates. Copy the directory to the server, answer two questions, start it.
It is the second way to run Ethora Core, next to the host installer
(deploy/scripts/install.sh). Both run the same images from the same
configuration templates; this one needs only Docker.
On a server with Docker Engine 24+ and the Compose plugin (2.24+), ports 80 and 443 open, 2 vCPU and 4 GB RAM:
cd ethora-compose # this directory, copied to the server
./configure.sh --domain chat.example.com --admin-email you@example.com
docker compose up -dconfigure.sh writes .env: the hosts derive from your root domain, every
secret is generated, and the admin password is printed once. The first start
pulls the images and takes a few minutes; docker compose logs -f init ends
with done when the base app and admin account are ready. Then open
https://app.chat.example.com and sign in with your e-mail and that password.
Before you start, create five DNS records pointing at the server, all plain
A (or AAAA) records, no proxy:
| Record | Points to |
|---|---|
api.chat.example.com |
your server's IP |
app.chat.example.com |
your server's IP |
xmpp.chat.example.com |
your server's IP |
files.chat.example.com |
your server's IP |
secure-files.chat.example.com |
your server's IP |
(chat.example.com is your root; every host derives from it. A wildcard
*.chat.example.com record covers all five. secure-files. serves chat
attachments, gated by chat membership; SECURE_FILES_DOMAIN=off in .env
drops it and attachments go to the public files. bucket.)
No domain yet? Use a magic DNS name for a test install: with server IP
203.0.113.10, pass --domain 203-0-113-10.sslip.io. It resolves
everywhere, gets a real Let's Encrypt certificate, and needs no DNS setup.
No configure.sh (a platform that only takes a compose file and variables):
copy .env.example to .env, or enter its values in the platform, and set
ROOT_DOMAIN and ADMIN_EMAIL. Secrets left empty are generated on the
first start and kept in the secrets volume; the admin password is printed
once by docker compose logs config.
single/docker-compose.yml is the same stack in
one file: nothing else on disk, for "paste a stack" screens (Portainer) and
for anyone copying a compose file from a web page. Save it in an empty
directory next to a two-line .env:
curl -fsSLo docker-compose.yml https://raw.githubusercontent.com/dappros/ethora-install/main/deploy/compose/single/docker-compose.yml
printf 'ROOT_DOMAIN=chat.example.com\nADMIN_EMAIL=you@example.com\n' > .env
docker compose up -d
docker compose logs config | grep 'admin password'Its scripts and templates come from the ethora-compose-init image (the
xmpp image of the same release plus this directory's scripts/,
templates/ and Caddyfile), which renders the configuration and hands
every other service its start script through the config volume. Every
variable of .env.example works there too. Caddy always runs in the single
file; behind a proxy of your own, start it with
docker compose up -d --scale caddy=0. The file is generated from
docker-compose.yml by single/build.sh, and the tests fail when it is out
of date.
Set PUBLIC_URL instead of ROOT_DOMAIN (./configure.sh --public-url ...)
to put everything on one address, routed by path: the web app at /, the
API at /v1, /v2 and /api-docs, XMPP at /ws and /bosh, Centrifugo at
/connection/websocket, files at /files/. Chat attachments then go to the
public files bucket (the membership-gated secure-files. host needs a host
of its own).
PUBLIC_URL=https://chat.example.com: one DNS record, a Let's Encrypt certificate for it.PUBLIC_URL=http://192.168.1.20:8456withHTTP_PORT=8456: a LAN install without TLS (NAS, home server). This is how the Umbrel and CasaOS apps run (platforms/).
The host of PUBLIC_URL is also the XMPP domain (an IP address works), and
the web app connects to XMPP and links stored files through PUBLIC_URL
itself; only its API calls follow the address the page was opened by. So
open it by the PUBLIC_URL address, and make sure that name resolves for
every client.
This directory is also what the cloud images run (the AWS AMI, the Azure,
DigitalOcean and Vultr images, the Akamai StackScript): their first-boot
setup page calls deploy/cloud/install.sh, which is ./configure.sh plus
docker compose up -d plus the check below, so everything on this page
applies to them.
docker compose ps # every service running or exited (0)
docker compose --profile verify run --rm verifyverify goes through the public URLs the way a browser does: certificates,
web app, API docs, admin login, licence state, a chat room created and a
message sent over wss://xmpp.<root>/ws, a file uploaded and read back from
files.<root>. It prints one line per check.
| Service | Image | Role |
|---|---|---|
caddy |
caddy |
TLS and routing for the five hosts (ports 80, 443) |
frontend |
dappros/ethora-frontend |
web chat and admin panel (app.<root>) |
api, jobs |
dappros/ethora-api |
HTTP API (api.<root>, Swagger at /api-docs/), cron and queue workers |
xmpp |
dappros/ethora-xmpp |
ejabberd with the Ethora modules (xmpp.<root>, WebSocket at /ws) |
minio |
dappros/minio |
file storage (files.<root>) |
mongo, mysql, redis, centrifugo |
stock images | internal only |
config, mongo-init, init |
the images above | one-shot steps, run on every up, then exit (0) |
ai-service, docs-parse, widget, ai-postgres |
dappros/ethora-ai, pgvector, caddy |
the ai module (profile ai; see Enterprise modules) |
push, push-worker, playground, mcp, uptime, uptime-db |
dappros/ethora-push, -playground, -mcp, -uptime, postgres |
the other modules, each behind its profile |
config renders every service's configuration from .env with the same
templates the host installer uses (and generates the secrets .env leaves
out), mongo-init makes MongoDB a replica set,
and init creates the base app and admin account and provisions their XMPP
accounts. All three are idempotent; running them again changes nothing.
Only Caddy publishes ports. Native XMPP clients on port 5222 are not exposed;
web, mobile and SDK clients use wss://xmpp.<root>/ws.
docker compose pull && docker compose up -dThe image tags in .env (...:2610) follow a release line, so a pull picks
up that line's fixes. To move to a newer line, change the three
ETHORA_*_IMAGE tags in .env (or take this directory from the newer
release) and run the same command. When nothing was published, the update
changes nothing.
Edit .env (or re-run ./configure.sh with the new answer; it keeps every
secret), then:
docker compose up -d --force-recreateADMIN_PASSWORD only sets the password the admin account starts with;
change it in the admin panel afterwards. MYSQL_ROOT_PASSWORD and
MINIO_ROOT_USER / MINIO_ROOT_PASSWORD are written into the databases on
the first start and must not change after it.
Any variable of the installer's service templates can be added to .env
(templates/backend.env.template, templates/frontend.env.template), for
example POSTMARK_ENABLED=true with POSTMARK_TOKEN=... for e-mail.
Named Docker volumes (under /var/lib/docker/volumes/ on a default host):
| Volume | Contents |
|---|---|
ethora_mongo |
apps, users, chats, message archive |
ethora_mysql |
ejabberd: XMPP accounts, rooms, message history |
ethora_minio |
uploaded files |
ethora_redis |
cache and queues |
ethora_caddy-data |
certificates and the ACME account |
ethora_secrets |
the secrets .env left out, generated on the first start |
ethora_ai-postgres |
ai module: the agents' document embeddings |
ethora_push-uploads |
push module: uploaded platform keys |
ethora_uptime-postgres |
uptime module: check history |
ethora_playground-next |
playground module: the built app (regenerated; no backup needed) |
.env (this directory) |
your settings and the secrets it carries |
They survive docker compose down, image updates and reboots.
docker compose down -v deletes them, and with them every chat and file.
The volumes ethora_config, ethora_mysql-initdb, ethora_mongo-config and
ethora_caddy-config are regenerated and need no backup; ethora_secrets
does, since the databases were initialised with its passwords.
Stop the stack for a consistent copy (a minute of downtime):
mkdir -p backup && cp .env backup/
docker compose stop
for v in mongo mysql minio redis caddy-data secrets; do
docker run --rm -v ethora_$v:/v:ro -v "$PWD/backup:/b" alpine tar czf /b/$v.tgz -C /v .
done
docker compose start(Add ai-postgres, push-uploads and uptime-postgres to the list with
those modules.)
Restore on a new host: copy this directory with backup/, then, before
anything has started there:
cp backup/.env .env
docker compose create # pulls the images, creates the empty volumes
for v in mongo mysql minio redis caddy-data secrets; do
docker run --rm -v ethora_$v:/v -v "$PWD/backup:/b:ro" alpine tar xzf /b/$v.tgz -C /v
done
docker compose up -dWithout downtime, dump the two databases instead and copy the files volume:
docker compose exec -T mongo mongodump --archive --gzip > backup/mongo.archive.gz
docker compose exec -T mysql sh -c 'mysqldump -uroot -p"$(cat "$MYSQL_ROOT_PASSWORD_FILE")" --single-transaction --databases ejabberd_db' | gzip > backup/mysql.sql.gz
docker run --rm -v ethora_minio:/v:ro -v "$PWD/backup:/b" alpine tar czf /b/minio.tgz -C /v .The Enterprise modules run from this same bundle as compose profiles. Each
module needs a licence key that carries its feature (the admin panel's
License page, or --license-key), and its images come from the private
dappros repositories on Docker Hub: sign in to the registry with the
token shown on the License page before the first start.
docker login -u <registry user> docker.io # the token from the License page
./configure.sh --modules ai --license-key ETHORA1.... --ai-api-key sk-...
docker compose up -dconfigure.sh --modules writes the profiles into COMPOSE_PROFILES
(caddy,ai); without configure.sh, set that variable and the module's
values in .env by hand (see .env.example, "modules").
| Module | Profile | Runs | Host |
|---|---|---|---|
| AI agents | ai |
ai-service, docs-parse, ai-postgres (pgvector), ai-init, widget-export, widget from dappros/ethora-ai |
widget.<root> (the website chat widget; /widget/ on one origin) |
| Push notifications | push |
push, push-worker from dappros/ethora-push |
none of its own: the API proxies /v1/push/*, mobile apps use api.<root>/push/ |
| SDK playground | playground |
playground from dappros/ethora-playground (builds on its first start, about 1.5 GB of RAM for a few minutes) |
playground.<root> |
| Hosted MCP server | mcp |
mcp from dappros/ethora-mcp; the API serves OAuth on api.<root> |
mcp.<root> (/mcp) |
| Uptime | uptime |
uptime, uptime-db (Postgres) from dappros/ethora-uptime |
uptime.<root>, basic auth (UPTIME_AUTH_USER, UPTIME_AUTH_PASSWORD in .env) |
./configure.sh --modules ai,push,playground,mcp,uptime takes any subset.
The playground, mcp and uptime modules need the five-host layout (a host
each, covered by a wildcard record); on a one-origin install only ai and
push run. The push service sends with the platform keys uploaded in the
admin panel; PUSH_PLATFORM_PROJECT_ID and the PUSH_GATEWAY_* values in
.env.example cover the other arrangements. The uptime dashboard's
password is generated into .env; behind a proxy of your own, protect
uptime.<root> with that proxy's basic auth. Each module's checks are part
of verify.
The ai module turns the Agents section of the admin panel on, serves the
embeddable website chat widget from widget.<root> (a sixth DNS record,
covered by a wildcard) and needs an OpenAI-compatible provider: AI_API_KEY,
and AI_API_URL, AI_CHAT_MODEL, AI_EMBEDDING_MODEL for another
provider or model. Embeddings live in the bundled Postgres (volume
ethora_ai-postgres; back it up with the others); AI_PG_URL points
ai-service at a Postgres of your own with the pgvector extension instead.
The widget's long-lived script URL carries a version suffix
(assistant<WIDGET_SCRIPT_VERSION>.js), fixed at the first start and kept,
so pages that embed it keep working across updates. docker compose --profile verify run --rm verify checks the module too: the widget script
on its public URL, ai-service and docs-parse answering, the licence
feature.
Switching a module off: remove its profile from COMPOSE_PROFILES and run
docker compose up -d --remove-orphans; its volumes stay until
docker volume rm.
The push, SDK playground, MCP and uptime modules follow the same pattern and are added in later releases; until then the host installer runs them.
Coolify, Dokploy and similar platforms run their own reverse proxy on ports
80 and 443. Leave the bundled one off (./configure.sh --no-caddy, or an
empty COMPOSE_PROFILES) and route the five hosts to these containers, with
WebSocket upgrades allowed:
| Host | Container and port | Paths |
|---|---|---|
api.<root> |
api:8080 |
all (block /metrics) |
app.<root> |
frontend:8080; centrifugo:8000 for /connection/websocket |
all |
xmpp.<root> |
xmpp:5280 |
/ws and /bosh only; never /api or /admin |
files.<root> |
minio:9000 |
all, with the original Host header |
secure-files.<root> |
api:8080 |
all, with the original Host header (the API serves this host itself) |
widget.<root> (ai module) |
widget:8080 |
all |
api.<root>/push/ (push module) |
push:8098, with /push stripped |
mobile apps' push registration |
playground.<root> (playground module) |
playground:3020 |
all |
mcp.<root> (mcp module) |
mcp:3030 |
all |
uptime.<root> (uptime module) |
uptime:8099 |
all, behind basic auth of your proxy |
For Coolify the exact steps (git-based application, and a draft one-click
service template) are in platforms/coolify/README.md; for Dokploy
(Compose service from git, and a template in the format of Dokploy's
templates repository) in platforms/dokploy/README.md.
Portainer users can add the bundle to their app templates: Settings > App Templates > URL, paste
https://raw.githubusercontent.com/dappros/ethora-install/main/deploy/compose/platforms/portainer-template.json
and Ethora Core appears in the templates list with a field for every value
.env needs; secret fields left empty are generated on the first start. To
paste a stack instead (Stacks > Add stack > Web editor), paste
single/docker-compose.yml and add
ROOT_DOMAIN and ADMIN_EMAIL as environment variables. See
platforms/ for the template and other platforms.
The Helm chart deploy/helm/ethora-core runs the same
images and renders the same configuration with ethora-compose-init:
helm install ethora oci://docker.io/dappros/ethora-core --set rootDomain=... --set admin.email=....
The Cloudron package deploy/cloudron runs the stack as one
Cloudron app: the same payloads, copied out of the release images, under
supervisord, on Cloudron's MongoDB, MySQL and Redis addons, rendered by this
bundle's render-config.sh in one-origin mode behind Cloudron's proxy.
App-store packages for home servers, generated from the single file and run
on one address over the LAN (PUBLIC_URL, port 8456):
platforms/umbrel/ (umbrelOS, behind its app_proxy) and
platforms/casaos/ (CasaOS and ZimaOS). Each README has
the store's submission steps.
- Certificate failed. DNS does not point at this server yet, or port 80
is blocked.
docker compose logs caddynames the host; fix it anddocker compose restart caddy. - A service keeps restarting.
docker compose logs <service>. Ifconfigexited non-zero,.envis missing a value; its log lists which. - Which admin password? The one in
.env(ADMIN_PASSWORD), or, when it was left empty, the onedocker compose logs configprinted on the first start (also in thesecretsvolume). Change it in the admin panel. - Can I put it behind Cloudflare? Start with plain DNS records first;
proxying can be switched on afterwards for
app.andapi..xmpp.must stay unproxied. - Mobile apps? The Ethora SDKs for iOS, Android and React connect to
api.<root>andxmpp.<root>; see https://ethora.com/docs.
Ethora Core, free with per-server limits (5 apps and 500 user accounts; 10 and 5,000 after registering for free on the admin panel's License page). Installing it accepts the Ethora Core Software License: https://ethora.com/legal/ethora-core-license/. The MinIO image is an unmodified copy of MinIO under AGPL-3.0; the other third-party images keep their own licences.