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/.github/workflows/docker-build.yaml b/.github/workflows/docker-build.yaml new file mode 100644 index 00000000..6af8f085 --- /dev/null +++ b/.github/workflows/docker-build.yaml @@ -0,0 +1,76 @@ +name: Docker Build + +on: + pull_request: + paths: + - 'docker/**' + - 'docker-compose.yaml' + - '.dockerignore' + - 'chatmaild/**' + - 'cmdeploy/**' + - '.github/workflows/docker-build.yaml' + push: + branches: + - main + - j4n/docker + paths: + - 'docker/**' + - 'docker-compose.yaml' + - '.dockerignore' + - 'chatmaild/**' + - 'cmdeploy/**' + - '.github/workflows/docker-build.yaml' + tags: + - 'v*' + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + build: + name: Build Docker image + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to GHCR + if: github.event_name == 'push' + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata (tags, labels) + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + # Tagged releases: v1.2.3 → :1.2.3, :1.2, :latest + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + # Branch pushes: j4n/docker → :j4n-docker + type=ref,event=branch + # Always: :sha- + type=sha + + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: . + file: docker/chatmail_relay.dockerfile + push: ${{ github.event_name == 'push' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + build-args: | + GIT_HASH=${{ github.sha }} diff --git a/.gitignore b/.gitignore index 6e1054d0..3d197fa5 100644 --- a/.gitignore +++ b/.gitignore @@ -164,3 +164,9 @@ cython_debug/ #.idea/ chatmail.zone + +# docker +/data/ +/custom/ +docker-compose.override.yaml +.env diff --git a/doc/source/docker.rst b/doc/source/docker.rst new file mode 100644 index 00000000..91009ff5 --- /dev/null +++ b/doc/source/docker.rst @@ -0,0 +1,262 @@ +Docker installation +=================== + +This section provides instructions for installing a chatmail relay +using Docker Compose. + +.. note:: + + Docker support is experimental and not yet covered by automated tests, please report bugs. + + +Known limitations +----------------- + +- Requires cgroups v2 on the host. Operation with cgroups v1 has not been tested. +- This preliminary image simply wraps the cmdeploy process detailed in the :doc:`getting_started` instructions in a full Debian-systemd image. +- Currently, the image has only been tested and built on amd64, though arm64 should theoretically work as well. + + +Prerequisites +------------- + +- **Docker Compose v2** (``docker compose``, not ``docker-compose``) is + required for its ``cgroup: host`` support (`Install instructions `_:) + +- **DNS records** for your domain (see step 1 below). + +- **Kernel parameters** — ``fs.inotify.max_user_instances`` and + ``fs.inotify.max_user_watches`` must be raised on the host because they + cannot be changed inside the container (see step 2 below). + + +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.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. + +2. 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, e.g., `/srv/chatmail-relay`:: + + mkdir -p /srv/chatmail-relay && cd /srv/chatmail-relay + wget https://raw.githubusercontent.com/chatmail/relay/refs/heads/main/docker-compose.yaml https://raw.githubusercontent.com/chatmail/relay/refs/heads/main/docker-compose.override.yaml.example + wget https://raw.githubusercontent.com/chatmail/relay/refs/heads/main/docker/env.example -O .env + + +- or clone the chatmail repo :: + + git clone https://github.com/chatmail/relay + cd relay + cp example.env .env + + + +Customize and start +^^^^^^^^^^^^^^^^^^^ + +1. 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. Copy + the example to get started:: + + cp docker/docker-compose.override.yaml.example docker-compose.override.yaml + # and edit docker-compose.override.yaml + + +2. Configure the ``.env`` file. Only ``MAIL_DOMAIN`` is required, the domain + name of the future server. + + 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). + +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. + + +Managing the server +------------------- + +Use ``docker exec`` to run cmdeploy commands inside the container:: + + # Show required DNS records + docker exec chatmail /opt/cmdeploy/bin/cmdeploy dns --ssh-host @local + + # Check server status + docker exec chatmail /opt/cmdeploy/bin/cmdeploy status --ssh-host @local + + # Run benchmarks (can also run from any machine with cmdeploy installed) + docker exec chatmail /opt/cmdeploy/bin/cmdeploy bench chat.example.org + + +Customization +------------- + +Custom 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 +^^^^^^^^^^^^^^^^^^^ + +There are two configuration modes: + +**Simple (default):** Set ``MAIL_DOMAIN`` in ``.env``. The container +auto-generates ``chatmail.ini`` with defaults on first start. This is +sufficient for most deployments. + +**Advanced:** Generate a ``chatmail.ini``, edit it, and mount it into +the container. This gives you full control over all chatmail settings. + +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 + + +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/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, 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 compose build chatmail + +The build bakes all binaries, Python packages, and the install stage +into the image. After building, only ``docker-compose.yaml`` and ``.env`` +are needed to run the container. + +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 69b019d7..19f2c7ff 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-compose.override.yaml.example b/docker-compose.override.yaml.example new file mode 100644 index 00000000..6503dc3b --- /dev/null +++ b/docker-compose.override.yaml.example @@ -0,0 +1,33 @@ +# Local overrides — copy to docker-compose.override.yaml in the repo root. +# Compose automatically merges this with docker-compose.yaml. +# +# cp docker/docker-compose.override.yaml.example docker-compose.override.yaml +# +# Volumes listed here are APPENDED to the base file's volumes. +# Scalar values (environment, image, etc.) are REPLACED. +services: + chatmail: + volumes: + ## Data paths — bind-mount to host directories for easy access/backup. + ## Uncomment and adjust paths as needed. These override the named + ## volumes in the base docker-compose.yaml. + # - ./data/chatmail:/home/vmail + # - ./data/chatmail-dkimkeys:/etc/dkimkeys + # - ./data/chatmail-acme:/var/lib/acme + + ## Or mount data from an existing bare-metal install. + ## Note: DKIM key ownership is fixed automatically on startup + ## (the host's opendkim UID may differ from the container's). + # - /home/vmail:/home/vmail + # - /etc/dkimkeys:/etc/dkimkeys + # - /var/lib/acme:/var/lib/acme + + ## Mount your own chatmail.ini (skips auto-generation): + # - ./chatmail.ini:/etc/chatmail/chatmail.ini + + ## Custom website: + # - ./custom/www:/opt/chatmail-www + + ## Debug — mount scripts from the repo for live editing: + # - ./docker/files/setup_chatmail_docker.sh:/setup_chatmail_docker.sh + # - ./docker/files/entrypoint.sh:/entrypoint.sh diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 00000000..fc3554ea --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,51 @@ +# Base compose file — do not edit. Put customizations (data paths, extra +# volumes, env overrides) in docker-compose.override.yaml instead. +# See docker/docker-compose.override.yaml.example for a starting point. +# +# Security note: this container uses network_mode:host (chatmail needs many +# ports: 25, 53, 80, 143, 443, 465, 587, 993, 3340, 8443) and 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 only + # privileged: true # compose v1 (not tested) + tty: true # required for logs + tmpfs: # required for systemd + - /tmp + - /run + - /run/lock + logging: + driver: json-file + options: + max-size: "10m" + max-file: "3" + environment: + MAIL_DOMAIN: $MAIL_DOMAIN + CMDEPLOY_STAGES: ${CMDEPLOY_STAGES:-} + CHATMAIL_NOSYSCTL: ${CHATMAIL_NOSYSCTL:-True} + CHATMAIL_NOPORTCHECK: ${CHATMAIL_NOPORTCHECK:-True} + CHATMAIL_NOACME: ${CHATMAIL_NOACME:-} + network_mode: "host" + volumes: + ## system (required) + - /sys/fs/cgroup:/sys/fs/cgroup:rw + ## data (defaults — override in docker-compose.override.yaml) + - chatmail-data:/home/vmail + - chatmail-dkimkeys:/etc/dkimkeys + - chatmail-acme:/var/lib/acme + +volumes: + chatmail-data: + chatmail-dkimkeys: + chatmail-acme: diff --git a/docker/build.sh b/docker/build.sh new file mode 100755 index 00000000..85369368 --- /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 --short HEAD) +exec docker compose build "$@" diff --git a/docker/chatmail_relay.dockerfile b/docker/chatmail_relay.dockerfile new file mode 100644 index 00000000..f54d76db --- /dev/null +++ b/docker/chatmail_relay.dockerfile @@ -0,0 +1,105 @@ +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=UTC \ + apt-get install -y tzdata && \ + apt-get install -y locales && \ + sed -i -e "s/# $LANG.*/$LANG UTF-8/" /etc/locale.gen && \ + dpkg-reconfigure --frontend=noninteractive locales && \ + update-locale LANG=$LANG \ + && rm -rf /var/lib/apt/lists/* + +RUN apt-get update && \ + apt-get install -y \ + git \ + python3 \ + python3-venv \ + python3-virtualenv \ + gcc \ + python3-dev \ + opendkim \ + opendkim-tools \ + curl \ + rsync \ + unbound \ + unbound-anchor \ + dnsutils \ + postfix \ + acl \ + nginx \ + libnginx-mod-stream \ + fcgiwrap \ + cron \ + && rm -rf /var/lib/apt/lists/* + +# --- 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 . /opt/chatmail/ +WORKDIR /opt/chatmail + +RUN printf '[params]\nmail_domain = build.local\n' > /tmp/chatmail.ini + +# Dummy git repo init: .git/ is excluded from the build context (.dockerignore) +# but setuptools calls `git ls-files` when building the sdist. +RUN git init -q && \ + python3 -m venv /opt/cmdeploy && \ + /opt/cmdeploy/bin/pip install --no-cache-dir \ + -e chatmaild/ -e cmdeploy/ + +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/ + +RUN 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. +ARG GIT_HASH=unknown +RUN echo "$GIT_HASH" > /etc/chatmail-image-version && \ + echo "$GIT_HASH" > /etc/chatmail-version +# --- End build-time install --- + +ENV CHATMAIL_INI=/etc/chatmail/chatmail.ini +ENV PATH="/opt/cmdeploy/bin:${PATH}" +RUN ln -s /etc/chatmail/chatmail.ini /opt/chatmail/chatmail.ini + +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" + +# Remove default nginx site config at build time (not in entrypoint) +RUN rm -f /etc/nginx/sites-enabled/default + +COPY --chmod=555 ./docker/files/setup_chatmail_docker.sh /setup_chatmail_docker.sh +COPY --chmod=555 ./docker/files/entrypoint.sh /entrypoint.sh + +# Certificate monitoring as a proper systemd timer (not a background process) +COPY --chmod=555 ./docker/files/chatmail-certmon.sh /chatmail-certmon.sh +COPY ./docker/files/chatmail-certmon.service /lib/systemd/system/chatmail-certmon.service +COPY ./docker/files/chatmail-certmon.timer /lib/systemd/system/chatmail-certmon.timer +RUN ln -sf /lib/systemd/system/chatmail-certmon.timer /etc/systemd/system/timers.target.wants/chatmail-certmon.timer + +HEALTHCHECK --interval=60s --timeout=10s --retries=3 \ + CMD systemctl is-active dovecot postfix nginx unbound opendkim filtermail doveauth chatmail-metadata || exit 1 + +STOPSIGNAL SIGRTMIN+3 + +ENTRYPOINT ["/entrypoint.sh"] + +CMD [ "--default-standard-output=journal+console", \ + "--default-standard-error=journal+console" ] + diff --git a/docker/files/chatmail-certmon.service b/docker/files/chatmail-certmon.service new file mode 100644 index 00000000..f89b950f --- /dev/null +++ b/docker/files/chatmail-certmon.service @@ -0,0 +1,8 @@ +[Unit] +Description=Check TLS certificate changes and reload services +After=setup_chatmail.service + +[Service] +Type=oneshot +ExecStart=/bin/bash /chatmail-certmon.sh +PassEnvironment=MAIL_DOMAIN PATH_TO_SSL diff --git a/docker/files/chatmail-certmon.sh b/docker/files/chatmail-certmon.sh new file mode 100644 index 00000000..107c169d --- /dev/null +++ b/docker/files/chatmail-certmon.sh @@ -0,0 +1,28 @@ +#!/bin/bash +# Check if TLS certificates have changed and reload services if so. +# Called by chatmail-certmon.timer (systemd timer, default every 60s). +set -eo pipefail + +PATH_TO_SSL="${PATH_TO_SSL:-/var/lib/acme/live/${MAIL_DOMAIN}}" +HASH_FILE="/run/chatmail-certmon.hash" + +if [ ! -d "$PATH_TO_SSL" ]; then + exit 0 +fi + +current_hash=$(find "$PATH_TO_SSL" -type f -exec sha1sum {} \; | sort | sha1sum | awk '{print $1}') +previous_hash="" +if [ -f "$HASH_FILE" ]; then + previous_hash=$(cat "$HASH_FILE") +fi + +if [ -n "$current_hash" ] && [ "$current_hash" != "$previous_hash" ]; then + echo "[INFO] Certificate hash changed, reloading nginx, dovecot and postfix." + echo "$current_hash" > "$HASH_FILE" + # On first run (no previous hash), don't reload — services may not be up yet + if [ -n "$previous_hash" ]; then + systemctl reload nginx.service + systemctl reload dovecot.service + systemctl reload postfix.service + fi +fi diff --git a/docker/files/chatmail-certmon.timer b/docker/files/chatmail-certmon.timer new file mode 100644 index 00000000..8dc5aa8d --- /dev/null +++ b/docker/files/chatmail-certmon.timer @@ -0,0 +1,9 @@ +[Unit] +Description=Periodically check TLS certificate changes + +[Timer] +OnBootSec=120 +OnUnitActiveSec=60 + +[Install] +WantedBy=timers.target diff --git a/docker/files/entrypoint.sh b/docker/files/entrypoint.sh new file mode 100755 index 00000000..f6a3fb1f --- /dev/null +++ b/docker/files/entrypoint.sh @@ -0,0 +1,12 @@ +#!/bin/bash +set -eo pipefail + +SETUP_CHATMAIL_SERVICE_PATH="${SETUP_CHATMAIL_SERVICE_PATH:-/lib/systemd/system/setup_chatmail.service}" + +# Whitelist only the env vars needed by setup_chatmail_docker.sh. +# Forwarding all env vars (via printenv) would leak Docker internals, +# orchestrator secrets, and other unrelated variables into systemd. +env_vars="MAIL_DOMAIN CMDEPLOY_STAGES CHATMAIL_INI CHATMAIL_NOSYSCTL CHATMAIL_NOPORTCHECK CHATMAIL_NOACME PATH_TO_SSL PATH" +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 00000000..2a0a48bc --- /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 00000000..a1f9c82a --- /dev/null +++ b/docker/files/setup_chatmail_docker.sh @@ -0,0 +1,54 @@ +#!/bin/bash + +set -euo pipefail +export CHATMAIL_INI="${CHATMAIL_INI:-/etc/chatmail/chatmail.ini}" + +CMDEPLOY=/opt/cmdeploy/bin/cmdeploy + +if [ -z "$MAIL_DOMAIN" ]; then + echo "ERROR: Environment variable 'MAIL_DOMAIN' must be set!" >&2 + exit 1 +fi + +### MAIN + +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 + +# Journald: forward to console for docker logs +grep -q '^ForwardToConsole=yes' /etc/systemd/journald.conf \ + || echo "ForwardToConsole=yes" >> /etc/systemd/journald.conf +systemctl restart systemd-journald + +# Create chatmail.ini (skips if file already exists, e.g. volume-mounted) +mkdir -p "$(dirname "$CHATMAIL_INI")" +if [ ! -f "$CHATMAIL_INI" ]; then + $CMDEPLOY init --config "$CHATMAIL_INI" "$MAIL_DOMAIN" +fi + +# --- Deploy fingerprint: skip cmdeploy run if nothing changed --- +# On restart with identical image+config, systemd already brings up all +# enabled services — the full cmdeploy run is redundant (~30s saved). +# The install stage runs at image build time (Dockerfile), so 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}" + $CMDEPLOY run --config "$CHATMAIL_INI" --ssh-host @local + echo "$current_fp" > "$FINGERPRINT_FILE" +fi diff --git a/env.example b/env.example new file mode 100644 index 00000000..ba62c93c --- /dev/null +++ b/env.example @@ -0,0 +1,7 @@ +MAIL_DOMAIN="chat.example.com" + +# CMDEPLOY_STAGES - default: "configure,activate". Set to "install,configure,activate" to force full reinstall. +# CMDEPLOY_STAGES="configure,activate" + +# Skip acmetool when using an external certificate manager (e.g. Traefik, Caddy). +# CHATMAIL_NOACME="True"