From 9afbd8a1293fef080860bbccb1bf3396d90d33e6 Mon Sep 17 00:00:00 2001 From: Keonik1 Date: Sat, 9 Aug 2025 15:55:37 +0300 Subject: [PATCH 01/26] Add installation via docker compose (MVP 1) --- .gitignore | 6 + CHANGELOG.md | 2 + cmdeploy/src/cmdeploy/cmdeploy.py | 13 ++ doc/source/getting_started.rst | 7 + docker/chatmail_server.dockerfile | 102 ++++++++++++++ docker/docker-compose-default.yaml | 57 ++++++++ docker/example.env | 3 + docker/files/entrypoint.sh | 20 +++ docker/files/setup_chatmail.service | 14 ++ docker/files/setup_chatmail_docker.sh | 73 ++++++++++ docker/files/update_ini.sh | 79 +++++++++++ docs/DOCKER_INSTALLATION_EN.md | 196 ++++++++++++++++++++++++++ docs/DOCKER_INSTALLATION_RU.md | 176 +++++++++++++++++++++++ 13 files changed, 748 insertions(+) create mode 100644 docker/chatmail_server.dockerfile create mode 100644 docker/docker-compose-default.yaml create mode 100644 docker/example.env create mode 100755 docker/files/entrypoint.sh create mode 100644 docker/files/setup_chatmail.service create mode 100755 docker/files/setup_chatmail_docker.sh create mode 100644 docker/files/update_ini.sh create mode 100644 docs/DOCKER_INSTALLATION_EN.md create mode 100644 docs/DOCKER_INSTALLATION_RU.md diff --git a/.gitignore b/.gitignore index 6e1054d07..c6260e934 100644 --- a/.gitignore +++ b/.gitignore @@ -164,3 +164,9 @@ cython_debug/ #.idea/ chatmail.zone + +# docker +/data/ +/custom/ +docker-compose.yaml +.env diff --git a/CHANGELOG.md b/CHANGELOG.md index 91ba87949..e5a4a8a53 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -121,6 +121,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 diff --git a/cmdeploy/src/cmdeploy/cmdeploy.py b/cmdeploy/src/cmdeploy/cmdeploy.py index 61a9b5f60..9877dd0e8 100644 --- a/cmdeploy/src/cmdeploy/cmdeploy.py +++ b/cmdeploy/src/cmdeploy/cmdeploy.py @@ -123,6 +123,19 @@ def run_cmd(args, out): else: out.red("Website deployment failed.") elif retcode == 0: + if not args.disable_mail: + print("\nYou can try out the relay by talking to this echo bot: ") + sshexec = SSHExec(args.config.mail_domain, verbose=args.verbose) + print( + sshexec( + call=remote.rshell.shell, + 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 args.dns_check_disabled and not remote_data["acme_account_url"]: out.red("Deploy completed but letsencrypt not configured") diff --git a/doc/source/getting_started.rst b/doc/source/getting_started.rst index 2553eff1b..ac204ba61 100644 --- a/doc/source/getting_started.rst +++ b/doc/source/getting_started.rst @@ -80,6 +80,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 `_, +but it is not covered by automated tests yet, +so don't expect everything to work. + Other helpful commands ---------------------- diff --git a/docker/chatmail_server.dockerfile b/docker/chatmail_server.dockerfile new file mode 100644 index 000000000..72c4a042c --- /dev/null +++ b/docker/chatmail_server.dockerfile @@ -0,0 +1,102 @@ +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 \ + openssh-client \ + openssh-server \ + 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/* + +RUN systemctl enable \ + ssh \ + fcgiwrap + +RUN sed -i 's/^#PasswordAuthentication .*/PasswordAuthentication no/' /etc/ssh/sshd_config && \ + sed -i 's/^#PermitRootLogin .*/PermitRootLogin prohibit-password/' /etc/ssh/sshd_config && \ + ssh-keygen -P "" -t rsa -b 2048 -f /root/.ssh/id_rsa && \ + mkdir -p /root/.ssh && \ + cat /root/.ssh/id_rsa.pub >> /root/.ssh/authorized_keys && \ + SSH_USER_CONFIG="/root/.ssh/config" && \ + echo "Host localhost" > "$SSH_USER_CONFIG" && \ + echo " HostName localhost" >> "$SSH_USER_CONFIG" && \ + echo " User root" >> "$SSH_USER_CONFIG" && \ + echo " StrictHostKeyChecking no" >> "$SSH_USER_CONFIG" && \ + echo " UserKnownHostsFile /dev/null" >> "$SSH_USER_CONFIG" + ## TODO: deny access for all insteed root form 127.0.0.1 https://unix.stackexchange.com/a/406264 + +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. diff --git a/docker/docker-compose-default.yaml b/docker/docker-compose-default.yaml new file mode 100644 index 000000000..736016b89 --- /dev/null +++ b/docker/docker-compose-default.yaml @@ -0,0 +1,57 @@ +services: + chatmail: + build: + context: ./docker + dockerfile: chatmail_server.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: + ACME_EMAIL: + + 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" + ports: + - "25:25" + - "587:587" + - "143:143" + - "993:993" + - "443:443" + volumes: + ## system + - /sys/fs/cgroup:/sys/fs/cgroup:rw # required for systemd + - ./:/opt/chatmail + - ./data/acme:/var/lib/acme + + ## 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 diff --git a/docker/example.env b/docker/example.env new file mode 100644 index 000000000..ef8ca28a2 --- /dev/null +++ b/docker/example.env @@ -0,0 +1,3 @@ +MAIL_DOMAIN="chat.example.com" + +PATH_TO_SSL_CONTAINER="/var/lib/acme/live/${MAIL_DOMAIN}" diff --git a/docker/files/entrypoint.sh b/docker/files/entrypoint.sh new file mode 100755 index 000000000..b704c2e3a --- /dev/null +++ b/docker/files/entrypoint.sh @@ -0,0 +1,20 @@ +#!/bin/bash +set -eo pipefail + +if [ "${USE_FOREIGN_CERT_MANAGER,,}" == "true" ]; then + if [ ! -f "$PATH_TO_SSL_CONTAINER/fullchain" ]; then + echo "Error: file '$PATH_TO_SSL_CONTAINER/fullchain' does not exist. Exiting..." > /dev/stderr + exit 1 + fi + if [ ! -f "$PATH_TO_SSL_CONTAINER/privkey" ]; then + echo "Error: file '$PATH_TO_SSL_CONTAINER/privkey' does not exist. Exiting..." > /dev/stderr + exit 1 + fi +fi + +SETUP_CHATMAIL_SERVICE_PATH="${SETUP_CHATMAIL_SERVICE_PATH:-/lib/systemd/system/setup_chatmail.service}" + +env_vars=$(printenv | cut -d= -f1 | xargs) +sed -i "s||$env_vars|g" $SETUP_CHATMAIL_SERVICE_PATH + +exec /lib/systemd/systemd $@ diff --git a/docker/files/setup_chatmail.service b/docker/files/setup_chatmail.service new file mode 100644 index 000000000..2a0a48bc0 --- /dev/null +++ b/docker/files/setup_chatmail.service @@ -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= + +[Install] +WantedBy=multi-user.target diff --git a/docker/files/setup_chatmail_docker.sh b/docker/files/setup_chatmail_docker.sh new file mode 100755 index 000000000..02d931a38 --- /dev/null +++ b/docker/files/setup_chatmail_docker.sh @@ -0,0 +1,73 @@ +#!/bin/bash + +set -eo pipefail +export INI_FILE="${INI_FILE:-chatmail.ini}" +export ENABLE_CERTS_MONITORING="${ENABLE_CERTS_MONITORING:-true}" +export CERTS_MONITORING_TIMEOUT="${CERTS_MONITORING_TIMEOUT:-60}" +export PATH_TO_SSL_CONTAINER="${PATH_TO_SSL_CONTAINER:-/var/lib/acme/live/${MAIL_DOMAIN}}" + +if [ -z "$MAIL_DOMAIN" ]; then + echo "ERROR: Environment variable 'MAIL_DOMAIN' must be set!" >&2 + exit 1 +fi + +debug_commands() { + echo "Executing debug commands" + # git config --global --add safe.directory /opt/chatmail + # ./scripts/initenv.sh +} + +calculate_hash() { + find "$PATH_TO_SSL_CONTAINER" -type f -exec sha1sum {} \; | sort | sha1sum | awk '{print $1}' +} + +monitor_certificates() { + if [ "$ENABLE_CERTS_MONITORING" != "true" ]; then + echo "Certs monitoring disabled." + exit 0 + fi + + current_hash=$(calculate_hash) + previous_hash=$current_hash + + while true; do + current_hash=$(calculate_hash) + if [[ "$current_hash" != "$previous_hash" ]]; then + # TODO: add an option to restart at a specific time interval + echo "[INFO] Certificate's folder hash was changed, restarting nginx, dovecot and postfix services." + systemctl restart nginx.service + systemctl reload dovecot.service + systemctl reload postfix.service + previous_hash=$current_hash + fi + sleep $CERTS_MONITORING_TIMEOUT + done +} + +### MAIN + +if [ "$DEBUG_COMMANDS_ENABLED" == "true" ]; then + debug_commands +fi + +if [ "$FORCE_REINIT_INI_FILE" == "true" ]; then + INI_CMD_ARGS=--force +fi + +/usr/sbin/opendkim-genkey -D /etc/dkimkeys -d $MAIL_DOMAIN -s opendkim +chown opendkim:opendkim /etc/dkimkeys/opendkim.private +chown opendkim:opendkim /etc/dkimkeys/opendkim.txt + +# TODO: Move to debug_commands after git clone is moved to dockerfile. +git config --global --add safe.directory /opt/chatmail +./scripts/initenv.sh + +./scripts/cmdeploy init --config "${INI_FILE}" $INI_CMD_ARGS $MAIL_DOMAIN +bash /update_ini.sh + +./scripts/cmdeploy run --ssh-host localhost --skip-dns-check + +echo "ForwardToConsole=yes" >> /etc/systemd/journald.conf +systemctl restart systemd-journald + +monitor_certificates & diff --git a/docker/files/update_ini.sh b/docker/files/update_ini.sh new file mode 100644 index 000000000..c5d65661a --- /dev/null +++ b/docker/files/update_ini.sh @@ -0,0 +1,79 @@ +#!/bin/bash +set -eo pipefail + +INI_FILE="${INI_FILE:-chatmail.ini}" + +if [ ! -f "$INI_FILE" ]; then + echo "Error: file $INI_FILE not found." >&2 + exit 1 +fi + +TMP_FILE="$(mktemp)" + +convert_to_bytes() { + local value="$1" + if [[ "$value" =~ ^([0-9]+)([KkMmGgTt])$ ]]; then + local num="${BASH_REMATCH[1]}" + local unit="${BASH_REMATCH[2]}" + case "$unit" in + [Kk]) echo $((num * 1024)) ;; + [Mm]) echo $((num * 1024 * 1024)) ;; + [Gg]) echo $((num * 1024 * 1024 * 1024)) ;; + [Tt]) echo $((num * 1024 * 1024 * 1024 * 1024)) ;; + esac + elif [[ "$value" =~ ^[0-9]+$ ]]; then + echo "$value" + else + echo "Error: incorrect size format: $value." >&2 + return 1 + fi +} + +process_specific_params() { + local key=$1 + local value=$2 + local destination_file=$3 + + if [[ "$key" == "max_message_size" ]]; then + converted=$(convert_to_bytes "$value") || exit 1 + if grep -q -e "## .* = .* bytes" "$destination_file"; then + sed "s|## .* = .* bytes|## $value = $converted bytes|g" "$destination_file"; + else + echo "## $value = $converted bytes" >> "$destination_file" + fi + echo "$key = $converted" >> "$destination_file" + else + echo "$key = $value" >> "$destination_file" + fi +} + +while IFS= read -r line; do + if [[ "$line" =~ ^[[:space:]]*#.* || "$line" =~ ^[[:space:]]*$ ]]; then + echo "$line" >> "$TMP_FILE" + continue + fi + + if [[ "$line" =~ ^([a-z0-9_]+)[[:space:]]*=[[:space:]]*(.*)$ ]]; then + key="${BASH_REMATCH[1]}" + current_value="${BASH_REMATCH[2]}" + env_var_name=$(echo "$key" | tr 'a-z' 'A-Z') + env_value="${!env_var_name}" + + if [[ -n "$env_value" ]]; then + process_specific_params "$key" "$env_value" "$TMP_FILE" + else + echo "$line" >> "$TMP_FILE" + fi + else + echo "$line" >> "$TMP_FILE" + fi +done < "$INI_FILE" + +PERMS=$(stat -c %a "$INI_FILE") +OWNER=$(stat -c %u "$INI_FILE") +GROUP=$(stat -c %g "$INI_FILE") + +chmod "$PERMS" "$TMP_FILE" +chown "$OWNER":"$GROUP" "$TMP_FILE" + +mv "$TMP_FILE" "$INI_FILE" diff --git a/docs/DOCKER_INSTALLATION_EN.md b/docs/DOCKER_INSTALLATION_EN.md new file mode 100644 index 000000000..86ff727ae --- /dev/null +++ b/docs/DOCKER_INSTALLATION_EN.md @@ -0,0 +1,196 @@ +# Known issues and limitations + +- Chatmail will be reinstalled every time the container is started (longer the first time, faster on subsequent starts). This is how the original installer works because it wasn’t designed for Docker. At the end of the documentation, there’s a [proposed solution](#locking-the-chatmail-version). +- Requires cgroups v2 configured in the system. Operation with cgroups v1 has not been tested. +- Yes, of course, using systemd inside a container is a hack, and it would be better to split it into several services, but since this is an MVP, it turned out to be easier to do it this way initially than to rewrite the entire deployment system. +- The Docker image is only suitable for amd64. If you need to run it on a different architecture, try modifying the Dockerfile (specifically the part responsible for installing dovecot). + +# Docker installation +This section provides instructions for installing Chatmail using docker-compose. + +## Preliminary setup +We use `chat.example.org` as the Chatmail domain in the following steps. +Please substitute it with your own domain. + +1. Setup the initial DNS records. + The following is an example in the familiar BIND zone file format with + a TTL of 1 hour (3600 seconds). + Please substitute your domain and IP addresses. + + ``` + chat.example.com. 3600 IN A 198.51.100.5 + chat.example.com. 3600 IN AAAA 2001:db8::5 + www.chat.example.com. 3600 IN CNAME chat.example.com. + mta-sts.chat.example.com. 3600 IN CNAME chat.example.com. + ``` + +2. clone the repository on your server. + + ```shell + git clone https://github.com/chatmail/relay + cd relay + ``` + +## Installation + +1. Copy the file `./docker/docker-compose-default.yaml` to `docker-compose.yaml`. This is necessary because `docker-compose.yaml` is in `.gitignore` and won’t cause conflicts when updating the git repository. + +```shell +cp ./docker/docker-compose-default.yaml docker-compose.yaml +``` + +3. Configure kernel parameters because they cannot be changed inside the container, specifically `fs.inotify.max_user_instances` and `fs.inotify.max_user_watches`. Run the following: + +```shell +echo "fs.inotify.max_user_instances=65536" | sudo tee -a /etc/sysctl.d/99-inotify.conf +echo "fs.inotify.max_user_watches=65536" | sudo tee -a /etc/sysctl.d/99-inotify.conf +sudo sysctl --system +``` + +4. Configure container environment variables. Below is the list of variables used during deployment: + +- `MAIL_DOMAIN` – The domain name of the future server. (required) +- `DEBUG_COMMANDS_ENABLED` – Run debug commands before installation. (default: `false`) +- `FORCE_REINIT_INI_FILE` – Recreate the ini configuration file on startup. (default: `false`) +- `USE_FOREIGN_CERT_MANAGER` – Use a third-party certificate manager. (default: `false`) +- `INI_FILE` – Path to the ini configuration file. (default: `./chatmail.ini`) +- `PATH_TO_SSL_CONTAINER` – Path to where the certificates are stored. (default: `/var/lib/acme/live/${MAIL_DOMAIN}`) +- `ENABLE_CERTS_MONITORING` – Enable certificate monitoring if `USE_FOREIGN_CERT_MANAGER=true`. If certificates change, services will be automatically restarted. (default: `false`) +- `CERTS_MONITORING_TIMEOUT` – Interval in seconds to check if certificates have changed. (default: `'60'`) + +You can also use any variables from the [ini configuration file](https://github.com/chatmail/relay/blob/main/chatmaild/src/chatmaild/ini/chatmail.ini.f); they must be in uppercase. + +Mandatory variables for deployment via Docker: + +- `CHANGE_KERNEL_SETTINGS` – Change kernel settings (`fs.inotify.max_user_instances` and `fs.inotify.max_user_watches`) on startup. Changing kernel settings inside the container is not possible! (default: `False`) + +5. Configure environment variables in the `.env` file. These variables are used in the `docker-compose.yaml` file to pass repeated values. + +6. Build the Docker image: + +```shell +docker compose build chatmail +``` + +7. Start docker compose and wait for the installation to finish: + +```shell +docker compose up -d # start service +docker compose logs -f chatmail # view container logs, press CTRL+C to exit +``` + +8. After installation is complete, you can open `https://` in your browser. + +## Using custom files + +When using Docker, you can apply modified configuration files to make the installation more personalized. This is usually needed for the `www/src` section so that the Chatmail landing page is customized to your taste, but it can be used for any other cases as well. + +To replace files correctly: + +1. Create the `./custom` directory. It is in `.gitignore`, so it won’t cause conflicts when updating. + +```shell +mkdir -p ./custom +``` + +2. Modify the required file. For example, `index.md`: + +```shell +mkdir -p ./custom/www/src +nano ./custom/www/src/index.md +``` + +3. In `docker-compose.yaml`, add the file mount in the `volumes` section: + +```yaml +services: + chatmail: + volumes: + ... + ## custom resources + - ./custom/www/src/index.md:/opt/chatmail/www/src/index.md +``` + +4. Restart the service: + +```shell +docker compose down +docker compose up -d +``` + +## Locking the Chatmail version + +> [!note] +> These steps are optional and should only be done if you are not satisfied that the service is installed each time the container starts. + +Since the current Docker version installs the Chatmail service every time the container starts, you can lock the container version after installation as follows: + +1. Commit the current state of the configured container: + +```shell +docker container commit chatmail configured-chatmail:$(date +'%Y-%m-%d') +docker image ls | grep configured-chatmail +``` + +2. Change the entrypoint for the container in `docker-compose.yaml` to: + +```yaml +services: + chatmail: + image: + volumes: + ... + ## custom resources + - ./custom/setup_chatmail_docker.sh:/setup_chatmail_docker.sh +``` + +3. Create the file `./custom/setup_chatmail_docker.sh` with the new configuration: + +```shell +mkdir -p ./custom +cat > ./custom/setup_chatmail_docker.sh << 'EOF' +#!/bin/bash + +set -eo pipefail + +export ENABLE_CERTS_MONITORING="${ENABLE_CERTS_MONITORING:-true}" +export CERTS_MONITORING_TIMEOUT="${CERTS_MONITORING_TIMEOUT:-60}" +export PATH_TO_SSL_CONTAINER="${PATH_TO_SSL_CONTAINER:-/var/lib/acme/live/${MAIL_DOMAIN}}" + +calculate_hash() { + find "$PATH_TO_SSL_CONTAINER" -type f -exec sha1sum {} \; | sort | sha1sum | awk '{print $1}' +} + +monitor_certificates() { + if [ "$ENABLE_CERTS_MONITORING" != "true" ]; then + echo "Certs monitoring disabled." + exit 0 + fi + + current_hash=$(calculate_hash) + previous_hash=$current_hash + + while true; do + current_hash=$(calculate_hash) + if [[ "$current_hash" != "$previous_hash" ]]; then + # TODO: add an option to restart at a specific time interval + echo "[INFO] Certificate's folder hash was changed, restarting nginx, dovecot and postfix services." + systemctl restart nginx.service + systemctl reload dovecot.service + systemctl reload postfix.service + previous_hash=$current_hash + fi + sleep $CERTS_MONITORING_TIMEOUT + done +} + +monitor_certificates & +EOF +``` + +4. Restart the service: + +```shell +docker compose down +docker compose up -d +``` diff --git a/docs/DOCKER_INSTALLATION_RU.md b/docs/DOCKER_INSTALLATION_RU.md new file mode 100644 index 000000000..f62243906 --- /dev/null +++ b/docs/DOCKER_INSTALLATION_RU.md @@ -0,0 +1,176 @@ +# Известные проблемы и ограничения +- Chatmail будет переустановлен при каждом запуске контейнера (при первом - долго, при последующих быстрее). Так устроен изначальный установщик, потому что он не был заточен под docker. В конце документации [представлено](#фиксирование-версии-chatmail) возможное решение +- Требуется настроенный в системе cgroups v2. Работа с cgroups v1 не тестировалась. +- Да, понятно дело что systemd использовать в контейнере костыль и надо это всё разнести на несколько сервисов, но это MVP и в первом приближении оказалось сделать проще так, чем переписывать всю систему развертывания. +- docker образ подходит только для amd64, если нужно запустить на другой архитектуре, попробуйте изменить dockerfile (конкретно ту часть что ответсвенна за установку dovecot) + +# Docker installation +Здесь представлена инструкция по установке chatmail с помощью docker-compose. + +## Предварительная настройка +We use `chat.example.org` as the chatmail domain in the following steps. +Please substitute it with your own domain. + +1. Настройте начальные записи DNS.Ниже приведен пример в привычном формате файла зоны BIND сTTL 1 час (3600 секунд). + Замените домен и IP-адреса на свои. + + ``` + chat.example.com. 3600 IN A 198.51.100.5 + chat.example.com. 3600 IN AAAA 2001:db8::5 + www.chat.example.com. 3600 IN CNAME chat.example.com. + mta-sts.chat.example.com. 3600 IN CNAME chat.example.com. + ``` + +2. Склонируйте репозиторий на свой сервер. + + ```shell + git clone https://github.com/chatmail/relay + cd relay + ``` + +## Installation + +1. Скопировать файл `./docker/docker-compose-default.yaml` в `docker-compose.yaml`. Это нужно потому что `docker-compose.yaml` находится в `.gitignore` и не будет создавать конфликты при обновлении гит репозитория. +```shell +cp ./docker/docker-compose-default.yaml docker-compose.yaml +``` + +3. Настроить параметры ядра, потому что внутри контейнера их нельзя изменить, а конкретно `fs.inotify.max_user_instances` и `fs.inotify.max_user_watches`. Для этого выполнить следующее: +```shell +echo "fs.inotify.max_user_instances=65536" | sudo tee -a /etc/sysctl.d/99-inotify.conf +echo "fs.inotify.max_user_watches=65536" | sudo tee -a /etc/sysctl.d/99-inotify.conf +sudo sysctl --system +``` + +4. Настроить переменные окружения контейнера. Ниже перечислен список переменных учавствующих при развертывании. +- `MAIL_DOMAIN` - Доменное имя будущего сервера. (required) +- `DEBUG_COMMANDS_ENABLED` - Выполнить debug команды перед установкой. (default: `false`) +- `FORCE_REINIT_INI_FILE` - Пересоздавать ini файл конфигурации при запуске. (default: `false`) +- `USE_FOREIGN_CERT_MANAGER` - Использовать сторонний менеджер сертификатов. (default: `false`) +- `INI_FILE` - путь к ini файлу конфигурации. (default: `./chatmail.ini`) +- `PATH_TO_SSL_CONTAINER` - Путь где располагаются сертификаты. (default: `/var/lib/acme/live/${MAIL_DOMAIN}`) +- `ENABLE_CERTS_MONITORING` - Включить мониторинг сертификатов, если `USE_FOREIGN_CERT_MANAGER=true`. Если сертфикаты изменятся сервисы будут автоматически перезапущены. (default: `false`) +- `CERTS_MONITORING_TIMEOUT` - Раз во сколько секунд проверять что изменились сертификаты. (default: `'60'`) + +Также могут быть использованы все переменные из [ini файла конфигурации](https://github.com/chatmail/relay/blob/main/chatmaild/src/chatmaild/ini/chatmail.ini.f), они обязаны быть в uppercase формате. + +Ниже перечислены переменные, которые обязательны быть выставлены при развертывании через docker: +- `CHANGE_KERNEL_SETTINGS` - Менять настройки ядра (`fs.inotify.max_user_instances` и `fs.inotify.max_user_watches`) при запуске. При запуске в контейнере смена настроек ядра не может быть выполнена! (default: `False`) + +5. Настроить переменные окружения в `.env` файле. Эти переменные используются в `docker-compose.yaml` файле, чтобы передавать повторяющиеся значения. + +6. Собрать docker образ +```shell +docker compose build chatmail +``` + +7. Запустить docker compose и дождаться завершения установки +```shell +docker compose up -d # запуск сервиса +docker compose logs -f chatmail # просмотр логов контейнера. Для выхода нажать CTRL+C +``` + +8. По окончанию установки можно открыть в браузер `https://` + +## Использование кастомных файлов +При использовании docker есть возможность использовать измененые файлы конфигурации, чтобы сделать установку более персонализированной. Обычно это требуется для секции `www/src`, чтобы ознакомительная страница Chatmail была сделана на ваш вкус. Но также это можно использовать и для любых других случаев. + +Для того чтобы корректно выполнить подмену файлов необходимо +1. создать каталог `./custom`, он находится в `.gitignore`, поэтому при обновлении не вызовет конфликтов. +```shell +mkdir -p ./custom +``` + +2. Изменить нужный файл. Для примера возьмем `index.md` +```shell +mkdir -p ./custom/www/src +nano ./custom/www/src/index.md +``` + +3. В `docker-compose.yaml` добавить монтирование файла с помощью секции `volumes` +```yaml +services: + chatmail: + volumes: + ... + ## custom resources + - ./custom/www/src/index.md:/opt/chatmail/www/src/index.md +``` + +4. Перезапустить сервис +```shell +docker compose down +docker compose up -d +``` + +## Фиксирование версии Chatmail +> [!note] +> Это опциональные шаги, их делать требуется только если вас не устраивает что сервис устанавливается каждый раз при запуске + +Поскольку в текущей версии docker chatmail сервис устанавливается каждый раз запуске контейнера, чтобы этого не происходило можно зафиксировать версию контейнера после установки. Делается это следующим образом: + +1. Зафиксировать текущее состояние сконфигурированного контейнера +```shell +docker container commit chatmail configured-chatmail:$(date +'%Y-%m-%d') +docker image ls | grep configured-chatmail +``` + +2. Изменить entrypoint для контейнера в `docker-compose.yaml` на +```yaml +services: + chatmail: + image: + volumes: + ... + ## custom resources + - ./custom/setup_chatmail_docker.sh:/setup_chatmail_docker.sh +``` + +3. Создать файл `./custom/setup_chatmail_docker.sh` с новым файлом конфигурации +```shell +mkdir -p ./custom +cat > ./custom/setup_chatmail_docker.sh << 'EOF' +#!/bin/bash + +set -eo pipefail + +export ENABLE_CERTS_MONITORING="${ENABLE_CERTS_MONITORING:-true}" +export CERTS_MONITORING_TIMEOUT="${CERTS_MONITORING_TIMEOUT:-60}" +export PATH_TO_SSL_CONTAINER="${PATH_TO_SSL_CONTAINER:-/var/lib/acme/live/${MAIL_DOMAIN}}" + +calculate_hash() { + find "$PATH_TO_SSL_CONTAINER" -type f -exec sha1sum {} \; | sort | sha1sum | awk '{print $1}' +} + +monitor_certificates() { + if [ "$ENABLE_CERTS_MONITORING" != "true" ]; then + echo "Certs monitoring disabled." + exit 0 + fi + + current_hash=$(calculate_hash) + previous_hash=$current_hash + + while true; do + current_hash=$(calculate_hash) + if [[ "$current_hash" != "$previous_hash" ]]; then + # TODO: add an option to restart at a specific time interval + echo "[INFO] Certificate's folder hash was changed, restarting nginx, dovecot and postfix services." + systemctl restart nginx.service + systemctl reload dovecot.service + systemctl reload postfix.service + previous_hash=$current_hash + fi + sleep $CERTS_MONITORING_TIMEOUT + done +} + +monitor_certificates & +EOF +``` + +4. Перезапустить сервис +```shell +docker compose down +docker compose up -d +``` From c5f0136264e111500fdea69a3804da3dec42ab1e Mon Sep 17 00:00:00 2001 From: Keonik1 Date: Sat, 23 Aug 2025 15:26:00 +0300 Subject: [PATCH 02/26] rename dockerfile https://github.com/chatmail/relay/pull/614#discussion_r2270031966 --- .../{chatmail_server.dockerfile => chatmail_relay.dockerfile} | 0 docker/docker-compose-default.yaml | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename docker/{chatmail_server.dockerfile => chatmail_relay.dockerfile} (100%) diff --git a/docker/chatmail_server.dockerfile b/docker/chatmail_relay.dockerfile similarity index 100% rename from docker/chatmail_server.dockerfile rename to docker/chatmail_relay.dockerfile diff --git a/docker/docker-compose-default.yaml b/docker/docker-compose-default.yaml index 736016b89..7dc00a85a 100644 --- a/docker/docker-compose-default.yaml +++ b/docker/docker-compose-default.yaml @@ -2,7 +2,7 @@ services: chatmail: build: context: ./docker - dockerfile: chatmail_server.dockerfile + dockerfile: chatmail_relay.dockerfile tags: - chatmail-relay:latest image: chatmail-relay:latest From 5102ffc4d2ad0350ac8ac893a26121cd36adef2e Mon Sep 17 00:00:00 2001 From: Keonik1 Date: Sat, 23 Aug 2025 15:27:30 +0300 Subject: [PATCH 03/26] add port 80 to docker-compose-default https://github.com/chatmail/relay/pull/614#discussion_r2279656441 --- docker/docker-compose-default.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/docker/docker-compose-default.yaml b/docker/docker-compose-default.yaml index 7dc00a85a..2bf8194ba 100644 --- a/docker/docker-compose-default.yaml +++ b/docker/docker-compose-default.yaml @@ -31,6 +31,7 @@ services: # CERTS_MONITORING_TIMEOUT: 10 # IS_DEVELOPMENT_INSTANCE: "True" ports: + - "80:80" - "25:25" - "587:587" - "143:143" From 22fc0e44eb4d976152e49a186a84a8c1e79c366b Mon Sep 17 00:00:00 2001 From: Keonik1 Date: Sat, 23 Aug 2025 15:28:44 +0300 Subject: [PATCH 04/26] add 465 port https://github.com/chatmail/relay/pull/614#discussion_r2279707059 --- docker/docker-compose-default.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docker/docker-compose-default.yaml b/docker/docker-compose-default.yaml index 2bf8194ba..66c758f76 100644 --- a/docker/docker-compose-default.yaml +++ b/docker/docker-compose-default.yaml @@ -32,11 +32,12 @@ services: # IS_DEVELOPMENT_INSTANCE: "True" ports: - "80:80" + - "443:443" - "25:25" - "587:587" - "143:143" + - "465:465" - "993:993" - - "443:443" volumes: ## system - /sys/fs/cgroup:/sys/fs/cgroup:rw # required for systemd From 7a5725d3b5b3e71da7082561db414c1efe31d28f Mon Sep 17 00:00:00 2001 From: Keonik1 Date: Sat, 23 Aug 2025 15:37:18 +0300 Subject: [PATCH 05/26] add RECREATE_VENV var https://github.com/chatmail/relay/pull/614#discussion_r2279742769 --- docker/docker-compose-default.yaml | 4 ++-- docker/files/setup_chatmail_docker.sh | 4 ++++ docs/DOCKER_INSTALLATION_EN.md | 1 + docs/DOCKER_INSTALLATION_RU.md | 1 + 4 files changed, 8 insertions(+), 2 deletions(-) diff --git a/docker/docker-compose-default.yaml b/docker/docker-compose-default.yaml index 66c758f76..9b145d6af 100644 --- a/docker/docker-compose-default.yaml +++ b/docker/docker-compose-default.yaml @@ -22,8 +22,8 @@ services: environment: MAIL_DOMAIN: ACME_EMAIL: - - MAX_MESSAGE_SIZE: "50M" + # RECREATE_VENV: "false" + # MAX_MESSAGE_SIZE: "50M" # DEBUG_COMMANDS_ENABLED: "true" # FORCE_REINIT_INI_FILE: "true" # USE_FOREIGN_CERT_MANAGER: "True" diff --git a/docker/files/setup_chatmail_docker.sh b/docker/files/setup_chatmail_docker.sh index 02d931a38..c3dad3712 100755 --- a/docker/files/setup_chatmail_docker.sh +++ b/docker/files/setup_chatmail_docker.sh @@ -5,6 +5,7 @@ export INI_FILE="${INI_FILE:-chatmail.ini}" export ENABLE_CERTS_MONITORING="${ENABLE_CERTS_MONITORING:-true}" export CERTS_MONITORING_TIMEOUT="${CERTS_MONITORING_TIMEOUT:-60}" export PATH_TO_SSL_CONTAINER="${PATH_TO_SSL_CONTAINER:-/var/lib/acme/live/${MAIL_DOMAIN}}" +export RECREATE_VENV=${RECREATE_VENV:-"false"} if [ -z "$MAIL_DOMAIN" ]; then echo "ERROR: Environment variable 'MAIL_DOMAIN' must be set!" >&2 @@ -60,6 +61,9 @@ chown opendkim:opendkim /etc/dkimkeys/opendkim.txt # TODO: Move to debug_commands after git clone is moved to dockerfile. git config --global --add safe.directory /opt/chatmail +if [ "$RECREATE_VENV" == "true" ]; then + rm -rf venv +fi ./scripts/initenv.sh ./scripts/cmdeploy init --config "${INI_FILE}" $INI_CMD_ARGS $MAIL_DOMAIN diff --git a/docs/DOCKER_INSTALLATION_EN.md b/docs/DOCKER_INSTALLATION_EN.md index 86ff727ae..ab2ef97eb 100644 --- a/docs/DOCKER_INSTALLATION_EN.md +++ b/docs/DOCKER_INSTALLATION_EN.md @@ -53,6 +53,7 @@ sudo sysctl --system - `DEBUG_COMMANDS_ENABLED` – Run debug commands before installation. (default: `false`) - `FORCE_REINIT_INI_FILE` – Recreate the ini configuration file on startup. (default: `false`) - `USE_FOREIGN_CERT_MANAGER` – Use a third-party certificate manager. (default: `false`) +- `RECREATE_VENV` - Recreate the virtual environment (venv). If set to `true`, the environment will be recreated when the container starts, which will increase the startup time of the service but can help avoid certain errors. (default: `false`) - `INI_FILE` – Path to the ini configuration file. (default: `./chatmail.ini`) - `PATH_TO_SSL_CONTAINER` – Path to where the certificates are stored. (default: `/var/lib/acme/live/${MAIL_DOMAIN}`) - `ENABLE_CERTS_MONITORING` – Enable certificate monitoring if `USE_FOREIGN_CERT_MANAGER=true`. If certificates change, services will be automatically restarted. (default: `false`) diff --git a/docs/DOCKER_INSTALLATION_RU.md b/docs/DOCKER_INSTALLATION_RU.md index f62243906..329ed28c0 100644 --- a/docs/DOCKER_INSTALLATION_RU.md +++ b/docs/DOCKER_INSTALLATION_RU.md @@ -47,6 +47,7 @@ sudo sysctl --system - `DEBUG_COMMANDS_ENABLED` - Выполнить debug команды перед установкой. (default: `false`) - `FORCE_REINIT_INI_FILE` - Пересоздавать ini файл конфигурации при запуске. (default: `false`) - `USE_FOREIGN_CERT_MANAGER` - Использовать сторонний менеджер сертификатов. (default: `false`) +- `RECREATE_VENV` - Пересоздать виртуальное окружение (venv). Если выставлено `true`, то окружение будет пересоздано при запуске контейнера, из-за чего включение сервиса займет больше времени, но поможет избежать ряда ошибок. (default: `false`) - `INI_FILE` - путь к ini файлу конфигурации. (default: `./chatmail.ini`) - `PATH_TO_SSL_CONTAINER` - Путь где располагаются сертификаты. (default: `/var/lib/acme/live/${MAIL_DOMAIN}`) - `ENABLE_CERTS_MONITORING` - Включить мониторинг сертификатов, если `USE_FOREIGN_CERT_MANAGER=true`. Если сертфикаты изменятся сервисы будут автоматически перезапущены. (default: `false`) From 9dc3aae22666998c976ce7c935d6efba3184e3b8 Mon Sep 17 00:00:00 2001 From: Keonik1 Date: Sat, 23 Aug 2025 18:06:53 +0300 Subject: [PATCH 06/26] change "restart nginx" to "reload nginx" https://github.com/chatmail/relay/pull/614#discussion_r2269896158 --- docker/files/setup_chatmail_docker.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker/files/setup_chatmail_docker.sh b/docker/files/setup_chatmail_docker.sh index c3dad3712..bac3b88ef 100755 --- a/docker/files/setup_chatmail_docker.sh +++ b/docker/files/setup_chatmail_docker.sh @@ -35,8 +35,8 @@ monitor_certificates() { current_hash=$(calculate_hash) if [[ "$current_hash" != "$previous_hash" ]]; then # TODO: add an option to restart at a specific time interval - echo "[INFO] Certificate's folder hash was changed, restarting nginx, dovecot and postfix services." - systemctl restart nginx.service + echo "[INFO] Certificate's folder hash was changed, reloading nginx, dovecot and postfix services." + systemctl reload nginx.service systemctl reload dovecot.service systemctl reload postfix.service previous_hash=$current_hash From dd21a8368ae22eb8cf33945cf4a03813967f219c Mon Sep 17 00:00:00 2001 From: Keonik1 Date: Sat, 23 Aug 2025 18:16:33 +0300 Subject: [PATCH 07/26] pass values to `MAIL_DOMAIN` and `ACME_EMAIL` from vars for docker-compose-default https://github.com/chatmail/relay/pull/614#discussion_r2279591922 --- docker/docker-compose-default.yaml | 4 ++-- docs/DOCKER_INSTALLATION_EN.md | 8 ++++---- docs/DOCKER_INSTALLATION_RU.md | 8 ++++---- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/docker/docker-compose-default.yaml b/docker/docker-compose-default.yaml index 9b145d6af..05211277d 100644 --- a/docker/docker-compose-default.yaml +++ b/docker/docker-compose-default.yaml @@ -20,8 +20,8 @@ services: max-size: "10m" max-file: "3" environment: - MAIL_DOMAIN: - ACME_EMAIL: + MAIL_DOMAIN: $MAIL_DOMAIN + ACME_EMAIL: $ACME_EMAIL # RECREATE_VENV: "false" # MAX_MESSAGE_SIZE: "50M" # DEBUG_COMMANDS_ENABLED: "true" diff --git a/docs/DOCKER_INSTALLATION_EN.md b/docs/DOCKER_INSTALLATION_EN.md index ab2ef97eb..923547395 100644 --- a/docs/DOCKER_INSTALLATION_EN.md +++ b/docs/DOCKER_INSTALLATION_EN.md @@ -39,7 +39,9 @@ Please substitute it with your own domain. cp ./docker/docker-compose-default.yaml docker-compose.yaml ``` -3. Configure kernel parameters because they cannot be changed inside the container, specifically `fs.inotify.max_user_instances` and `fs.inotify.max_user_watches`. Run the following: +3. Configure environment variables in the `.env` file. These variables are used in the `docker-compose.yaml` file to pass repeated values. + +4. Configure kernel parameters because they cannot be changed inside the container, specifically `fs.inotify.max_user_instances` and `fs.inotify.max_user_watches`. Run the following: ```shell echo "fs.inotify.max_user_instances=65536" | sudo tee -a /etc/sysctl.d/99-inotify.conf @@ -47,7 +49,7 @@ echo "fs.inotify.max_user_watches=65536" | sudo tee -a /etc/sysctl.d/99-inotify. sudo sysctl --system ``` -4. Configure container environment variables. Below is the list of variables used during deployment: +5. Configure container environment variables. Below is the list of variables used during deployment: - `MAIL_DOMAIN` – The domain name of the future server. (required) - `DEBUG_COMMANDS_ENABLED` – Run debug commands before installation. (default: `false`) @@ -65,8 +67,6 @@ Mandatory variables for deployment via Docker: - `CHANGE_KERNEL_SETTINGS` – Change kernel settings (`fs.inotify.max_user_instances` and `fs.inotify.max_user_watches`) on startup. Changing kernel settings inside the container is not possible! (default: `False`) -5. Configure environment variables in the `.env` file. These variables are used in the `docker-compose.yaml` file to pass repeated values. - 6. Build the Docker image: ```shell diff --git a/docs/DOCKER_INSTALLATION_RU.md b/docs/DOCKER_INSTALLATION_RU.md index 329ed28c0..1c715fdd3 100644 --- a/docs/DOCKER_INSTALLATION_RU.md +++ b/docs/DOCKER_INSTALLATION_RU.md @@ -35,14 +35,16 @@ Please substitute it with your own domain. cp ./docker/docker-compose-default.yaml docker-compose.yaml ``` -3. Настроить параметры ядра, потому что внутри контейнера их нельзя изменить, а конкретно `fs.inotify.max_user_instances` и `fs.inotify.max_user_watches`. Для этого выполнить следующее: +3. Настроить переменные окружения в `.env` файле. Эти переменные используются в `docker-compose.yaml` файле, чтобы передавать повторяющиеся значения. + +4. Настроить параметры ядра, потому что внутри контейнера их нельзя изменить, а конкретно `fs.inotify.max_user_instances` и `fs.inotify.max_user_watches`. Для этого выполнить следующее: ```shell echo "fs.inotify.max_user_instances=65536" | sudo tee -a /etc/sysctl.d/99-inotify.conf echo "fs.inotify.max_user_watches=65536" | sudo tee -a /etc/sysctl.d/99-inotify.conf sudo sysctl --system ``` -4. Настроить переменные окружения контейнера. Ниже перечислен список переменных учавствующих при развертывании. +5. Настроить переменные окружения контейнера. Ниже перечислен список переменных учавствующих при развертывании. - `MAIL_DOMAIN` - Доменное имя будущего сервера. (required) - `DEBUG_COMMANDS_ENABLED` - Выполнить debug команды перед установкой. (default: `false`) - `FORCE_REINIT_INI_FILE` - Пересоздавать ini файл конфигурации при запуске. (default: `false`) @@ -58,8 +60,6 @@ sudo sysctl --system Ниже перечислены переменные, которые обязательны быть выставлены при развертывании через docker: - `CHANGE_KERNEL_SETTINGS` - Менять настройки ядра (`fs.inotify.max_user_instances` и `fs.inotify.max_user_watches`) при запуске. При запуске в контейнере смена настроек ядра не может быть выполнена! (default: `False`) -5. Настроить переменные окружения в `.env` файле. Эти переменные используются в `docker-compose.yaml` файле, чтобы передавать повторяющиеся значения. - 6. Собрать docker образ ```shell docker compose build chatmail From 9bb1d56c687268947fb979ba2a7989e987b9af2a Mon Sep 17 00:00:00 2001 From: Keonik1 Date: Sat, 23 Aug 2025 21:30:08 +0300 Subject: [PATCH 08/26] Fix bug with attaching certs --- docker/example.env | 2 -- docker/files/entrypoint.sh | 11 ----------- docker/files/setup_chatmail_docker.sh | 4 ++-- docs/DOCKER_INSTALLATION_EN.md | 6 +++--- docs/DOCKER_INSTALLATION_RU.md | 6 +++--- 5 files changed, 8 insertions(+), 21 deletions(-) diff --git a/docker/example.env b/docker/example.env index ef8ca28a2..48655812d 100644 --- a/docker/example.env +++ b/docker/example.env @@ -1,3 +1 @@ MAIL_DOMAIN="chat.example.com" - -PATH_TO_SSL_CONTAINER="/var/lib/acme/live/${MAIL_DOMAIN}" diff --git a/docker/files/entrypoint.sh b/docker/files/entrypoint.sh index b704c2e3a..2bc5d76f5 100755 --- a/docker/files/entrypoint.sh +++ b/docker/files/entrypoint.sh @@ -1,17 +1,6 @@ #!/bin/bash set -eo pipefail -if [ "${USE_FOREIGN_CERT_MANAGER,,}" == "true" ]; then - if [ ! -f "$PATH_TO_SSL_CONTAINER/fullchain" ]; then - echo "Error: file '$PATH_TO_SSL_CONTAINER/fullchain' does not exist. Exiting..." > /dev/stderr - exit 1 - fi - if [ ! -f "$PATH_TO_SSL_CONTAINER/privkey" ]; then - echo "Error: file '$PATH_TO_SSL_CONTAINER/privkey' does not exist. Exiting..." > /dev/stderr - exit 1 - fi -fi - SETUP_CHATMAIL_SERVICE_PATH="${SETUP_CHATMAIL_SERVICE_PATH:-/lib/systemd/system/setup_chatmail.service}" env_vars=$(printenv | cut -d= -f1 | xargs) diff --git a/docker/files/setup_chatmail_docker.sh b/docker/files/setup_chatmail_docker.sh index bac3b88ef..bdc122072 100755 --- a/docker/files/setup_chatmail_docker.sh +++ b/docker/files/setup_chatmail_docker.sh @@ -4,7 +4,7 @@ set -eo pipefail export INI_FILE="${INI_FILE:-chatmail.ini}" export ENABLE_CERTS_MONITORING="${ENABLE_CERTS_MONITORING:-true}" export CERTS_MONITORING_TIMEOUT="${CERTS_MONITORING_TIMEOUT:-60}" -export PATH_TO_SSL_CONTAINER="${PATH_TO_SSL_CONTAINER:-/var/lib/acme/live/${MAIL_DOMAIN}}" +export PATH_TO_SSL="${PATH_TO_SSL:-/var/lib/acme/live/${MAIL_DOMAIN}}" export RECREATE_VENV=${RECREATE_VENV:-"false"} if [ -z "$MAIL_DOMAIN" ]; then @@ -19,7 +19,7 @@ debug_commands() { } calculate_hash() { - find "$PATH_TO_SSL_CONTAINER" -type f -exec sha1sum {} \; | sort | sha1sum | awk '{print $1}' + find "$PATH_TO_SSL" -type f -exec sha1sum {} \; | sort | sha1sum | awk '{print $1}' } monitor_certificates() { diff --git a/docs/DOCKER_INSTALLATION_EN.md b/docs/DOCKER_INSTALLATION_EN.md index 923547395..0a2e4a3eb 100644 --- a/docs/DOCKER_INSTALLATION_EN.md +++ b/docs/DOCKER_INSTALLATION_EN.md @@ -57,7 +57,7 @@ sudo sysctl --system - `USE_FOREIGN_CERT_MANAGER` – Use a third-party certificate manager. (default: `false`) - `RECREATE_VENV` - Recreate the virtual environment (venv). If set to `true`, the environment will be recreated when the container starts, which will increase the startup time of the service but can help avoid certain errors. (default: `false`) - `INI_FILE` – Path to the ini configuration file. (default: `./chatmail.ini`) -- `PATH_TO_SSL_CONTAINER` – Path to where the certificates are stored. (default: `/var/lib/acme/live/${MAIL_DOMAIN}`) +- `PATH_TO_SSL` – Path to where the certificates are stored. (default: `/var/lib/acme/live/${MAIL_DOMAIN}`) - `ENABLE_CERTS_MONITORING` – Enable certificate monitoring if `USE_FOREIGN_CERT_MANAGER=true`. If certificates change, services will be automatically restarted. (default: `false`) - `CERTS_MONITORING_TIMEOUT` – Interval in seconds to check if certificates have changed. (default: `'60'`) @@ -156,10 +156,10 @@ set -eo pipefail export ENABLE_CERTS_MONITORING="${ENABLE_CERTS_MONITORING:-true}" export CERTS_MONITORING_TIMEOUT="${CERTS_MONITORING_TIMEOUT:-60}" -export PATH_TO_SSL_CONTAINER="${PATH_TO_SSL_CONTAINER:-/var/lib/acme/live/${MAIL_DOMAIN}}" +export PATH_TO_SSL="${PATH_TO_SSL:-/var/lib/acme/live/${MAIL_DOMAIN}}" calculate_hash() { - find "$PATH_TO_SSL_CONTAINER" -type f -exec sha1sum {} \; | sort | sha1sum | awk '{print $1}' + find "$PATH_TO_SSL" -type f -exec sha1sum {} \; | sort | sha1sum | awk '{print $1}' } monitor_certificates() { diff --git a/docs/DOCKER_INSTALLATION_RU.md b/docs/DOCKER_INSTALLATION_RU.md index 1c715fdd3..f05a63821 100644 --- a/docs/DOCKER_INSTALLATION_RU.md +++ b/docs/DOCKER_INSTALLATION_RU.md @@ -51,7 +51,7 @@ sudo sysctl --system - `USE_FOREIGN_CERT_MANAGER` - Использовать сторонний менеджер сертификатов. (default: `false`) - `RECREATE_VENV` - Пересоздать виртуальное окружение (venv). Если выставлено `true`, то окружение будет пересоздано при запуске контейнера, из-за чего включение сервиса займет больше времени, но поможет избежать ряда ошибок. (default: `false`) - `INI_FILE` - путь к ini файлу конфигурации. (default: `./chatmail.ini`) -- `PATH_TO_SSL_CONTAINER` - Путь где располагаются сертификаты. (default: `/var/lib/acme/live/${MAIL_DOMAIN}`) +- `PATH_TO_SSL` - Путь где располагаются сертификаты. (default: `/var/lib/acme/live/${MAIL_DOMAIN}`) - `ENABLE_CERTS_MONITORING` - Включить мониторинг сертификатов, если `USE_FOREIGN_CERT_MANAGER=true`. Если сертфикаты изменятся сервисы будут автоматически перезапущены. (default: `false`) - `CERTS_MONITORING_TIMEOUT` - Раз во сколько секунд проверять что изменились сертификаты. (default: `'60'`) @@ -137,10 +137,10 @@ set -eo pipefail export ENABLE_CERTS_MONITORING="${ENABLE_CERTS_MONITORING:-true}" export CERTS_MONITORING_TIMEOUT="${CERTS_MONITORING_TIMEOUT:-60}" -export PATH_TO_SSL_CONTAINER="${PATH_TO_SSL_CONTAINER:-/var/lib/acme/live/${MAIL_DOMAIN}}" +export PATH_TO_SSL="${PATH_TO_SSL:-/var/lib/acme/live/${MAIL_DOMAIN}}" calculate_hash() { - find "$PATH_TO_SSL_CONTAINER" -type f -exec sha1sum {} \; | sort | sha1sum | awk '{print $1}' + find "$PATH_TO_SSL" -type f -exec sha1sum {} \; | sort | sha1sum | awk '{print $1}' } monitor_certificates() { From fedfab2bdec3bd61fde7fe0d83c743ed6e6f4be2 Mon Sep 17 00:00:00 2001 From: Keonik1 Date: Sat, 23 Aug 2025 21:36:16 +0300 Subject: [PATCH 09/26] fix docs - nginx "restart" to "reload" https://github.com/chatmail/relay/pull/614#discussion_r2269896158 --- docs/DOCKER_INSTALLATION_EN.md | 4 ++-- docs/DOCKER_INSTALLATION_RU.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/DOCKER_INSTALLATION_EN.md b/docs/DOCKER_INSTALLATION_EN.md index 0a2e4a3eb..7a66e456a 100644 --- a/docs/DOCKER_INSTALLATION_EN.md +++ b/docs/DOCKER_INSTALLATION_EN.md @@ -175,8 +175,8 @@ monitor_certificates() { current_hash=$(calculate_hash) if [[ "$current_hash" != "$previous_hash" ]]; then # TODO: add an option to restart at a specific time interval - echo "[INFO] Certificate's folder hash was changed, restarting nginx, dovecot and postfix services." - systemctl restart nginx.service + echo "[INFO] Certificate's folder hash was changed, reloading nginx, dovecot and postfix services." + systemctl reload nginx.service systemctl reload dovecot.service systemctl reload postfix.service previous_hash=$current_hash diff --git a/docs/DOCKER_INSTALLATION_RU.md b/docs/DOCKER_INSTALLATION_RU.md index f05a63821..e90cf8895 100644 --- a/docs/DOCKER_INSTALLATION_RU.md +++ b/docs/DOCKER_INSTALLATION_RU.md @@ -156,8 +156,8 @@ monitor_certificates() { current_hash=$(calculate_hash) if [[ "$current_hash" != "$previous_hash" ]]; then # TODO: add an option to restart at a specific time interval - echo "[INFO] Certificate's folder hash was changed, restarting nginx, dovecot and postfix services." - systemctl restart nginx.service + echo "[INFO] Certificate's folder hash was changed, reloading nginx, dovecot and postfix services." + systemctl reload nginx.service systemctl reload dovecot.service systemctl reload postfix.service previous_hash=$current_hash From a0b13c1e9f2be10d2ebc2bdf50c173955048638c Mon Sep 17 00:00:00 2001 From: Keonik1 Date: Sat, 23 Aug 2025 22:47:32 +0300 Subject: [PATCH 10/26] Delete ssh connection from docker installation - https://github.com/chatmail/relay/pull/614#discussion_r2269986372 - https://github.com/chatmail/relay/pull/614#discussion_r2269991175 - https://github.com/chatmail/relay/pull/614#discussion_r2269995037 - https://github.com/chatmail/relay/pull/614#discussion_r2270004922 --- docker/chatmail_relay.dockerfile | 19 ------------------- docker/files/setup_chatmail_docker.sh | 2 +- 2 files changed, 1 insertion(+), 20 deletions(-) diff --git a/docker/chatmail_relay.dockerfile b/docker/chatmail_relay.dockerfile index 72c4a042c..3ec5f81e5 100644 --- a/docker/chatmail_relay.dockerfile +++ b/docker/chatmail_relay.dockerfile @@ -18,8 +18,6 @@ RUN echo 'APT::Install-Recommends "0";' > /etc/apt/apt.conf.d/01norecommend && \ RUN apt-get update && \ apt-get install -y \ - openssh-client \ - openssh-server \ git \ python3 \ python3-venv \ @@ -54,23 +52,6 @@ RUN apt-get update && \ done \ && rm -rf /var/lib/apt/lists/* -RUN systemctl enable \ - ssh \ - fcgiwrap - -RUN sed -i 's/^#PasswordAuthentication .*/PasswordAuthentication no/' /etc/ssh/sshd_config && \ - sed -i 's/^#PermitRootLogin .*/PermitRootLogin prohibit-password/' /etc/ssh/sshd_config && \ - ssh-keygen -P "" -t rsa -b 2048 -f /root/.ssh/id_rsa && \ - mkdir -p /root/.ssh && \ - cat /root/.ssh/id_rsa.pub >> /root/.ssh/authorized_keys && \ - SSH_USER_CONFIG="/root/.ssh/config" && \ - echo "Host localhost" > "$SSH_USER_CONFIG" && \ - echo " HostName localhost" >> "$SSH_USER_CONFIG" && \ - echo " User root" >> "$SSH_USER_CONFIG" && \ - echo " StrictHostKeyChecking no" >> "$SSH_USER_CONFIG" && \ - echo " UserKnownHostsFile /dev/null" >> "$SSH_USER_CONFIG" - ## TODO: deny access for all insteed root form 127.0.0.1 https://unix.stackexchange.com/a/406264 - WORKDIR /opt/chatmail ARG SETUP_CHATMAIL_SERVICE_PATH=/lib/systemd/system/setup_chatmail.service diff --git a/docker/files/setup_chatmail_docker.sh b/docker/files/setup_chatmail_docker.sh index bdc122072..302291aef 100755 --- a/docker/files/setup_chatmail_docker.sh +++ b/docker/files/setup_chatmail_docker.sh @@ -69,7 +69,7 @@ fi ./scripts/cmdeploy init --config "${INI_FILE}" $INI_CMD_ARGS $MAIL_DOMAIN bash /update_ini.sh -./scripts/cmdeploy run --ssh-host localhost --skip-dns-check +./scripts/cmdeploy run --ssh-host docker --skip-dns-check echo "ForwardToConsole=yes" >> /etc/systemd/journald.conf systemctl restart systemd-journald From d7018582129136e0e29ea5e6175852f3480461a3 Mon Sep 17 00:00:00 2001 From: Keonik1 Date: Sun, 24 Aug 2025 16:14:45 +0300 Subject: [PATCH 11/26] Fix issue with acmetool - https://github.com/chatmail/relay/pull/614#discussion_r2279630626 --- docker/docker-compose-default.yaml | 1 - docker/files/entrypoint.sh | 2 ++ 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docker/docker-compose-default.yaml b/docker/docker-compose-default.yaml index 05211277d..b5b793447 100644 --- a/docker/docker-compose-default.yaml +++ b/docker/docker-compose-default.yaml @@ -42,7 +42,6 @@ services: ## system - /sys/fs/cgroup:/sys/fs/cgroup:rw # required for systemd - ./:/opt/chatmail - - ./data/acme:/var/lib/acme ## data - ./data/chatmail:/home diff --git a/docker/files/entrypoint.sh b/docker/files/entrypoint.sh index 2bc5d76f5..3a2fb1914 100755 --- a/docker/files/entrypoint.sh +++ b/docker/files/entrypoint.sh @@ -1,6 +1,8 @@ #!/bin/bash set -eo pipefail +unlink /etc/nginx/sites-enabled/default + SETUP_CHATMAIL_SERVICE_PATH="${SETUP_CHATMAIL_SERVICE_PATH:-/lib/systemd/system/setup_chatmail.service}" env_vars=$(printenv | cut -d= -f1 | xargs) From 293bb4bcff305da40ba84999f1e37f673667dade Mon Sep 17 00:00:00 2001 From: Keonik1 Date: Mon, 25 Aug 2025 22:07:40 +0300 Subject: [PATCH 12/26] fix unlink if default nginx conf is not exist - https://github.com/chatmail/relay/pull/614#discussion_r2297828830 --- docker/files/entrypoint.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/files/entrypoint.sh b/docker/files/entrypoint.sh index 3a2fb1914..bce52a56f 100755 --- a/docker/files/entrypoint.sh +++ b/docker/files/entrypoint.sh @@ -1,7 +1,7 @@ #!/bin/bash set -eo pipefail -unlink /etc/nginx/sites-enabled/default +unlink /etc/nginx/sites-enabled/default || true SETUP_CHATMAIL_SERVICE_PATH="${SETUP_CHATMAIL_SERVICE_PATH:-/lib/systemd/system/setup_chatmail.service}" From 9bf27550c4c6bbced59a5f0bd64bf283ac885cb3 Mon Sep 17 00:00:00 2001 From: missytake Date: Tue, 26 Aug 2025 10:46:48 +0200 Subject: [PATCH 13/26] docker: enable DNS checks before cmdeploy run again --- docker/files/setup_chatmail_docker.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/files/setup_chatmail_docker.sh b/docker/files/setup_chatmail_docker.sh index 302291aef..e22b6c7ff 100755 --- a/docker/files/setup_chatmail_docker.sh +++ b/docker/files/setup_chatmail_docker.sh @@ -69,7 +69,7 @@ fi ./scripts/cmdeploy init --config "${INI_FILE}" $INI_CMD_ARGS $MAIL_DOMAIN bash /update_ini.sh -./scripts/cmdeploy run --ssh-host docker --skip-dns-check +./scripts/cmdeploy run --ssh-host docker echo "ForwardToConsole=yes" >> /etc/systemd/journald.conf systemctl restart systemd-journald From a41f21bc4320355819d2df622ddb7d9c804b0e75 Mon Sep 17 00:00:00 2001 From: missytake Date: Thu, 13 Nov 2025 21:22:31 +0100 Subject: [PATCH 14/26] Suggestions from @Keonik1 Co-authored-by: Keonik <57857901+Keonik1@users.noreply.github.com> --- docker/files/setup_chatmail_docker.sh | 10 +++++----- docs/DOCKER_INSTALLATION_EN.md | 4 ++++ docs/DOCKER_INSTALLATION_RU.md | 5 ++++- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/docker/files/setup_chatmail_docker.sh b/docker/files/setup_chatmail_docker.sh index e22b6c7ff..c5daff34f 100755 --- a/docker/files/setup_chatmail_docker.sh +++ b/docker/files/setup_chatmail_docker.sh @@ -47,11 +47,11 @@ monitor_certificates() { ### MAIN -if [ "$DEBUG_COMMANDS_ENABLED" == "true" ]; then +if [ "$DEBUG_COMMANDS_ENABLED" = true ]; then debug_commands fi -if [ "$FORCE_REINIT_INI_FILE" == "true" ]; then +if [ "$FORCE_REINIT_INI_FILE" = true ]; then INI_CMD_ARGS=--force fi @@ -61,15 +61,15 @@ chown opendkim:opendkim /etc/dkimkeys/opendkim.txt # TODO: Move to debug_commands after git clone is moved to dockerfile. git config --global --add safe.directory /opt/chatmail -if [ "$RECREATE_VENV" == "true" ]; then +if [ "$RECREATE_VENV" = true ]; then rm -rf venv fi ./scripts/initenv.sh -./scripts/cmdeploy init --config "${INI_FILE}" $INI_CMD_ARGS $MAIL_DOMAIN +./scripts/cmdeploy init --config "${INI_FILE}" $INI_CMD_ARGS $MAIL_DOMAIN || true bash /update_ini.sh -./scripts/cmdeploy run --ssh-host docker +./scripts/cmdeploy run --ssh-host @docker echo "ForwardToConsole=yes" >> /etc/systemd/journald.conf systemctl restart systemd-journald diff --git a/docs/DOCKER_INSTALLATION_EN.md b/docs/DOCKER_INSTALLATION_EN.md index 7a66e456a..8e61256d2 100644 --- a/docs/DOCKER_INSTALLATION_EN.md +++ b/docs/DOCKER_INSTALLATION_EN.md @@ -38,7 +38,11 @@ Please substitute it with your own domain. ```shell cp ./docker/docker-compose-default.yaml docker-compose.yaml ``` +2. Copy `./docker/example.env` and rename it to `.env`. This file stores variables used in `docker-compose.yaml`. +```shell +cp ./docker/example.env .env +```\ 3. Configure environment variables in the `.env` file. These variables are used in the `docker-compose.yaml` file to pass repeated values. 4. Configure kernel parameters because they cannot be changed inside the container, specifically `fs.inotify.max_user_instances` and `fs.inotify.max_user_watches`. Run the following: diff --git a/docs/DOCKER_INSTALLATION_RU.md b/docs/DOCKER_INSTALLATION_RU.md index e90cf8895..6c7409cf1 100644 --- a/docs/DOCKER_INSTALLATION_RU.md +++ b/docs/DOCKER_INSTALLATION_RU.md @@ -34,7 +34,10 @@ Please substitute it with your own domain. ```shell cp ./docker/docker-compose-default.yaml docker-compose.yaml ``` - +2. Скопировать `./docker/example.env` и переименовать в `.env`. Здесь хранятся переменные, которые используются в `docker-compose.yaml`. +```shell +cp ./docker/example.env .env +```\ 3. Настроить переменные окружения в `.env` файле. Эти переменные используются в `docker-compose.yaml` файле, чтобы передавать повторяющиеся значения. 4. Настроить параметры ядра, потому что внутри контейнера их нельзя изменить, а конкретно `fs.inotify.max_user_instances` и `fs.inotify.max_user_watches`. Для этого выполнить следующее: From fb7b76ae7ea72987c0ad5f885416fa3df66eca7c Mon Sep 17 00:00:00 2001 From: missytake Date: Thu, 13 Nov 2025 21:29:51 +0100 Subject: [PATCH 15/26] docker: disable port check if docker is running. fix #694 --- cmdeploy/src/cmdeploy/cmdeploy.py | 2 + cmdeploy/src/cmdeploy/deployers.py | 60 +++++++++++++++--------------- cmdeploy/src/cmdeploy/run.py | 4 +- 3 files changed, 35 insertions(+), 31 deletions(-) diff --git a/cmdeploy/src/cmdeploy/cmdeploy.py b/cmdeploy/src/cmdeploy/cmdeploy.py index 9877dd0e8..9888b0070 100644 --- a/cmdeploy/src/cmdeploy/cmdeploy.py +++ b/cmdeploy/src/cmdeploy/cmdeploy.py @@ -109,6 +109,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"): diff --git a/cmdeploy/src/cmdeploy/deployers.py b/cmdeploy/src/cmdeploy/deployers.py index 5812aa284..d9a549d9b 100644 --- a/cmdeploy/src/cmdeploy/deployers.py +++ b/cmdeploy/src/cmdeploy/deployers.py @@ -535,12 +535,13 @@ def activate(self): ) -def deploy_chatmail(config_path: Path, disable_mail: bool, website_only: bool) -> None: +def deploy_chatmail(config_path: Path, disable_mail: bool, website_only: 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 website_only: if True, only deploy the website + :param docker: whether it is running in a docker container """ config = read_config(config_path) check_config(config) @@ -566,34 +567,35 @@ def deploy_chatmail(config_path: Path, disable_mail: bool, website_only: bool) - Out().red(f"Deploy failed: mtail_address {config.mtail_address} is not available (VPN up?).\n") exit(1) - 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), - ("mtail", 3903), - ("stats", 3904), - ("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) - services = [service] if isinstance(service, str) else service - if running_service: - if running_service not in services: - 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), + ("mtail", 3903), + ("stats", 3904), + ("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) + services = [service] if isinstance(service, str) else service + if running_service: + if running_service not in services: + 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}"] diff --git a/cmdeploy/src/cmdeploy/run.py b/cmdeploy/src/cmdeploy/run.py index 0b0fc8581..91f96d7d9 100644 --- a/cmdeploy/src/cmdeploy/run.py +++ b/cmdeploy/src/cmdeploy/run.py @@ -15,8 +15,8 @@ def main(): ) disable_mail = bool(os.environ.get("CHATMAIL_DISABLE_MAIL")) website_only = bool(os.environ.get("CHATMAIL_WEBSITE_ONLY")) - - deploy_chatmail(config_path, disable_mail, website_only) + docker = bool(os.environ.get("CHATMAIL_DOCKER")) + deploy_chatmail(config_path, disable_mail, website_only, docker) if pyinfra.is_cli: From 48b00292dea3aaad3178c12e821c057b07c64ff3 Mon Sep 17 00:00:00 2001 From: missytake Date: Fri, 14 Nov 2025 12:18:20 +0100 Subject: [PATCH 16/26] doc: fix linebreak --- docs/DOCKER_INSTALLATION_EN.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/DOCKER_INSTALLATION_EN.md b/docs/DOCKER_INSTALLATION_EN.md index 8e61256d2..4e32155a4 100644 --- a/docs/DOCKER_INSTALLATION_EN.md +++ b/docs/DOCKER_INSTALLATION_EN.md @@ -42,7 +42,8 @@ cp ./docker/docker-compose-default.yaml docker-compose.yaml ```shell cp ./docker/example.env .env -```\ +``` + 3. Configure environment variables in the `.env` file. These variables are used in the `docker-compose.yaml` file to pass repeated values. 4. Configure kernel parameters because they cannot be changed inside the container, specifically `fs.inotify.max_user_instances` and `fs.inotify.max_user_watches`. Run the following: From cd7940229bfd0e65025c1cdbae9a1c6ec37fc6ff Mon Sep 17 00:00:00 2001 From: missytake Date: Fri, 14 Nov 2025 12:29:09 +0100 Subject: [PATCH 17/26] docker: move all configuration to example.env --- .gitignore | 1 - ...ompose-default.yaml => docker-compose.yaml | 16 +++++------ docker/example.env | 9 ++++++ docs/DOCKER_INSTALLATION_EN.md | 28 ++++++------------- docs/DOCKER_INSTALLATION_RU.md | 28 ++++++++----------- 5 files changed, 37 insertions(+), 45 deletions(-) rename docker/docker-compose-default.yaml => docker-compose.yaml (75%) diff --git a/.gitignore b/.gitignore index c6260e934..da7cd1c80 100644 --- a/.gitignore +++ b/.gitignore @@ -168,5 +168,4 @@ chatmail.zone # docker /data/ /custom/ -docker-compose.yaml .env diff --git a/docker/docker-compose-default.yaml b/docker-compose.yaml similarity index 75% rename from docker/docker-compose-default.yaml rename to docker-compose.yaml index b5b793447..08c656734 100644 --- a/docker/docker-compose-default.yaml +++ b/docker-compose.yaml @@ -22,14 +22,14 @@ services: environment: MAIL_DOMAIN: $MAIL_DOMAIN ACME_EMAIL: $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" + 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 ports: - "80:80" - "443:443" diff --git a/docker/example.env b/docker/example.env index 48655812d..e517654eb 100644 --- a/docker/example.env +++ b/docker/example.env @@ -1 +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" diff --git a/docs/DOCKER_INSTALLATION_EN.md b/docs/DOCKER_INSTALLATION_EN.md index 4e32155a4..5102049d8 100644 --- a/docs/DOCKER_INSTALLATION_EN.md +++ b/docs/DOCKER_INSTALLATION_EN.md @@ -33,11 +33,14 @@ Please substitute it with your own domain. ## Installation -1. Copy the file `./docker/docker-compose-default.yaml` to `docker-compose.yaml`. This is necessary because `docker-compose.yaml` is in `.gitignore` and won’t cause conflicts when updating the git repository. +1. Configure kernel parameters because they cannot be changed inside the container, specifically `fs.inotify.max_user_instances` and `fs.inotify.max_user_watches`. Run the following: ```shell -cp ./docker/docker-compose-default.yaml docker-compose.yaml +echo "fs.inotify.max_user_instances=65536" | sudo tee -a /etc/sysctl.d/99-inotify.conf +echo "fs.inotify.max_user_watches=65536" | sudo tee -a /etc/sysctl.d/99-inotify.conf +sudo sysctl --system ``` + 2. Copy `./docker/example.env` and rename it to `.env`. This file stores variables used in `docker-compose.yaml`. ```shell @@ -45,16 +48,7 @@ cp ./docker/example.env .env ``` 3. Configure environment variables in the `.env` file. These variables are used in the `docker-compose.yaml` file to pass repeated values. - -4. Configure kernel parameters because they cannot be changed inside the container, specifically `fs.inotify.max_user_instances` and `fs.inotify.max_user_watches`. Run the following: - -```shell -echo "fs.inotify.max_user_instances=65536" | sudo tee -a /etc/sysctl.d/99-inotify.conf -echo "fs.inotify.max_user_watches=65536" | sudo tee -a /etc/sysctl.d/99-inotify.conf -sudo sysctl --system -``` - -5. Configure container environment variables. Below is the list of variables used during deployment: + Below is the list of variables used during deployment: - `MAIL_DOMAIN` – The domain name of the future server. (required) - `DEBUG_COMMANDS_ENABLED` – Run debug commands before installation. (default: `false`) @@ -68,24 +62,20 @@ sudo sysctl --system You can also use any variables from the [ini configuration file](https://github.com/chatmail/relay/blob/main/chatmaild/src/chatmaild/ini/chatmail.ini.f); they must be in uppercase. -Mandatory variables for deployment via Docker: - -- `CHANGE_KERNEL_SETTINGS` – Change kernel settings (`fs.inotify.max_user_instances` and `fs.inotify.max_user_watches`) on startup. Changing kernel settings inside the container is not possible! (default: `False`) - -6. Build the Docker image: +4. Build the Docker image: ```shell docker compose build chatmail ``` -7. Start docker compose and wait for the installation to finish: +5. Start docker compose and wait for the installation to finish: ```shell docker compose up -d # start service docker compose logs -f chatmail # view container logs, press CTRL+C to exit ``` -8. After installation is complete, you can open `https://` in your browser. +6. After installation is complete, you can open `https://` in your browser. ## Using custom files diff --git a/docs/DOCKER_INSTALLATION_RU.md b/docs/DOCKER_INSTALLATION_RU.md index 6c7409cf1..fef0c29b8 100644 --- a/docs/DOCKER_INSTALLATION_RU.md +++ b/docs/DOCKER_INSTALLATION_RU.md @@ -30,24 +30,21 @@ Please substitute it with your own domain. ## Installation -1. Скопировать файл `./docker/docker-compose-default.yaml` в `docker-compose.yaml`. Это нужно потому что `docker-compose.yaml` находится в `.gitignore` и не будет создавать конфликты при обновлении гит репозитория. +1. Настроить параметры ядра, потому что внутри контейнера их нельзя изменить, а конкретно `fs.inotify.max_user_instances` и `fs.inotify.max_user_watches`. Для этого выполнить следующее: ```shell -cp ./docker/docker-compose-default.yaml docker-compose.yaml +echo "fs.inotify.max_user_instances=65536" | sudo tee -a /etc/sysctl.d/99-inotify.conf +echo "fs.inotify.max_user_watches=65536" | sudo tee -a /etc/sysctl.d/99-inotify.conf +sudo sysctl --system ``` + 2. Скопировать `./docker/example.env` и переименовать в `.env`. Здесь хранятся переменные, которые используются в `docker-compose.yaml`. ```shell cp ./docker/example.env .env -```\ -3. Настроить переменные окружения в `.env` файле. Эти переменные используются в `docker-compose.yaml` файле, чтобы передавать повторяющиеся значения. - -4. Настроить параметры ядра, потому что внутри контейнера их нельзя изменить, а конкретно `fs.inotify.max_user_instances` и `fs.inotify.max_user_watches`. Для этого выполнить следующее: -```shell -echo "fs.inotify.max_user_instances=65536" | sudo tee -a /etc/sysctl.d/99-inotify.conf -echo "fs.inotify.max_user_watches=65536" | sudo tee -a /etc/sysctl.d/99-inotify.conf -sudo sysctl --system ``` -5. Настроить переменные окружения контейнера. Ниже перечислен список переменных учавствующих при развертывании. +3. Настроить переменные окружения в `.env` файле. Эти переменные используются в `docker-compose.yaml` файле, чтобы передавать повторяющиеся значения. + Ниже перечислен список переменных учавствующих при развертывании: + - `MAIL_DOMAIN` - Доменное имя будущего сервера. (required) - `DEBUG_COMMANDS_ENABLED` - Выполнить debug команды перед установкой. (default: `false`) - `FORCE_REINIT_INI_FILE` - Пересоздавать ini файл конфигурации при запуске. (default: `false`) @@ -60,21 +57,18 @@ sudo sysctl --system Также могут быть использованы все переменные из [ini файла конфигурации](https://github.com/chatmail/relay/blob/main/chatmaild/src/chatmaild/ini/chatmail.ini.f), они обязаны быть в uppercase формате. -Ниже перечислены переменные, которые обязательны быть выставлены при развертывании через docker: -- `CHANGE_KERNEL_SETTINGS` - Менять настройки ядра (`fs.inotify.max_user_instances` и `fs.inotify.max_user_watches`) при запуске. При запуске в контейнере смена настроек ядра не может быть выполнена! (default: `False`) - -6. Собрать docker образ +4. Собрать docker образ ```shell docker compose build chatmail ``` -7. Запустить docker compose и дождаться завершения установки +5. Запустить docker compose и дождаться завершения установки ```shell docker compose up -d # запуск сервиса docker compose logs -f chatmail # просмотр логов контейнера. Для выхода нажать CTRL+C ``` -8. По окончанию установки можно открыть в браузер `https://` +6. По окончанию установки можно открыть в браузер `https://` ## Использование кастомных файлов При использовании docker есть возможность использовать измененые файлы конфигурации, чтобы сделать установку более персонализированной. Обычно это требуется для секции `www/src`, чтобы ознакомительная страница Chatmail была сделана на ваш вкус. Но также это можно использовать и для любых других случаев. From 15f7740aec4fb33b5f46dbf81ef8ba3b9149a9ab Mon Sep 17 00:00:00 2001 From: missytake Date: Fri, 14 Nov 2025 12:43:44 +0100 Subject: [PATCH 18/26] docker: open ports for TURN + STUN --- docker-compose.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docker-compose.yaml b/docker-compose.yaml index 08c656734..76d145e85 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -38,6 +38,8 @@ services: - "143:143" - "465:465" - "993:993" + - "3478:3478" + - "49152-65535:49152-65535/udp" volumes: ## system - /sys/fs/cgroup:/sys/fs/cgroup:rw # required for systemd From 1f03093616783e93c4cb6bee80064f8b02f0d3cc Mon Sep 17 00:00:00 2001 From: missytake Date: Fri, 14 Nov 2025 14:26:52 +0100 Subject: [PATCH 19/26] docker: use --network=host so chatmail-turn can use any port --- docker-compose.yaml | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/docker-compose.yaml b/docker-compose.yaml index 76d145e85..79feb1491 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -30,16 +30,7 @@ services: ENABLE_CERTS_MONITORING: $ENABLE_CERTS_MONITORING CERTS_MONITORING_TIMEOUT: $CERTS MONITORING TIMEOUT IS_DEVELOPMENT_INSTANCE: $IS_DEVELOPMENT_INSTANCE - ports: - - "80:80" - - "443:443" - - "25:25" - - "587:587" - - "143:143" - - "465:465" - - "993:993" - - "3478:3478" - - "49152-65535:49152-65535/udp" + network_mode: "host" volumes: ## system - /sys/fs/cgroup:/sys/fs/cgroup:rw # required for systemd From 0c10e63a6a562e64a47fcf6e78258d4bee0ec913 Mon Sep 17 00:00:00 2001 From: missytake Date: Tue, 18 Nov 2025 14:05:44 +0100 Subject: [PATCH 20/26] cmdeploy: add config (, ) --- cmdeploy/src/cmdeploy/dovecot/deployer.py | 25 ++++++++++++----------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/cmdeploy/src/cmdeploy/dovecot/deployer.py b/cmdeploy/src/cmdeploy/dovecot/deployer.py index 3b3a27158..5a505ecb5 100644 --- a/cmdeploy/src/cmdeploy/dovecot/deployer.py +++ b/cmdeploy/src/cmdeploy/dovecot/deployer.py @@ -116,18 +116,19 @@ def _configure_dovecot(config: Config, debug: bool = False) -> (bool, bool): # 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] > 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", From c7a92ee27be62c2cac566eb198bfd582e7a5ea71 Mon Sep 17 00:00:00 2001 From: Keonik1 Date: Tue, 18 Nov 2025 14:18:18 +0100 Subject: [PATCH 21/26] cmdeploy: Add config parameters `change_kernel_settings` and `fs_inotify_max_user_instances_and_watchers` --- CHANGELOG.md | 5 +++++ chatmaild/src/chatmaild/config.py | 6 ++++++ chatmaild/src/chatmaild/ini/chatmail.ini.f | 10 ++++++++++ docker-compose.yaml | 1 + docker/files/setup_chatmail_docker.sh | 1 + 5 files changed, 23 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e5a4a8a53..49fbea8e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -124,6 +124,11 @@ - 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 - Make www upload path configurable diff --git a/chatmaild/src/chatmaild/config.py b/chatmaild/src/chatmaild/config.py index d19e966af..42fc9c514 100644 --- a/chatmaild/src/chatmaild/config.py +++ b/chatmaild/src/chatmaild/config.py @@ -47,6 +47,12 @@ def __init__(self, inipath, params): self.addr_v4 = os.environ.get("CHATMAIL_ADDR_V4", "") self.addr_v6 = os.environ.get("CHATMAIL_ADDR_V6", "") 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" self.imap_compress = params.get("imap_compress", "false").lower() == "true" if "iroh_relay" not in params: diff --git a/chatmaild/src/chatmaild/ini/chatmail.ini.f b/chatmaild/src/chatmaild/ini/chatmail.ini.f index 29d7baa9e..fc2cfa787 100644 --- a/chatmaild/src/chatmaild/ini/chatmail.ini.f +++ b/chatmaild/src/chatmaild/ini/chatmail.ini.f @@ -69,6 +69,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 diff --git a/docker-compose.yaml b/docker-compose.yaml index 79feb1491..b339812d8 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -20,6 +20,7 @@ services: max-size: "10m" max-file: "3" environment: + CHANGE_KERNEL_SETTINGS: "False" MAIL_DOMAIN: $MAIL_DOMAIN ACME_EMAIL: $ACME_EMAIL RECREATE_VENV: $RECREATE_VENV diff --git a/docker/files/setup_chatmail_docker.sh b/docker/files/setup_chatmail_docker.sh index c5daff34f..be3f0853e 100755 --- a/docker/files/setup_chatmail_docker.sh +++ b/docker/files/setup_chatmail_docker.sh @@ -5,6 +5,7 @@ export INI_FILE="${INI_FILE:-chatmail.ini}" export ENABLE_CERTS_MONITORING="${ENABLE_CERTS_MONITORING:-true}" export CERTS_MONITORING_TIMEOUT="${CERTS_MONITORING_TIMEOUT:-60}" export PATH_TO_SSL="${PATH_TO_SSL:-/var/lib/acme/live/${MAIL_DOMAIN}}" +export CHANGE_KERNEL_SETTINGS=${CHANGE_KERNEL_SETTINGS:-"False"} export RECREATE_VENV=${RECREATE_VENV:-"false"} if [ -z "$MAIL_DOMAIN" ]; then From f3eadbd770fc62047297ebf1779bb00965b992dd Mon Sep 17 00:00:00 2001 From: j4n Date: Fri, 13 Feb 2026 14:19:47 +0100 Subject: [PATCH 22/26] docker: remove echobot parts that were lingering in the feature branch --- cmdeploy/src/cmdeploy/cmdeploy.py | 13 ------------- cmdeploy/src/cmdeploy/run.py | 1 + docker-compose.yaml | 7 +++---- 3 files changed, 4 insertions(+), 17 deletions(-) diff --git a/cmdeploy/src/cmdeploy/cmdeploy.py b/cmdeploy/src/cmdeploy/cmdeploy.py index 9888b0070..4336f1023 100644 --- a/cmdeploy/src/cmdeploy/cmdeploy.py +++ b/cmdeploy/src/cmdeploy/cmdeploy.py @@ -125,19 +125,6 @@ def run_cmd(args, out): else: out.red("Website deployment failed.") elif retcode == 0: - if not args.disable_mail: - print("\nYou can try out the relay by talking to this echo bot: ") - sshexec = SSHExec(args.config.mail_domain, verbose=args.verbose) - print( - sshexec( - call=remote.rshell.shell, - 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 args.dns_check_disabled and not remote_data["acme_account_url"]: out.red("Deploy completed but letsencrypt not configured") diff --git a/cmdeploy/src/cmdeploy/run.py b/cmdeploy/src/cmdeploy/run.py index 91f96d7d9..e95d5a1e4 100644 --- a/cmdeploy/src/cmdeploy/run.py +++ b/cmdeploy/src/cmdeploy/run.py @@ -16,6 +16,7 @@ def main(): disable_mail = bool(os.environ.get("CHATMAIL_DISABLE_MAIL")) website_only = bool(os.environ.get("CHATMAIL_WEBSITE_ONLY")) docker = bool(os.environ.get("CHATMAIL_DOCKER")) + deploy_chatmail(config_path, disable_mail, website_only, docker) diff --git a/docker-compose.yaml b/docker-compose.yaml index b339812d8..a81476437 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,13 +1,13 @@ services: chatmail: - build: + build: context: ./docker dockerfile: chatmail_relay.dockerfile tags: - chatmail-relay:latest image: chatmail-relay:latest restart: unless-stopped - container_name: chatmail + container_name: chatmail cgroup: host # required for systemd tty: true # required for logs tmpfs: # required for systemd @@ -36,11 +36,10 @@ services: ## 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 From e57bdd2a664c883dd1ff08ed3474beb13801fa67 Mon Sep 17 00:00:00 2001 From: j4n Date: Fri, 13 Feb 2026 14:58:27 +0100 Subject: [PATCH 23/26] feat(cmdeploy): guard against non-running systemd This enables docker image building without systemd running, which would make pyinfra SystemdEnabled fail. --- cmdeploy/src/cmdeploy/basedeploy.py | 5 +++++ cmdeploy/src/cmdeploy/deployers.py | 5 ++++- cmdeploy/src/cmdeploy/dovecot/deployer.py | 10 ++++++---- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/cmdeploy/src/cmdeploy/basedeploy.py b/cmdeploy/src/cmdeploy/basedeploy.py index dcb17a3ca..45654c27e 100644 --- a/cmdeploy/src/cmdeploy/basedeploy.py +++ b/cmdeploy/src/cmdeploy/basedeploy.py @@ -5,6 +5,11 @@ from pyinfra.operations import files, server, systemd +def has_systemd(): + """Returns False during Docker image builds or any other non-systemd environment.""" + return os.path.isdir("/run/systemd/system") + + def get_resource(arg, pkg=__package__): return importlib.resources.files(pkg).joinpath(arg) diff --git a/cmdeploy/src/cmdeploy/deployers.py b/cmdeploy/src/cmdeploy/deployers.py index d9a549d9b..c38f2dd5f 100644 --- a/cmdeploy/src/cmdeploy/deployers.py +++ b/cmdeploy/src/cmdeploy/deployers.py @@ -25,6 +25,7 @@ activate_remote_units, configure_remote_units, get_resource, + has_systemd, ) from .dovecot.deployer import DovecotDeployer from .filtermail.deployer import FiltermailDeployer @@ -65,6 +66,8 @@ def _build_chatmaild(dist_dir) -> None: def remove_legacy_artifacts(): + if not has_systemd(): + return # disable legacy doveauth-dictproxy.service if host.get_fact(SystemdEnabled).get("doveauth-dictproxy.service"): systemd.service( @@ -299,7 +302,7 @@ def install(self): present=False, ) # remove echobot if it is still running - if host.get_fact(SystemdEnabled).get("echobot.service"): + if has_systemd() and host.get_fact(SystemdEnabled).get("echobot.service"): systemd.service( name="Disable echobot.service", service="echobot.service", diff --git a/cmdeploy/src/cmdeploy/dovecot/deployer.py b/cmdeploy/src/cmdeploy/dovecot/deployer.py index 5a505ecb5..9a7a42321 100644 --- a/cmdeploy/src/cmdeploy/dovecot/deployer.py +++ b/cmdeploy/src/cmdeploy/dovecot/deployer.py @@ -9,6 +9,7 @@ activate_remote_units, configure_remote_units, get_resource, + has_systemd, ) @@ -22,10 +23,11 @@ def __init__(self, config, disable_mail): def install(self): arch = host.get_fact(Arch) - if not "dovecot.service" in host.get_fact(SystemdEnabled): - _install_dovecot_package("core", arch) - _install_dovecot_package("imapd", arch) - _install_dovecot_package("lmtpd", arch) + if has_systemd() and "dovecot.service" in host.get_fact(SystemdEnabled): + return # already installed and running + _install_dovecot_package("core", arch) + _install_dovecot_package("imapd", arch) + _install_dovecot_package("lmtpd", arch) def configure(self): configure_remote_units(self.config.mail_domain, self.units) From ab918ca84abb9caf95e7afc2dbaa519de280807d Mon Sep 17 00:00:00 2001 From: j4n Date: Fri, 13 Feb 2026 15:03:04 +0100 Subject: [PATCH 24/26] docker: widen build context to repo root for build-time install stage The Dockerfile will need access to chatmaild/ and cmdeploy/ source trees to run CMDEPLOY_STAGES=install via pyinfra during image build, moving install-time work out of container startup. The previous context (./docker) only included helper scripts. Also adds .dockerignore to exclude .git, data/, venv/ etc. from the build context, and updates COPY paths accordingly. --- .dockerignore | 7 +++++++ docker-compose.yaml | 4 ++-- docker/chatmail_relay.dockerfile | 8 ++++---- 3 files changed, 13 insertions(+), 6 deletions(-) create mode 100644 .dockerignore diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..5cd92bcc2 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,7 @@ +.git +data/ +venv/ +__pycache__ +*.pyc +*.orig +.pytest_cache diff --git a/docker-compose.yaml b/docker-compose.yaml index a81476437..9372eb0f4 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,8 +1,8 @@ services: chatmail: build: - context: ./docker - dockerfile: chatmail_relay.dockerfile + context: ./ + dockerfile: docker/chatmail_relay.dockerfile tags: - chatmail-relay:latest image: chatmail-relay:latest diff --git a/docker/chatmail_relay.dockerfile b/docker/chatmail_relay.dockerfile index 3ec5f81e5..b03e495fe 100644 --- a/docker/chatmail_relay.dockerfile +++ b/docker/chatmail_relay.dockerfile @@ -55,12 +55,12 @@ RUN apt-get update && \ WORKDIR /opt/chatmail ARG SETUP_CHATMAIL_SERVICE_PATH=/lib/systemd/system/setup_chatmail.service -COPY ./files/setup_chatmail.service "$SETUP_CHATMAIL_SERVICE_PATH" +COPY ./docker/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 +COPY --chmod=555 ./docker/files/setup_chatmail_docker.sh /setup_chatmail_docker.sh +COPY --chmod=555 ./docker/files/update_ini.sh /update_ini.sh +COPY --chmod=555 ./docker/files/entrypoint.sh /entrypoint.sh ## TODO: add git clone. ## Problem: how correct save only required files inside container.... From 91f8f3fc740312e20f866b9a2cd8d185455b5cb7 Mon Sep 17 00:00:00 2001 From: j4n Date: Fri, 13 Feb 2026 15:10:23 +0100 Subject: [PATCH 25/26] docker: run install stage at build time, configure+activate at startup Move the CMDEPLOY_STAGES=install execution into the Dockerfile these operations baked into the image layer. On container start, only configure and activate stages run by default. Users can override with CMDEPLOY_STAGES="install,configure,activate" to force a full reinstall without rebuilding the image. Also fixes CERTS_MONITORING_TIMEOUT typo in docker-compose.yaml (was "$CERTS MONITORING TIMEOUT"), and replaces the docker-commit workaround in docs with CMDEPLOY_STAGES documentation. --- docker-compose.yaml | 3 +- docker/chatmail_relay.dockerfile | 37 +++++++++---- docker/example.env | 1 + docker/files/setup_chatmail_docker.sh | 1 + docs/DOCKER_INSTALLATION_EN.md | 76 +++------------------------ 5 files changed, 39 insertions(+), 79 deletions(-) diff --git a/docker-compose.yaml b/docker-compose.yaml index 9372eb0f4..8d5733edf 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -29,8 +29,9 @@ services: 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 + CERTS_MONITORING_TIMEOUT: $CERTS_MONITORING_TIMEOUT IS_DEVELOPMENT_INSTANCE: $IS_DEVELOPMENT_INSTANCE + CMDEPLOY_STAGES: ${CMDEPLOY_STAGES:-} network_mode: "host" volumes: ## system diff --git a/docker/chatmail_relay.dockerfile b/docker/chatmail_relay.dockerfile index b03e495fe..13322c6ed 100644 --- a/docker/chatmail_relay.dockerfile +++ b/docker/chatmail_relay.dockerfile @@ -54,6 +54,33 @@ RUN apt-get update && \ WORKDIR /opt/chatmail +# --- Build-time install stage --- +# Bake the "install" deployer stage into the image; we can't use +# scripts/initenv.sh because /opt/chatmail is empty at build time as +# source arrives at runtime via volume mount., so we use a throwaway venv. +# On container start only "configure,activate" stages run. +COPY . /tmp/chatmail-src/ +WORKDIR /tmp/chatmail-src + +# Dummy config — deploy_chatmail() needs a parseable ini to instantiate deployers +RUN printf '[params]\nmail_domain = build.local\n' > /tmp/chatmail.ini + +# Do what initenv.sh would do without the docs +RUN python3 -m venv /tmp/build-venv && \ + /tmp/build-venv/bin/pip install --no-cache-dir \ + -e chatmaild -e cmdeploy + +RUN CMDEPLOY_STAGES=install \ + CHATMAIL_INI=/tmp/chatmail.ini \ + CHATMAIL_DOCKER=True \ + /tmp/build-venv/bin/pyinfra @local \ + /tmp/chatmail-src/cmdeploy/src/cmdeploy/run.py -y + +RUN rm -rf /tmp/chatmail-src /tmp/build-venv /tmp/chatmail.ini + +WORKDIR /opt/chatmail +# --- End build-time install stage --- + ARG SETUP_CHATMAIL_SERVICE_PATH=/lib/systemd/system/setup_chatmail.service COPY ./docker/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" @@ -62,13 +89,6 @@ COPY --chmod=555 ./docker/files/setup_chatmail_docker.sh /setup_chatmail_docker. COPY --chmod=555 ./docker/files/update_ini.sh /update_ini.sh COPY --chmod=555 ./docker/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 @@ -78,6 +98,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. diff --git a/docker/example.env b/docker/example.env index e517654eb..4aa8e2dc6 100644 --- a/docker/example.env +++ b/docker/example.env @@ -8,3 +8,4 @@ MAIL_DOMAIN="chat.example.com" # ENABLE_CERTS_MONITORING="true" # CERTS_MONITORING_TIMEOUT=10 # IS_DEVELOPMENT_INSTANCE="True" +# CMDEPLOY_STAGES - default: "configure,activate". Set to "install,configure,activate" to force full reinstall. diff --git a/docker/files/setup_chatmail_docker.sh b/docker/files/setup_chatmail_docker.sh index be3f0853e..bfcfb5492 100755 --- a/docker/files/setup_chatmail_docker.sh +++ b/docker/files/setup_chatmail_docker.sh @@ -70,6 +70,7 @@ fi ./scripts/cmdeploy init --config "${INI_FILE}" $INI_CMD_ARGS $MAIL_DOMAIN || true bash /update_ini.sh +export CMDEPLOY_STAGES="${CMDEPLOY_STAGES:-configure,activate}" ./scripts/cmdeploy run --ssh-host @docker echo "ForwardToConsole=yes" >> /etc/systemd/journald.conf diff --git a/docs/DOCKER_INSTALLATION_EN.md b/docs/DOCKER_INSTALLATION_EN.md index 5102049d8..d306b2ef1 100644 --- a/docs/DOCKER_INSTALLATION_EN.md +++ b/docs/DOCKER_INSTALLATION_EN.md @@ -1,6 +1,5 @@ # Known issues and limitations -- Chatmail will be reinstalled every time the container is started (longer the first time, faster on subsequent starts). This is how the original installer works because it wasn’t designed for Docker. At the end of the documentation, there’s a [proposed solution](#locking-the-chatmail-version). - Requires cgroups v2 configured in the system. Operation with cgroups v1 has not been tested. - Yes, of course, using systemd inside a container is a hack, and it would be better to split it into several services, but since this is an MVP, it turned out to be easier to do it this way initially than to rewrite the entire deployment system. - The Docker image is only suitable for amd64. If you need to run it on a different architecture, try modifying the Dockerfile (specifically the part responsible for installing dovecot). @@ -59,6 +58,7 @@ cp ./docker/example.env .env - `PATH_TO_SSL` – Path to where the certificates are stored. (default: `/var/lib/acme/live/${MAIL_DOMAIN}`) - `ENABLE_CERTS_MONITORING` – Enable certificate monitoring if `USE_FOREIGN_CERT_MANAGER=true`. If certificates change, services will be automatically restarted. (default: `false`) - `CERTS_MONITORING_TIMEOUT` – Interval in seconds to check if certificates have changed. (default: `'60'`) +- `CMDEPLOY_STAGES` – Deployment stages to run on container start. (default: `"configure,activate"`). Set to `"install,configure,activate"` to force a full reinstall. You can also use any variables from the [ini configuration file](https://github.com/chatmail/relay/blob/main/chatmaild/src/chatmaild/ini/chatmail.ini.f); they must be in uppercase. @@ -114,79 +114,19 @@ docker compose down docker compose up -d ``` -## Locking the Chatmail version +## Forcing a full reinstall -> [!note] -> These steps are optional and should only be done if you are not satisfied that the service is installed each time the container starts. +The Docker image bakes the install stage (binary downloads, package setup, chatmaild venv) into the image at build time. On container start, only the `configure` and `activate` stages run by default. -Since the current Docker version installs the Chatmail service every time the container starts, you can lock the container version after installation as follows: - -1. Commit the current state of the configured container: - -```shell -docker container commit chatmail configured-chatmail:$(date +'%Y-%m-%d') -docker image ls | grep configured-chatmail -``` - -2. Change the entrypoint for the container in `docker-compose.yaml` to: - -```yaml -services: - chatmail: - image: - volumes: - ... - ## custom resources - - ./custom/setup_chatmail_docker.sh:/setup_chatmail_docker.sh -``` - -3. Create the file `./custom/setup_chatmail_docker.sh` with the new configuration: +To force a full reinstall (e.g., after updating the source), either rebuild the image: ```shell -mkdir -p ./custom -cat > ./custom/setup_chatmail_docker.sh << 'EOF' -#!/bin/bash - -set -eo pipefail - -export ENABLE_CERTS_MONITORING="${ENABLE_CERTS_MONITORING:-true}" -export CERTS_MONITORING_TIMEOUT="${CERTS_MONITORING_TIMEOUT:-60}" -export PATH_TO_SSL="${PATH_TO_SSL:-/var/lib/acme/live/${MAIL_DOMAIN}}" - -calculate_hash() { - find "$PATH_TO_SSL" -type f -exec sha1sum {} \; | sort | sha1sum | awk '{print $1}' -} - -monitor_certificates() { - if [ "$ENABLE_CERTS_MONITORING" != "true" ]; then - echo "Certs monitoring disabled." - exit 0 - fi - - current_hash=$(calculate_hash) - previous_hash=$current_hash - - while true; do - current_hash=$(calculate_hash) - if [[ "$current_hash" != "$previous_hash" ]]; then - # TODO: add an option to restart at a specific time interval - echo "[INFO] Certificate's folder hash was changed, reloading nginx, dovecot and postfix services." - systemctl reload nginx.service - systemctl reload dovecot.service - systemctl reload postfix.service - previous_hash=$current_hash - fi - sleep $CERTS_MONITORING_TIMEOUT - done -} - -monitor_certificates & -EOF +docker compose build chatmail +docker compose up -d ``` -4. Restart the service: +Or override the stages at runtime without rebuilding: ```shell -docker compose down -docker compose up -d +CMDEPLOY_STAGES="install,configure,activate" docker compose up -d ``` From 477635aba543a23f831e2d830339cf7f8f4b3655 Mon Sep 17 00:00:00 2001 From: j4n Date: Fri, 13 Feb 2026 15:43:08 +0100 Subject: [PATCH 26/26] docker: don't overwrite existing DKIM keys on container start opendkim-genkey was running unconditionally on every startup, check if file exists and skip. --- docker/files/setup_chatmail_docker.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docker/files/setup_chatmail_docker.sh b/docker/files/setup_chatmail_docker.sh index bfcfb5492..81a47bd96 100755 --- a/docker/files/setup_chatmail_docker.sh +++ b/docker/files/setup_chatmail_docker.sh @@ -56,7 +56,9 @@ if [ "$FORCE_REINIT_INI_FILE" = true ]; then INI_CMD_ARGS=--force fi -/usr/sbin/opendkim-genkey -D /etc/dkimkeys -d $MAIL_DOMAIN -s opendkim +if [ ! -f /etc/dkimkeys/opendkim.private ]; then + /usr/sbin/opendkim-genkey -D /etc/dkimkeys -d $MAIL_DOMAIN -s opendkim +fi chown opendkim:opendkim /etc/dkimkeys/opendkim.private chown opendkim:opendkim /etc/dkimkeys/opendkim.txt