From 40051f7ac3e35ff3df26cc6818e9c012b908c816 Mon Sep 17 00:00:00 2001 From: j4n Date: Thu, 5 Mar 2026 17:15:16 +0100 Subject: [PATCH] feat: add Docker Compose support Add container-based deployment as an alternative to bare-metal pyinfra. - systemd inside container reusing the existing deployer infrastructure - chatmail-init.sh runs `cmdeploy run --ssh-host @local` on first boot, so the container self-deploys using the same code path as bare-metal - Config via MAIL_DOMAIN env var (simple) or mounted chatmail.ini (advanced) - External TLS support via TLS_EXTERNAL_CERT_AND_KEY for reverse proxy setups - Image version tracking in /etc/chatmail-image-version for upgrade detection - .git/ excluded, but version file mocked so git revparse still works - Health check verifies postfix, dovecot, and nginx are listening Files added: - docker/chatmail_relay.dockerfile: multi-stage build (build + runtime) - docker/chatmail-init.sh: first-boot deployment script - docker/chatmail-init.service: systemd unit for init script - docker/entrypoint.sh: container entrypoint (starts systemd) - docker/healthcheck.sh: container health check - docker/docker-compose.yaml: main compose config - docker/docker-compose.ci.yaml: CI override (uses GHCR image) - docker/docker-compose.override.yaml.example: customization template - docker/build.sh: helper script - doc/source/docker.rst: documentation - .dockerignore: build context filter --- .dockerignore | 18 ++ .gitignore | 6 + doc/source/docker.rst | 264 ++++++++++++++++++++ doc/source/getting_started.rst | 6 + doc/source/index.rst | 1 + docker/build.sh | 9 + docker/chatmail-init.service | 14 ++ docker/chatmail-init.sh | 87 +++++++ docker/chatmail_relay.dockerfile | 110 ++++++++ docker/docker-compose.ci.yaml | 11 + docker/docker-compose.override.yaml.example | 44 ++++ docker/docker-compose.yaml | 48 ++++ docker/entrypoint.sh | 9 + docker/env.example | 1 + docker/healthcheck.sh | 16 ++ 15 files changed, 644 insertions(+) create mode 100644 .dockerignore create mode 100644 doc/source/docker.rst create mode 100755 docker/build.sh create mode 100644 docker/chatmail-init.service create mode 100755 docker/chatmail-init.sh create mode 100644 docker/chatmail_relay.dockerfile create mode 100644 docker/docker-compose.ci.yaml create mode 100644 docker/docker-compose.override.yaml.example create mode 100644 docker/docker-compose.yaml create mode 100755 docker/entrypoint.sh create mode 100644 docker/env.example create mode 100644 docker/healthcheck.sh diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..37c3d866 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,18 @@ +data/ +venv/ +__pycache__ +*.pyc +*.orig +*.ini +.pytest_cache +.env + +# Slim build context — .git/ alone can be 100s of MB +.git +.github/ +docs/ +tests/ + +# Exclude markdown files but keep www/src/*.md (used by WebsiteDeployer) +*.md +!www/**/*.md diff --git a/.gitignore b/.gitignore index c0f40b9b..db23c96e 100644 --- a/.gitignore +++ b/.gitignore @@ -164,3 +164,9 @@ cython_debug/ #.idea/ chatmail.zone + +# docker +/data/ +/custom/ +docker/docker-compose.override.yaml +docker/.env diff --git a/doc/source/docker.rst b/doc/source/docker.rst new file mode 100644 index 00000000..c8de5be0 --- /dev/null +++ b/doc/source/docker.rst @@ -0,0 +1,264 @@ +Docker installation +=================== + +This section provides instructions for installing a chatmail relay +using Docker Compose. + +.. note:: + + - Docker support is experimental, CI builds and tests the image automatically, but please report bugs. + - The image wraps the cmdeploy process detailed in the :doc:`getting_started` instructions in a Debian-systemd image with r/w access to `/sys/fs` + - Currently amd64-only (arm64 should work but is untested). + + +Setup Preparation +----------------- + +We use ``chat.example.org`` as the chatmail domain in the following +steps. Please substitute it with your own domain. + +1. Install docker and docker compose v2 (check with `docker compose version`), install, e.g., on + - Debian 12 through the `official install instructions `_ + - Debian 13+ with `apt install docker docker-compose` + +2. 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.org. 3600 IN A 198.51.100.5 + chat.example.org. 3600 IN AAAA 2001:db8::5 + www.chat.example.org. 3600 IN CNAME chat.example.org. + mta-sts.chat.example.org. 3600 IN CNAME chat.example.org. + +3. Configure kernel parameters on the host, as these can not be set from the container:: + + 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 + + +Docker Compose Setup +-------------------- + +Pre-built images are available from GitHub Container Registry. The +``main`` branch and tagged releases are pushed automatically by CI:: + + docker pull ghcr.io/chatmail/relay:main # latest main branch + docker pull ghcr.io/chatmail/relay:1.2.3 # tagged release + + +Create service directory +^^^^^^^^^^^^^^^^^^^^^^^^ + +Either: + +- Create a service directory and download the compose files:: + + mkdir -p /srv/chatmail-relay && cd /srv/chatmail-relay + wget https://raw.githubusercontent.com/chatmail/relay/refs/heads/main/docker/docker-compose.yaml + wget https://raw.githubusercontent.com/chatmail/relay/refs/heads/main/docker/docker-compose.override.yaml.example -O docker-compose.override.yaml + +- or clone the chatmail repo and enter the docker directory:: + + git clone https://github.com/chatmail/relay + cd relay/docker + + +Customize and start +^^^^^^^^^^^^^^^^^^^ + +1. Set the fully qualified domain name of the relay:: + + echo 'MAIL_DOMAIN=chat.example.org' > .env + + The container generates a ``chatmail.ini`` with defaults from + ``MAIL_DOMAIN`` on first start. To customize chatmail settings, mount + your own ``chatmail.ini`` instead (see `Custom chatmail.ini`_ below). + +2. All local customizations (data paths, extra volumes, config mounts) go in + ``docker-compose.override.yaml``, which Compose merges automatically with + the base file. By default, all data is stored in docker volumes, you will + likely want to at least create and configure the mail storage location, but + you might also want to configure external TLS certificates there. + +3. Start the container:: + + docker compose up -d + docker compose logs -f chatmail # view logs, Ctrl+C to exit + +4. After installation is complete, open ``https://chat.example.org`` in + your browser. + +Finish install and test +----------------------- + +You can test the installation with:: + + pip install cmping chat.example.org # or + uvx cmping chat.example.org # if you use https://docs.astral.sh/uv/ + +You should check and extend your DNS records for better interoperability:: + + # Show required DNS records + docker exec chatmail cmdeploy dns --ssh-host @local + +You can check server status with:: + + docker exec chatmail cmdeploy status --ssh-host @local + +You can run some benchmarks (can also run from any machine with cmdeploy installed):: + + docker exec chatmail cmdeploy bench + +You can run the test suite with:: + + docker exec chatmail cmdeploy test --ssh-host localhost + +You can look at logs:: + + docker exec chatmail journalctl -fu postfix@- + + +Customization +------------- + +Website +^^^^^^^^^^^^^^ + +You can customize the chatmail landing page by mounting a directory with +your own website source files. + +1. Create a directory with your custom website source:: + + mkdir -p ./custom/www/src + nano ./custom/www/src/index.md + +2. Add the volume mount in ``docker-compose.override.yaml``:: + + services: + chatmail: + volumes: + - ./custom/www:/opt/chatmail-www + +3. Restart the service:: + + docker compose down + docker compose up -d + + +Custom chatmail.ini +^^^^^^^^^^^^^^^^^^^ + +If you want to go beyond simply setting the ``MAIL_DOMAIN`` in ``.env``, you +can use a regular `chatmail.ini` to give you full control. + +1. Extract the generated config from a running container:: + + docker cp chatmail:/etc/chatmail/chatmail.ini ./chatmail.ini + +2. Edit ``chatmail.ini`` as needed. + +3. Add the volume mount in ``docker-compose.override.yaml`` :: + + services: + chatmail: + volumes: + - ./chatmail.ini:/etc/chatmail/chatmail.ini + +4. Restart the container, the container skips generating a new one: :: + + docker compose down && docker compose up -d + + +External TLS certificates +^^^^^^^^^^^^^^^^^^^^^^^^^ + +If TLS certificates are managed outside the container (e.g. by certbot, +acmetool, or Traefik on the host), mount them into the container and set +``TLS_EXTERNAL_CERT_AND_KEY`` in ``docker-compose.override.yaml``. +Changed certificates are picked up automatically via inotify. +See the examples in the example override and :ref:`external-tls` in the getting started guide for details. + + +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:: + + 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. Copy your existing ``chatmail.ini`` and mount it into the container + (see `Custom chatmail.ini`_ above):: + + cp /usr/local/lib/chatmaild/chatmail.ini ./chatmail.ini + +3. Copy persistent data into the ``./data/`` subdirectories (for example, as configured in `Customize and start`_) :: + + mkdir -p data/dkim data/certs data/mail + + # DKIM keys + cp -a /etc/dkimkeys/* data/dkim/ + + # TLS certificates + rsync -a /var/lib/acme/ data/certs/ + + Note that ownership of dkim and acme is adjusted on container start. + + For the mail directory:: + + rsync -a /home/vmail/ data/mail/ + + Alternatively, mount ``/home/vmail`` directly by changing the volume + in ``docker-compose-override.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. + +Building the image +------------------ + +Clone the repository and build the Docker image:: + + git clone https://github.com/chatmail/relay + cd relay + docker/build.sh + +The build bakes all binaries, Python packages, and the install stage +into the image. After building, only the ``docker/`` directory and a ``.env`` +with ``MAIL_DOMAIN`` are needed to run the container. The `build.sh` passes the +git hash onto the docker build so it can be determined if there has been a +change that warrants a redeploy. + +You can transfer a locally built image to your server directly (pigz is parallel `gzip` which can be used instead as well) :: + + docker save chatmail-relay:latest | pigz | ssh chat.example.org 'pigz -d | docker load' + + +Forcing a full reinstall +------------------------ + +On container start, only the ``configure`` and ``activate`` stages run by default. + +To force a full reinstall (e.g. after updating the source), either +rebuild the image:: + + docker compose build chatmail + docker compose up -d + +Or override the stages at runtime without rebuilding:: + + CMDEPLOY_STAGES="install,configure,activate" docker compose up -d diff --git a/doc/source/getting_started.rst b/doc/source/getting_started.rst index 28781f28..259d31b6 100644 --- a/doc/source/getting_started.rst +++ b/doc/source/getting_started.rst @@ -98,6 +98,12 @@ steps. Please substitute it with your own domain. configure at your DNS provider (it can take some time until they are public). +Docker installation +------------------- + +There is experimental support for running chatmail via Docker Compose. +See :doc:`docker` for full setup instructions. + Other helpful commands ---------------------- diff --git a/doc/source/index.rst b/doc/source/index.rst index d37a10f6..2ff58599 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -13,6 +13,7 @@ Contributions and feedback welcome through the https://github.com/chatmail/relay :maxdepth: 5 getting_started + docker proxy migrate overview diff --git a/docker/build.sh b/docker/build.sh new file mode 100755 index 00000000..04fa97aa --- /dev/null +++ b/docker/build.sh @@ -0,0 +1,9 @@ +#!/bin/sh +# Build the chatmail Docker image with the current git hash baked in. +# Usage: ./docker/build.sh [extra docker-compose build args...] +# +# .git/ is excluded from the build context (.dockerignore) so the hash +# must be passed as a build arg from the host. + +export GIT_HASH=$(git rev-parse HEAD) +exec docker compose -f docker/docker-compose.yaml build "$@" diff --git a/docker/chatmail-init.service b/docker/chatmail-init.service new file mode 100644 index 00000000..9e0a517b --- /dev/null +++ b/docker/chatmail-init.service @@ -0,0 +1,14 @@ +[Unit] +Description=Run container setup commands +After=multi-user.target +ConditionPathExists=/chatmail-init.sh + +[Service] +Type=oneshot +ExecStart=/bin/bash /chatmail-init.sh +RemainAfterExit=true +WorkingDirectory=/opt/chatmail +PassEnvironment= + +[Install] +WantedBy=multi-user.target diff --git a/docker/chatmail-init.sh b/docker/chatmail-init.sh new file mode 100755 index 00000000..ed94fe82 --- /dev/null +++ b/docker/chatmail-init.sh @@ -0,0 +1,87 @@ +#!/bin/bash + +set -euo pipefail +export CHATMAIL_INI="${CHATMAIL_INI:-/etc/chatmail/chatmail.ini}" +export CHATMAIL_NOSYSCTL=True +export CHATMAIL_NOPORTCHECK=True + +CMDEPLOY=/opt/cmdeploy/bin/cmdeploy + +if [ -z "$MAIL_DOMAIN" ]; then + echo "ERROR: Environment variable 'MAIL_DOMAIN' must be set!" >&2 + exit 1 +fi + +# Generate DKIM keys if not mounted +if [ ! -f /etc/dkimkeys/opendkim.private ]; then + /usr/sbin/opendkim-genkey -D /etc/dkimkeys -d "$MAIL_DOMAIN" -s opendkim +fi +# Fix ownership for bind-mounted keys (host opendkim UID may differ from container) +chown -R opendkim:opendkim /etc/dkimkeys + +# Create chatmail.ini, skip if mounted +mkdir -p "$(dirname "$CHATMAIL_INI")" +if [ ! -f "$CHATMAIL_INI" ]; then + $CMDEPLOY init --config "$CHATMAIL_INI" "$MAIL_DOMAIN" +fi + +# Auto-detect IPv6: if the host has no IPv6 connectivity, set disable_ipv6 +# in the ini so dovecot/postfix/nginx bind to IPv4 only. +# Uses network_mode:host so /proc/net/if_inet6 reflects the host's stack. +if [ ! -e /proc/net/if_inet6 ]; then + if grep -q '^disable_ipv6 = False' "$CHATMAIL_INI"; then + sed -i 's/^disable_ipv6 = False/disable_ipv6 = True/' "$CHATMAIL_INI" + echo "[INFO] IPv6 not available, set disable_ipv6 = True" + fi +fi + +# Inject external TLS paths from env var unless defined in chatmail.ini +if [ -n "${TLS_EXTERNAL_CERT_AND_KEY:-}" ]; then + if ! grep -q '^tls_external_cert_and_key' "$CHATMAIL_INI"; then + echo "tls_external_cert_and_key = $TLS_EXTERNAL_CERT_AND_KEY" >> "$CHATMAIL_INI" + fi +fi + +# Ensure mailboxes directory exists (chatmail-metadata needs it at startup, +# but Dovecot only creates it on first mail delivery) +mkdir -p "/home/vmail/mail/${MAIL_DOMAIN}" +chown vmail:vmail "/home/vmail/mail/${MAIL_DOMAIN}" + +# --- Deploy fingerprint: skip cmdeploy run if nothing changed --- +# On restart with identical image+config, systemd already brings up all +# enabled services only configure+activate are needed here. +IMAGE_VERSION_FILE="/etc/chatmail-image-version" +FINGERPRINT_FILE="/etc/chatmail/.deploy-fingerprint" +image_ver="none" +[ -f "$IMAGE_VERSION_FILE" ] && image_ver=$(cat "$IMAGE_VERSION_FILE") +config_hash=$(sha256sum "$CHATMAIL_INI" | cut -c1-16) +current_fp="${image_ver}:${config_hash}" + +# CMDEPLOY_STAGES non-empty in env = operator override -> always run. +# Otherwise, if fingerprint matches the last successful deploy, skip. +if [ -z "${CMDEPLOY_STAGES:-}" ] \ + && [ -f "$FINGERPRINT_FILE" ] \ + && [ "$(cat "$FINGERPRINT_FILE")" = "$current_fp" ]; then + echo "[INFO] No changes detected ($current_fp), skipping deploy." +else + export CMDEPLOY_STAGES="${CMDEPLOY_STAGES:-configure,activate}" + + # Skip DNS check when MAIL_DOMAIN is a bare IP address + SKIP_DNS="" + if [[ "$MAIL_DOMAIN" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]] || [[ "$MAIL_DOMAIN" =~ : ]]; then + SKIP_DNS="--skip-dns-check" + fi + $CMDEPLOY run --config "$CHATMAIL_INI" --ssh-host @local $SKIP_DNS + + # Restore the build-time hash + cp /etc/chatmail-image-version /etc/chatmail-version + echo "$current_fp" > "$FINGERPRINT_FILE" +fi + +# Signal success to Docker healthcheck +touch /run/chatmail-init.done + +# Forward journald to console so `docker compose logs` works +grep -q '^ForwardToConsole=yes' /etc/systemd/journald.conf \ + || echo "ForwardToConsole=yes" >> /etc/systemd/journald.conf +systemctl restart systemd-journald diff --git a/docker/chatmail_relay.dockerfile b/docker/chatmail_relay.dockerfile new file mode 100644 index 00000000..5cbc3eb3 --- /dev/null +++ b/docker/chatmail_relay.dockerfile @@ -0,0 +1,110 @@ +# syntax=docker/dockerfile:1 +FROM jrei/systemd-debian:12 AS base + +ENV LANG=en_US.UTF-8 + +RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ + --mount=type=cache,target=/var/lib/apt/lists,sharing=locked \ + 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 && \ + DEBIAN_FRONTEND=noninteractive TZ=UTC \ + apt-get install -y \ + ca-certificates \ + gcc \ + git \ + python3 \ + python3-dev \ + python3-venv \ + tzdata \ + locales && \ + sed -i -e "s/# $LANG.*/$LANG UTF-8/" /etc/locale.gen && \ + dpkg-reconfigure --frontend=noninteractive locales && \ + update-locale LANG=$LANG + +# --- Build-time: install cmdeploy venv and run install stage --- +# Editable install so importlib.resources reads directly from the source tree. +# On container start only "configure,activate" stages run. + +# Copy dependency metadata first so pip install layer is cached +COPY cmdeploy/pyproject.toml /opt/chatmail/cmdeploy/pyproject.toml +COPY chatmaild/pyproject.toml /opt/chatmail/chatmaild/pyproject.toml + +# Dummy scaffolding so editable install can discover packages +RUN mkdir -p /opt/chatmail/cmdeploy/src/cmdeploy \ + /opt/chatmail/chatmaild/src/chatmaild && \ + touch /opt/chatmail/cmdeploy/src/cmdeploy/__init__.py \ + /opt/chatmail/chatmaild/src/chatmaild/__init__.py + +# Dummy git repo: .git/ is excluded from the build context (.dockerignore) +# but setuptools calls `git ls-files` when building the sdist. +WORKDIR /opt/chatmail +RUN --mount=type=cache,target=/root/.cache/pip \ + git init -q && \ + python3 -m venv /opt/cmdeploy && \ + /opt/cmdeploy/bin/pip install -e chatmaild/ -e cmdeploy/ + +# Full source copy (editable install's .egg-link still points here) +COPY . /opt/chatmail/ + +# Minimal chatmail.ini +RUN printf '[params]\nmail_domain = build.local\n' > /tmp/chatmail.ini + +RUN CMDEPLOY_STAGES=install \ + CHATMAIL_INI=/tmp/chatmail.ini \ + CHATMAIL_NOSYSCTL=True \ + CHATMAIL_NOPORTCHECK=True \ + /opt/cmdeploy/bin/pyinfra @local \ + /opt/chatmail/cmdeploy/src/cmdeploy/run.py -y + +RUN cp -a www/ /opt/chatmail-www/ + +# Remove build-only packages — not needed at runtime. +# Keep git: test_deployed_state needs `git rev-parse HEAD` to verify the +# deployed version hash matches /etc/chatmail-version. +RUN apt-get purge -y gcc python3-dev && \ + apt-get autoremove -y && \ + rm -f /tmp/chatmail.ini + +# Record image version (used in deploy fingerprint at runtime). +# GIT_HASH is passed as a build arg (from docker-compose or CI) so that +# .git/ can be excluded from the build context via .dockerignore. +# Two files: chatmail-image-version is the immutable build hash (survives +# deploys); chatmail-version is overwritten by cmdeploy run and restored +# from the image version after each deploy in chatmail-init.sh. +ARG GIT_HASH=unknown +RUN echo "$GIT_HASH" > /etc/chatmail-image-version && \ + echo "$GIT_HASH" > /etc/chatmail-version + +# Mock git HEAD so `git rev-parse HEAD` returns the source repo's commit hash. +# The .git/ dir was created by `git init` earlier (for setuptools); we just +# write the build hash into whatever branch HEAD points to. +RUN head_ref=$(sed 's/^ref: //' /opt/chatmail/.git/HEAD) && \ + mkdir -p "/opt/chatmail/.git/$(dirname "$head_ref")" && \ + echo "$GIT_HASH" > "/opt/chatmail/.git/$head_ref" +# --- End build-time install --- + +ENV TZ=:/etc/localtime +ENV PATH="/opt/cmdeploy/bin:${PATH}" +RUN ln -s /etc/chatmail/chatmail.ini /opt/chatmail/chatmail.ini + +ARG CHATMAIL_INIT_SERVICE_PATH=/lib/systemd/system/chatmail-init.service +COPY ./docker/chatmail-init.service "$CHATMAIL_INIT_SERVICE_PATH" +RUN ln -sf "$CHATMAIL_INIT_SERVICE_PATH" "/etc/systemd/system/multi-user.target.wants/chatmail-init.service" + +# Remove default nginx site config at build time (not in entrypoint) +RUN rm -f /etc/nginx/sites-enabled/default + +COPY --chmod=555 ./docker/chatmail-init.sh /chatmail-init.sh +COPY --chmod=555 ./docker/entrypoint.sh /entrypoint.sh +COPY --chmod=555 ./docker/healthcheck.sh /healthcheck.sh + +HEALTHCHECK --interval=10s --start-period=180s --timeout=10s --retries=3 \ + CMD /healthcheck.sh + +STOPSIGNAL SIGRTMIN+3 + +ENTRYPOINT ["/entrypoint.sh"] + +CMD [ "--default-standard-output=journal+console", \ + "--default-standard-error=journal+console" ] diff --git a/docker/docker-compose.ci.yaml b/docker/docker-compose.ci.yaml new file mode 100644 index 00000000..760ad451 --- /dev/null +++ b/docker/docker-compose.ci.yaml @@ -0,0 +1,11 @@ +# Used by .github/workflows/docker-ci.yaml +# The GHCR image is set via CHATMAIL_IMAGE env var at deploy time. +services: + chatmail: + image: ${CHATMAIL_IMAGE:-chatmail-relay:latest} + volumes: + - /srv/chatmail/chatmail.ini:/etc/chatmail/chatmail.ini + - /srv/chatmail/dkim:/etc/dkimkeys + - /srv/chatmail/certs:/var/lib/acme + environment: + TLS_EXTERNAL_CERT_AND_KEY: /var/lib/acme/live/${MAIL_DOMAIN}/fullchain /var/lib/acme/live/${MAIL_DOMAIN}/privkey diff --git a/docker/docker-compose.override.yaml.example b/docker/docker-compose.override.yaml.example new file mode 100644 index 00000000..c7ad5369 --- /dev/null +++ b/docker/docker-compose.override.yaml.example @@ -0,0 +1,44 @@ +# Local overrides: copy to docker-compose.override.yaml in this directory. +# Compose automatically merges this with docker-compose.yaml. +# +# cp docker-compose.override.yaml.example docker-compose.override.yaml +# +# Volumes are APPENDED to the base file's volumes list, environment and other scalar keys are MERGED by key. +services: + chatmail: + volumes: + ## Data paths — bind-mount to host directories for easy access/backup. + + # - ./data/dkim:/etc/dkimkeys + # - ./data/certs:/var/lib/acme + + # - ./data/mail:/home/vmail + ## Or mount from an existing bare-metal install. + # - /home/vmail:/home/vmail + + ## Mount your own chatmail.ini (skips auto-generation): + # - ./chatmail.ini:/etc/chatmail/chatmail.ini + + ## Custom website: + # - ./custom/www:/opt/chatmail-www + + ## Debug — mount scripts for live editing: + # - ./chatmail-init.sh:/chatmail-init.sh + # - ./entrypoint.sh:/entrypoint.sh + + # environment: + ## Mount certs (above) and set TLS_EXTERNAL_CERT_AND_KEY to in-container paths. + ## A tls-cert-reload.path watcher inside the container reloads services + ## when the cert file changes. However, inotify does not cross bind-mount + ## boundaries, so host-side renewals (certbot, acmetool, etc.) must + ## notify the container explicitly. Add this to your renewal hook: + ## + ## docker exec chatmail systemctl start tls-cert-reload.service + ## + ## Host acmetool (bare-metal migration): create mount above, and + ## rsync -a /var/lib/acme/live data/certs + # TLS_EXTERNAL_CERT_AND_KEY: "/var/lib/acme/live/${MAIL_DOMAIN}/fullchain /var/lib/acme/live/${MAIL_DOMAIN}/privkey" + ## + ## (Untested) Traefik certs-dumper (see docker/docker-compose-traefik.yaml) - also add volume: + ## - traefik-certs:/certs:ro + # TLS_EXTERNAL_CERT_AND_KEY: "/certs/${MAIL_DOMAIN}/certificate.crt /certs/${MAIL_DOMAIN}/privatekey.key" diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml new file mode 100644 index 00000000..8ec99e68 --- /dev/null +++ b/docker/docker-compose.yaml @@ -0,0 +1,48 @@ +# Base compose file — do not edit. Put customizations (data paths, extra +# volumes, env overrides) in docker-compose.override.yaml instead. +# See docker-compose.override.yaml.example in this directory for a starting point. +# +# Security notes: this container uses +# - network_mode:host chatmail needs many ports (25, 53, 80, 143, 443, 465, +# 587, 993, 3340, 8443) and needs to operate from the real IP, which bridging +# would make tricky +# - cgroup:host (required for systemd). +# Together these give the container near-host-level access. This is acceptable +# for a dedicated mail server, but be aware that the container can bind any +# port and see all host network traffic. + +services: + chatmail: + build: + context: ../ + dockerfile: docker/chatmail_relay.dockerfile + args: + GIT_HASH: ${GIT_HASH:-unknown} + image: chatmail-relay:latest + restart: unless-stopped + container_name: chatmail + # Required for systemd — use only one of the following: + cgroup: host # compose v2 + # privileged: true # compose v1 (less restricted) + tty: true # required for logs + tmpfs: # required for systemd + - /tmp + - /run + - /run/lock + logging: + driver: none + environment: + MAIL_DOMAIN: $MAIL_DOMAIN + network_mode: "host" + volumes: + ## system (required) + - /sys/fs/cgroup:/sys/fs/cgroup:rw + ## data (defaults — override in docker-compose.override.yaml) + - mail:/home/vmail + - dkim:/etc/dkimkeys + - certs:/var/lib/acme + +volumes: + mail: + dkim: + certs: diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh new file mode 100755 index 00000000..3e0e348a --- /dev/null +++ b/docker/entrypoint.sh @@ -0,0 +1,9 @@ +#!/bin/bash +set -eo pipefail + +CHATMAIL_INIT_SERVICE_PATH="${CHATMAIL_INIT_SERVICE_PATH:-/lib/systemd/system/chatmail-init.service}" + +env_vars="MAIL_DOMAIN CMDEPLOY_STAGES CHATMAIL_INI TLS_EXTERNAL_CERT_AND_KEY PATH" +sed -i "s||$env_vars|g" "$CHATMAIL_INIT_SERVICE_PATH" + +exec /lib/systemd/systemd "$@" diff --git a/docker/env.example b/docker/env.example new file mode 100644 index 00000000..3eebb373 --- /dev/null +++ b/docker/env.example @@ -0,0 +1 @@ +MAIL_DOMAIN=chat.example.com diff --git a/docker/healthcheck.sh b/docker/healthcheck.sh new file mode 100644 index 00000000..88d9f806 --- /dev/null +++ b/docker/healthcheck.sh @@ -0,0 +1,16 @@ +#!/bin/bash +# returns 0 when chatmail-init succeeded and all expected services are running. + +set -e + +test -f /run/chatmail-init.done + +# Core services +services="chatmail-metadata doveauth dovecot filtermail filtermail-incoming nginx postfix unbound" + +# Optional services +for svc in iroh-relay turnserver; do + systemctl is-enabled "$svc" 2>/dev/null && services="$services $svc" +done + +exec systemctl is-active $services