Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
efc5f50
Add installation via docker compose (MVP 1)
Aug 9, 2025
99030d0
rename dockerfile
Aug 23, 2025
86d9359
add port 80 to docker-compose-default
Aug 23, 2025
e442d43
add 465 port
Aug 23, 2025
ef2744e
add RECREATE_VENV var
Aug 23, 2025
83ab9b1
change "restart nginx" to "reload nginx"
Aug 23, 2025
3901c39
pass values to `MAIL_DOMAIN` and `ACME_EMAIL` from vars for docker-co…
Aug 23, 2025
6b397ca
Fix bug with attaching certs
Aug 23, 2025
830d01d
fix docs - nginx "restart" to "reload"
Aug 23, 2025
8b04d31
Delete ssh connection from docker installation
Aug 23, 2025
761ad0d
Fix issue with acmetool
Aug 24, 2025
cfe9819
fix unlink if default nginx conf is not exist
Aug 25, 2025
2db2d93
docker: enable DNS checks before cmdeploy run again
missytake Aug 26, 2025
5d4c40c
Suggestions from @Keonik1
missytake Nov 13, 2025
9b90b1f
docker: disable port check if docker is running. fix #694
missytake Nov 13, 2025
eb91870
doc: fix linebreak
missytake Nov 14, 2025
b4052ab
docker: move all configuration to example.env
missytake Nov 14, 2025
0dd8404
docker: open ports for TURN + STUN
missytake Nov 14, 2025
97c8ab3
docker: use --network=host so chatmail-turn can use any port
missytake Nov 14, 2025
b1f15ae
cmdeploy: add config (, )
missytake Nov 18, 2025
953bdd3
cmdeploy: Add config parameters `change_kernel_settings` and `fs_inot…
Nov 18, 2025
0415da4
Revert "cmdeploy: Add config parameters `change_kernel_settings` and …
missytake Nov 25, 2025
3b1779f
dovecot: don't use sysctl in docker containers
missytake Nov 25, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -164,3 +164,8 @@ cython_debug/
#.idea/

chatmail.zone

# docker
/data/
/custom/
.env
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@
Provide an "fsreport" CLI for more fine grained analysis of message files.
([#637](https://github.com/chatmail/relay/pull/637))

- Add installation via docker compose (MVP 1). The instructions, known issues and limitations are located in `/docs`
([#614](https://github.com/chatmail/relay/pull/614))

## 1.7.0 2025-09-11

Expand Down
5 changes: 5 additions & 0 deletions cmdeploy/src/cmdeploy/cmdeploy.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,8 @@ def run_cmd(args, out):

cmd = f"{pyinf} --ssh-user root {ssh_host} {deploy_path} -y"
if ssh_host in ["localhost", "@docker"]:
if ssh_host == "@docker":
env["CHATMAIL_DOCKER"] = "True"
cmd = f"{pyinf} @local {deploy_path} -y"

if version.parse(pyinfra.__version__) < version.parse("3"):
Expand All @@ -118,6 +120,9 @@ def run_cmd(args, out):
kwargs=dict(command="cat /var/lib/echobot/invite-link.txt"),
)
)
server_deployed_message = f"Chatmail server started: https://{args.config.mail_domain}/"
delimiter_line = "=" * len(server_deployed_message)
out.green(f"{delimiter_line}\n{server_deployed_message}\n{delimiter_line}")
out.green("Deploy completed, call `cmdeploy dns` next.")
elif not remote_data["acme_account_url"]:
out.red("Deploy completed but letsencrypt not configured")
Expand Down
54 changes: 28 additions & 26 deletions cmdeploy/src/cmdeploy/deployers.py
Original file line number Diff line number Diff line change
Expand Up @@ -526,11 +526,12 @@ def activate(self):
)


def deploy_chatmail(config_path: Path, disable_mail: bool) -> None:
def deploy_chatmail(config_path: Path, disable_mail: bool, docker: bool) -> None:
"""Deploy a chat-mail instance.

:param config_path: path to chatmail.ini
:param disable_mail: whether to disable postfix & dovecot
:param docker: whether it is running in a docker container
"""
config = read_config(config_path)
check_config(config)
Expand All @@ -543,31 +544,32 @@ def deploy_chatmail(config_path: Path, disable_mail: bool) -> None:
line="nameserver 9.9.9.9",
)

port_services = [
(["master", "smtpd"], 25),
("unbound", 53),
("acmetool", 80),
(["imap-login", "dovecot"], 143),
("nginx", 443),
(["master", "smtpd"], 465),
(["master", "smtpd"], 587),
(["imap-login", "dovecot"], 993),
("iroh-relay", 3340),
("nginx", 8443),
(["master", "smtpd"], config.postfix_reinject_port),
(["master", "smtpd"], config.postfix_reinject_port_incoming),
("filtermail", config.filtermail_smtp_port),
("filtermail", config.filtermail_smtp_port_incoming),
]
for service, port in port_services:
print(f"Checking if port {port} is available for {service}...")
running_service = host.get_fact(Port, port=port)
if running_service:
if running_service not in service:
Out().red(
f"Deploy failed: port {port} is occupied by: {running_service}"
)
exit(1)
if not docker:
port_services = [
(["master", "smtpd"], 25),
("unbound", 53),
("acmetool", 80),
(["imap-login", "dovecot"], 143),
("nginx", 443),
(["master", "smtpd"], 465),
(["master", "smtpd"], 587),
(["imap-login", "dovecot"], 993),
("iroh-relay", 3340),
("nginx", 8443),
(["master", "smtpd"], config.postfix_reinject_port),
(["master", "smtpd"], config.postfix_reinject_port_incoming),
("filtermail", config.filtermail_smtp_port),
("filtermail", config.filtermail_smtp_port_incoming),
]
for service, port in port_services:
print(f"Checking if port {port} is available for {service}...")
running_service = host.get_fact(Port, port=port)
if running_service:
if running_service not in service:
Out().red(
f"Deploy failed: port {port} is occupied by: {running_service}"
)
exit(1)

tls_domains = [mail_domain, f"mta-sts.{mail_domain}", f"www.{mail_domain}"]

Expand Down
27 changes: 15 additions & 12 deletions cmdeploy/src/cmdeploy/dovecot/deployer.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import os

from chatmaild.config import Config
from pyinfra import host
from pyinfra.facts.server import Arch, Sysctl
Expand Down Expand Up @@ -114,18 +116,19 @@ def _configure_dovecot(config: Config, debug: bool = False) -> bool:

# as per https://doc.dovecot.org/configuration_manual/os/
# it is recommended to set the following inotify limits
for name in ("max_user_instances", "max_user_watches"):
key = f"fs.inotify.{name}"
if host.get_fact(Sysctl)[key] > 65535:
# Skip updating limits if already sufficient
# (enables running in incus containers where sysctl readonly)
continue
server.sysctl(
name=f"Change {key}",
key=key,
value=65535,
persist=True,
)
if not os.environ.get("CHATMAIL_DOCKER"):
for name in ("max_user_instances", "max_user_watches"):
key = f"fs.inotify.{name}"
if host.get_fact(Sysctl)[key] > 65535:
# Skip updating limits if already sufficient
# (enables running in incus containers where sysctl readonly)
continue
server.sysctl(
name=f"Change {key}",
key=key,
value=65535,
persist=True,
)

timezone_env = files.line(
name="Set TZ environment variable",
Expand Down
3 changes: 2 additions & 1 deletion cmdeploy/src/cmdeploy/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@ def main():
importlib.resources.files("cmdeploy").joinpath("../../../chatmail.ini"),
)
disable_mail = bool(os.environ.get("CHATMAIL_DISABLE_MAIL"))
docker = bool(os.environ.get("CHATMAIL_DOCKER"))

deploy_chatmail(config_path, disable_mail)
deploy_chatmail(config_path, disable_mail, docker)


if pyinfra.is_cli:
Expand Down
7 changes: 7 additions & 0 deletions doc/source/getting_started.rst
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,13 @@ steps. Please substitute it with your own domain.
configure at your DNS provider (it can take some time until they are
public).

Docker installation
-------------------

We have experimental support for `docker compose <https://github.com/chatmail/relay/blob/docker-rebase/docs/DOCKER_INSTALLATION_EN.md>`_,
but it is not covered by automated tests yet,
so don't expect everything to work.

