Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,13 @@
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))

- Add configuration parameters
([#614](https://github.com/chatmail/relay/pull/614)):
- `change_kernel_settings` - Whether to change kernel parameters during installation (default: `True`)
- `fs_inotify_max_user_instances_and_watchers` - Value for kernel parameters `fs.inotify.max_user_instances` and `fs.inotify.max_user_watches` (default: `65535`)

## 1.7.0 2025-09-11

Expand Down
6 changes: 6 additions & 0 deletions chatmaild/src/chatmaild/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,12 @@ def __init__(self, inipath, params):
self.mtail_address = params.get("mtail_address")
self.disable_ipv6 = params.get("disable_ipv6", "false").lower() == "true"
self.acme_email = params.get("acme_email", "")
self.change_kernel_settings = (
params.get("change_kernel_settings", "true").lower() == "true"
)
self.fs_inotify_max_user_instances_and_watchers = int(
params["fs_inotify_max_user_instances_and_watchers"]
)
self.imap_rawlog = params.get("imap_rawlog", "false").lower() == "true"
if "iroh_relay" not in params:
self.iroh_relay = "https://" + params["mail_domain"]
Expand Down
10 changes: 10 additions & 0 deletions chatmaild/src/chatmaild/ini/chatmail.ini.f
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,16 @@
# Your email adress, which will be used in acmetool to manage Let's Encrypt SSL certificates
acme_email =

#
# Kernel settings
#

# if you set "True", the kernel settings will be configured according to the values below
change_kernel_settings = True

# change fs.inotify.max_user_instances and fs.inotify.max_user_watches kernel settings
fs_inotify_max_user_instances_and_watchers = 65535

# Defaults to https://iroh.{{mail_domain}} and running `iroh-relay` on the chatmail
# service.
# If you set it to anything else, the service will be disabled
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
81 changes: 42 additions & 39 deletions cmdeploy/src/cmdeploy/deployers.py
Original file line number Diff line number Diff line change
Expand Up @@ -516,20 +516,21 @@ def _configure_dovecot(config: Config, debug: bool = False) -> bool:
)
need_restart |= lua_push_notification_script.changed

# as per https://doc.dovecot.org/configuration_manual/os/
# as per https://doc.dovecot.org/2.3/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 config.change_kernel_settings:
for name in ("max_user_instances", "max_user_watches"):
key = f"fs.inotify.{name}"
if host.get_fact(Sysctl)[key] > config.fs_inotify_max_user_instances_and_watchers:
# Skip updating limits if already sufficient
# (enables running in incus containers where sysctl readonly)
continue
server.sysctl(
name=f"Change {key}",
key=key,
value=config.fs_inotify_max_user_instances_and_watchers,
persist=True,
)

timezone_env = files.line(
name="Set TZ environment variable",
Expand Down Expand Up @@ -1062,11 +1063,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 @@ -1079,31 +1081,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
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
52 changes: 52 additions & 0 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
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:
CHANGE_KERNEL_SETTINGS: "False"
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