From 645b60d293989974a8278281f061287ce8ad0f2d Mon Sep 17 00:00:00 2001 From: j4n Date: Mon, 16 Feb 2026 11:31:42 +0100 Subject: [PATCH] docker: make compose work with cgroups (v2), conversion scripts/docs --- docker-compose.yaml | 6 +- docker/cm_ini_to_env.py | 84 +++++++++++++++++++++++++++ docker/files/setup_chatmail_docker.sh | 5 +- docs/DOCKER_INSTALLATION_EN.md | 55 +++++++++++++++++- 4 files changed, 145 insertions(+), 5 deletions(-) create mode 100755 docker/cm_ini_to_env.py diff --git a/docker-compose.yaml b/docker-compose.yaml index 8d5733ed..92c13562 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -3,12 +3,12 @@ services: build: context: ./ dockerfile: docker/chatmail_relay.dockerfile - tags: - - chatmail-relay:latest image: chatmail-relay:latest restart: unless-stopped container_name: chatmail - cgroup: host # required for systemd + # Required for systemd — use only one of the following: + cgroup: host # compose v2 only + # privileged: true # compose v1 (not tested) tty: true # required for logs tmpfs: # required for systemd - /tmp diff --git a/docker/cm_ini_to_env.py b/docker/cm_ini_to_env.py new file mode 100755 index 00000000..fcb2c576 --- /dev/null +++ b/docker/cm_ini_to_env.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python3 +"""Convert a chatmail.ini to a Docker .env file. + +Usage: python docker/cm_ini_to_env.py [chatmail.ini] [.env] + +Reads the ini file, extracts all non-default key=value pairs, +and writes them as UPPER_CASE env vars suitable for docker-compose. +""" + +import configparser +import sys +from pathlib import Path + +# Keys that only make sense for bare-metal deploys or are handled +# separately by the Docker setup and should not appear in .env. +SKIP_KEYS = set() + +# Keys that exist in .env but have a different name than the ini key. +# ini_key -> env_key +RENAMES = {} + + +def read_ini(path): + """Return dict of key=value from [params] section.""" + cp = configparser.ConfigParser() + cp.read(path) + if not cp.has_section("params"): + sys.exit(f"Error: {path} has no [params] section") + return dict(cp.items("params")) + + +def read_defaults(): + """Return dict of default values from the ini template.""" + template = Path(__file__).resolve().parent.parent / "chatmaild/src/chatmaild/ini/chatmail.ini.f" + if not template.exists(): + return {} + cp = configparser.ConfigParser() + cp.read(template) + if not cp.has_section("params"): + return {} + defaults = {} + for key, value in cp.items("params"): + # Template placeholders like {mail_domain} aren't real defaults. + if "{" not in value: + defaults[key] = value + return defaults + + +def ini_to_env(ini_path, only_non_default=True): + """Yield (ENV_KEY, value) pairs from an ini file.""" + params = read_ini(ini_path) + defaults = read_defaults() if only_non_default else {} + + for key, value in sorted(params.items()): + if key in SKIP_KEYS: + continue + if only_non_default and key in defaults and value.strip() == defaults[key].strip(): + continue + env_key = RENAMES.get(key, key.upper()) + yield env_key, value.strip() + + +def main(): + ini_path = sys.argv[1] if len(sys.argv) > 1 else "chatmail.ini" + env_path = sys.argv[2] if len(sys.argv) > 2 else None + + if not Path(ini_path).exists(): + sys.exit(f"Error: {ini_path} not found") + + lines = [] + for env_key, value in ini_to_env(ini_path): + lines.append(f'{env_key}="{value}"') + + output = "\n".join(lines) + "\n" + + if env_path: + Path(env_path).write_text(output) + print(f"Wrote {len(lines)} variables to {env_path}") + else: + print(output, end="") + + +if __name__ == "__main__": + main() diff --git a/docker/files/setup_chatmail_docker.sh b/docker/files/setup_chatmail_docker.sh index 81a47bd9..f3ac4277 100755 --- a/docker/files/setup_chatmail_docker.sh +++ b/docker/files/setup_chatmail_docker.sh @@ -67,7 +67,10 @@ git config --global --add safe.directory /opt/chatmail if [ "$RECREATE_VENV" = true ]; then rm -rf venv fi -./scripts/initenv.sh +# Skip venv creation if it already exists +if [ ! -x venv/bin/python ] || [ ! -x venv/bin/cmdeploy ]; then + ./scripts/initenv.sh +fi ./scripts/cmdeploy init --config "${INI_FILE}" $INI_CMD_ARGS $MAIL_DOMAIN || true bash /update_ini.sh diff --git a/docs/DOCKER_INSTALLATION_EN.md b/docs/DOCKER_INSTALLATION_EN.md index d306b2ef..f93161a6 100644 --- a/docs/DOCKER_INSTALLATION_EN.md +++ b/docs/DOCKER_INSTALLATION_EN.md @@ -5,7 +5,13 @@ - 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. +This section provides instructions for installing Chatmail using Docker Compose. + +**Note:** Docker Compose v2 is required (`docker compose`, not `docker-compose`) for its support of the `cgroup: host` option in `docker-compose.yaml` is only supported by Compose v2. +[see documentation](https://docs.docker.com/engine/install/debian/#install-using-the-repository) +```shell +apt install docker-ce docker-compose-plugin docker.io- docker-compose- +``` ## Preliminary setup We use `chat.example.org` as the Chatmail domain in the following steps. @@ -75,6 +81,9 @@ docker compose up -d # start service docker compose logs -f chatmail # view container logs, press CTRL+C to exit ``` +### venv creation +The first container start takes longer because it creates the cmdeploy Python virtualenv at `/opt/chatmail/venv` (persisted on the host via volume mount). Subsequent starts reuse the existing venv. Set `RECREATE_VENV=true` in `.env` to force a rebuild if needed. + 6. After installation is complete, you can open `https://` in your browser. ## Using custom files @@ -114,6 +123,50 @@ docker compose down docker compose up -d ``` +## Migrating from a bare-metal install + +If you have an existing bare-metal Chatmail installation and want to switch to Docker: + +1. Stop all existing services: + +```shell +systemctl stop postfix dovecot doveauth nginx opendkim unbound acmetool-redirector \ + filtermail filtermail-incoming chatmail-turn iroh-relay chatmail-metadata \ + lastlogin mtail +systemctl disable postfix dovecot doveauth nginx opendkim unbound acmetool-redirector \ + filtermail filtermail-incoming chatmail-turn iroh-relay chatmail-metadata \ + lastlogin mtail +``` + +2. Convert your existing `chatmail.ini` to the Docker `.env` format: + +```shell +python3 docker/cm_ini_to_env.py /usr/local/lib/chatmaild/chatmail.ini .env +``` + +3. Copy persistent data into the `./data/` subdirectories: + +```shell +mkdir -p data/chatmail-dkimkeys data/chatmail-acme data/chatmail + +# DKIM keys +cp -a /etc/dkimkeys/* data/chatmail-dkimkeys/ + +# ACME certificates and account +rsync -a /var/lib/acme/ data/chatmail-acme/ + +# Mail data +rsync -a /home/ data/chatmail/ +``` + +Alternatively, you can mount `/home/vmail` directly by changing the volume in `docker-compose.yaml`: + +```yaml +- /home/vmail:/home/vmail +``` + +The three `./data/` subdirectories cover all persistent state. Everything else is regenerated by the `configure` and `activate` stages on container start. + ## Forcing a full reinstall 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.