Other helpful commands
----------------------

Expand Down
51 changes: 51 additions & 0 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
services:
chatmail:
build:
context: ./docker
dockerfile: chatmail_relay.dockerfile
tags:
- chatmail-relay:latest
image: chatmail-relay:latest
restart: unless-stopped
container_name: chatmail
cgroup: host # required for systemd
tty: true # required for logs
tmpfs: # required for systemd
- /tmp
- /run
- /run/lock
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
environment:
MAIL_DOMAIN: $MAIL_DOMAIN
ACME_EMAIL: $ACME_EMAIL
RECREATE_VENV: $RECREATE_VENV
MAX_MESSAGE_SIZE: $MAX_MESSAGE_SIZE
DEBUG_COMMANDS_ENABLED: $DEBUG_COMMANDS_ENABLED
FORCE_REINIT_INI_FILE: $FORCE_REINIT_INI_FILE
USE_FOREIGN_CERT_MANAGER: $USE_FOREIGN_CERT_MANAGER
ENABLE_CERTS_MONITORING: $ENABLE_CERTS_MONITORING
CERTS_MONITORING_TIMEOUT: $CERTS MONITORING TIMEOUT
IS_DEVELOPMENT_INSTANCE: $IS_DEVELOPMENT_INSTANCE
network_mode: "host"
volumes:
## system
- /sys/fs/cgroup:/sys/fs/cgroup:rw # required for systemd
- ./:/opt/chatmail

## data
- ./data/chatmail:/home
- ./data/chatmail-dkimkeys:/etc/dkimkeys
- ./data/chatmail-echobot:/run/echobot
- ./data/chatmail-acme:/var/lib/acme

## custom resources
# - ./custom/www/src/index.md:/opt/chatmail/www/src/index.md

## debug
# - ./docker/files/setup_chatmail_docker.sh:/setup_chatmail_docker.sh
# - ./docker/files/entrypoint.sh:/entrypoint.sh
# - ./docker/files/update_ini.sh:/update_ini.sh
83 changes: 83 additions & 0 deletions docker/chatmail_relay.dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
FROM jrei/systemd-debian:12 AS base

ENV LANG=en_US.UTF-8

RUN echo 'APT::Install-Recommends "0";' > /etc/apt/apt.conf.d/01norecommend && \
echo 'APT::Install-Suggests "0";' >> /etc/apt/apt.conf.d/01norecommend && \
apt-get update && \
apt-get install -y \
ca-certificates && \
DEBIAN_FRONTEND=noninteractive \
TZ=Europe/London \
apt-get install -y tzdata && \
apt-get install -y locales && \
sed -i -e "s/# $LANG.*/$LANG UTF-8/" /etc/locale.gen && \
dpkg-reconfigure --frontend=noninteractive locales && \
update-locale LANG=$LANG \
&& rm -rf /var/lib/apt/lists/*

RUN apt-get update && \
apt-get install -y \
git \
python3 \
python3-venv \
python3-virtualenv \
gcc \
python3-dev \
opendkim \
opendkim-tools \
curl \
rsync \
unbound \
unbound-anchor \
dnsutils \
postfix \
acl \
nginx \
libnginx-mod-stream \
fcgiwrap \
cron \
&& for pkg in core imapd lmtpd; do \
case "$pkg" in \
core) sha256="43f593332e22ac7701c62d58b575d2ca409e0f64857a2803be886c22860f5587" ;; \
imapd) sha256="8d8dc6fc00bbb6cdb25d345844f41ce2f1c53f764b79a838eb2a03103eebfa86" ;; \
lmtpd) sha256="2f69ba5e35363de50962d42cccbfe4ed8495265044e244007d7ccddad77513ab" ;; \
esac; \
url="https://download.delta.chat/dovecot/dovecot-${pkg}_2.3.21%2Bdfsg1-3_amd64.deb"; \
file="/tmp/$(basename "$url")"; \
curl -fsSL "$url" -o "$file"; \
echo "$sha256 $file" | sha256sum -c -; \
apt-get install -y "$file"; \
rm -f "$file"; \
done \
&& rm -rf /var/lib/apt/lists/*

WORKDIR /opt/chatmail

ARG SETUP_CHATMAIL_SERVICE_PATH=/lib/systemd/system/setup_chatmail.service
COPY ./files/setup_chatmail.service "$SETUP_CHATMAIL_SERVICE_PATH"
RUN ln -sf "$SETUP_CHATMAIL_SERVICE_PATH" "/etc/systemd/system/multi-user.target.wants/setup_chatmail.service"

COPY --chmod=555 ./files/setup_chatmail_docker.sh /setup_chatmail_docker.sh
COPY --chmod=555 ./files/update_ini.sh /update_ini.sh
COPY --chmod=555 ./files/entrypoint.sh /entrypoint.sh

## TODO: add git clone.
## Problem: how correct save only required files inside container....
# RUN git clone https://github.com/chatmail/relay.git -b master . \
# && ./scripts/initenv.sh

# EXPOSE 443 25 587 143 993

VOLUME ["/sys/fs/cgroup", "/home"]

STOPSIGNAL SIGRTMIN+3

ENTRYPOINT ["/entrypoint.sh"]

CMD [ "--default-standard-output=journal+console", \
"--default-standard-error=journal+console" ]

## TODO: Add installation and configuration of chatmaild inside the Dockerfile.
## This is required to ensure repeatable deployment.
## In the current MVP, the chatmaild server is updated on every container restart.
10 changes: 10 additions & 0 deletions docker/example.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
MAIL_DOMAIN="chat.example.com"
# ACME_EMAIL=""
# RECREATE_VENV="false"
# MAX_MESSAGE_SIZE="50M"
# DEBUG_COMMANDS_ENABLED="true"
# FORCE_REINIT_INI_FILE="true"
# USE_FOREIGN_CERT_MANAGER="True"
# ENABLE_CERTS_MONITORING="true"
# CERTS_MONITORING_TIMEOUT=10
# IS_DEVELOPMENT_INSTANCE="True"
11 changes: 11 additions & 0 deletions docker/files/entrypoint.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#!/bin/bash
set -eo pipefail

unlink /etc/nginx/sites-enabled/default || true

SETUP_CHATMAIL_SERVICE_PATH="${SETUP_CHATMAIL_SERVICE_PATH:-/lib/systemd/system/setup_chatmail.service}"

env_vars=$(printenv | cut -d= -f1 | xargs)
sed -i "s|<envs_list>|$env_vars|g" $SETUP_CHATMAIL_SERVICE_PATH

exec /lib/systemd/systemd $@
14 changes: 14 additions & 0 deletions docker/files/setup_chatmail.service
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[Unit]
Description=Run container setup commands
After=multi-user.target
ConditionPathExists=/setup_chatmail_docker.sh

[Service]
Type=oneshot
ExecStart=/bin/bash /setup_chatmail_docker.sh
RemainAfterExit=true
WorkingDirectory=/opt/chatmail
PassEnvironment=<envs_list>

[Install]
WantedBy=multi-user.target
Loading