Compare commits

..

66 Commits

Author SHA1 Message Date
j4n
606f36ee13 docker: integrate documentation
delete original markdown notes (the russian version was severely
outdated) and add a new section.
2026-02-18 17:05:28 +01:00
j4n
72973631f7 docker: move key files to repo root as per convention 2026-02-18 17:05:28 +01:00
j4n
5b5b09dc2e filtermail wait dont know where this came from 2026-02-18 17:05:28 +01:00
j4n
aa2f41158f docker: move all relevant files to repository root as per convention 2026-02-18 17:05:28 +01:00
j4n
e0ca4b25f4 docker/chatmaild/config: default to false on CHATMAIL_NOACME 2026-02-18 17:05:28 +01:00
j4n
0b593f98bf docker: chatmail.ini.f empty line remove 2026-02-18 17:05:28 +01:00
j4n
2e23fadb54 docker: skip redundant cmdeploy run on container restart
Replace the old IMAGE_VERSION_FILE/RUNNING_VERSION_FILE mechanism with a
single deploy fingerprint (image_version:sha256(chatmail.ini)) stored at
/etc/chatmail/.deploy-fingerprint. On restart, if the fingerprint matches
the last successful deploy, skip cmdeploy run entirely. The fingerprint
lives on the container's writable layer. On fresh containers, setting
CMDEPLOY_STAGES non-empty in env forces a deploy run regardless of
fingerprint.

Also narrow the /home volume mount to /home/vmail.
2026-02-18 17:05:28 +01:00
j4n
bc19966801 docker: run a dummy git init to make cmdeploy tooling happy 2026-02-18 17:05:28 +01:00
j4n
bafbaa1b81 docker: fix DKIM key permission denied on bind-mounted volumes
chown the entire /etc/acmekeys directory
2026-02-18 17:05:28 +01:00
j4n
feecf6affd docker: add build.sh to set GIT_HASH for local builds
Simple wrapper as .git is excluded from build context.
2026-02-18 17:05:28 +01:00
j4n
c2c3be1115 docker: add DKIM/ACME mount examples for bare-metal migration 2026-02-18 17:05:28 +01:00
j4n
c6d6e272be docker: pass CHATMAIL_NOSYSCTL and CHATMAIL_NOPORTCHECK to container
These got lost somehow
2026-02-18 17:05:28 +01:00
j4n
425e3db07a docker: slim build by excluding .git and non-essential files
Replace the in-Dockerfile `git rev-parse HEAD` with a GIT_HASH build arg
passed from docker-compose (local) or github.sha (CI), defaulting to
"unknown" when unset.

Also exclude .github/, docs/, tests/, and *.md (except www/**/*.md).
2026-02-18 17:05:28 +01:00
j4n
c22efeb74b docker: use docker-compose.override.yaml for user customizations
The base docker-compose.yaml was checked into git and thus would get
overwritten on pull.
- docker-compose.yaml uses named volumes as safe defaults
- docker-compose.override.yaml (gitignored) holds user customizations
- Compose automatically merges both files
2026-02-18 17:05:28 +01:00
j4n
71bd0da51a docs: document ghcr.io built images
Added pull instructions for pre-built images from ghcr.io (main branch,
tagged releases, feature branches).
2026-02-18 17:05:28 +01:00
j4n
0ed5ec75fb docker: add GitHub Action to build Docker image
Builds the Docker image on PRs and pushes that touch docker/, compose,
chatmaild/, or cmdeploy/ files.
- PRs: build only (no push, no login)
- Branch push (main, j4n/docker): build + push as :main or :j4n-docker
- Tagged release (v*): build + push as :1.2.3, :1.2, :sha-<hash>
Uses GITHUB_TOKEN for ghcr.io auth.
2026-02-18 17:05:28 +01:00
j4n
4fd0429cd3 docker: add Traefik support
USE_FOREIGN_CERT_MANAGER existed in compose/example.env but was never
read by any code. This wires it up end-to-end based on PR 662.

- Preliminarily add config options for this, and skip AcmetoolDeployer if
set.
- Add Traefik integration in docker/docker-compose-traefik.yaml, with
  traefik-certs-dumper
- post-hook.sh creates fullchain/privkey symlinks for chatmail
- Chatmail container uses ports 25/143/465/587/993 directly, Traefik
  handles 80/443
- docker/traefik/ contains config.yaml and dynamic configs
- docker/example-traefik.env for the Traefik setup
- rename USE_FOREIGN_CERT_MANAGER to CHATMAIL_NOACME
2026-02-18 17:05:28 +01:00
j4n
45717de6cb docker: add set -u to setup_chatmail_docker.sh 2026-02-18 17:05:28 +01:00
j4n
77dc67dde9 docker: auto-detect image upgrades and include install stage
Without version tracking, if a new image requires the install stage
(e.g. new package versions), the default configure,activate will skip
it and potentially fail silently.

At build time, the git hash is written to /etc/chatmail-image-version.
At runtime, setup_chatmail_docker.sh compares it against the persisted
/home/.chatmail-running-version (survives container restarts via the
/home volume). If they differ, the install stage is automatically
prepended to CMDEPLOY_STAGES. After a successful deploy, the running
version is updated.

Files: docker/chatmail_relay.dockerfile:68-69, docker/files/setup_chatmail_docker.sh:27-48
2026-02-18 17:05:28 +01:00
j4n
f017f88901 docker: docs: replace outdated Russian Docker docs with redirect to EN 2026-02-18 17:05:28 +01:00
j4n
0585314468 docker: extract cert monitor from background process to systemd timer
The cert monitoring was an orphaned background process (`monitor_certificates &`)
Replace with a proper systemd timer/service (every 60s).
Also made journald ForwardToConsole=yes idempotent.
2026-02-18 17:05:28 +01:00
j4n
85ee7dbeb5 docker: document security implications of host networking + cgroups 2026-02-18 17:05:28 +01:00
j4n
e503e120e5 docker: add HEALTHCHECK, remove VOLUME, fix Dockerfile hygiene
- Added HEALTHCHECK that verifies chatmail services are active via systemctl
- Removed `VOLUME ["/sys/fs/cgroup", "/home"]` as anonymous volumes are
  an anti-pattern for user data (leads to data loss on upgrades). Let
  compose/`docker run -v` handle volume management.
- Changed TZ from Europe/London to UTC (server best practice)
- Removed duplicate WORKDIR /opt/chatmail
- Moved `unlink /etc/nginx/sites-enabled/default` from entrypoint.sh to
  Dockerfile build time
2026-02-18 17:05:28 +01:00
j4n
475975dfa0 docker: use @local instead of @docker inside container 2026-02-18 17:05:28 +01:00
j4n
a930f8f46b docker: whitelist env vars in entrypoint, quote $@ and paths
Instead of forwarding ALL environment variables into systemd's
PassEnvironment, only forward a whitelist of variables to prevent
leaking of environment variables.
2026-02-18 17:05:28 +01:00
j4n
75ef0f2698 docker: remove duplicated dovecot hashes from Dockerfile 2026-02-18 17:05:28 +01:00
j4n
57f9327d4d docker: fix cert monitoring — wait for certs dir, use return not exit
Fix bugs in certificate monitoring function:
- `exit 0` inside monitor_certificates() would kill the background process
- calculate_hash() now checks dir existence instead of silenty dying
- Added wait loop until $PATH_TO_SSL exists before monitoring

Files: docker/files/setup_chatmail_docker.sh:16-41
2026-02-18 17:05:28 +01:00
j4n
e99d979eb8 docker: update docs to use @local for cmdeploy 2026-02-18 17:05:28 +01:00
j4n
ffa45c1ca1 docker: symlink chatmail.ini into /opt/chatmail for bench/tests 2026-02-18 17:05:28 +01:00
j4n
9f6de19121 fix(cmdeploy): add __call__ to LocalExec so status works with @local 2026-02-18 17:05:28 +01:00
j4n
cc779ec04f feat(cmdeploy): read CHATMAIL_INI env var for default --config path
Avoids needing --config /etc/chatmail/chatmail.ini on every command
inside the Docker container, where CHATMAIL_INI is already set.
2026-02-18 17:05:28 +01:00
j4n
04bd38baaa docker: add quickstart docker send note 2026-02-18 17:05:28 +01:00
j4n
4df6a96a14 docker: keep .git in build context for GithashDeployer 2026-02-18 17:05:28 +01:00
j4n
47131533df docker: set CHATMAIL_NOPORTCHECK during build-time install 2026-02-18 17:05:28 +01:00
j4n
a84c02e1e5 docker: replace config flags with env vars, drop docker param from deploy_chatmail
Remove change_kernel_settings/fs_inotify_max_user_instances_and_watchers
from chatmail.ini — use CHATMAIL_NOSYSCTL and CHATMAIL_NOPORTCHECK env
vars instead. deploy_chatmail() no longer takes a docker flag; deployers
check the env directly.
2026-02-18 17:05:28 +01:00
j4n
0edff3205f docker: remove dead utilities, fix cmdeploy run using wrong config path
Remove no longer needed docker/files/update_ini.sh and docker/cm_ini_to_env.py.
Fix cmdeploy run not receiving --config.
Document env files
2026-02-18 17:05:28 +01:00
j4n
a48552d69e docker: drop env to ini translation, use chatmail.ini directly
Remove update_ini.sh and the env-var-to-ini pipeline. The container now
has two config modes:

- Simple: set MAIL_DOMAIN in .env, container generates chatmail.ini
  with defaults via `cmdeploy init` on first start.
- Advanced: mount a custom chatmail.ini into the container; the init
  step is skipped when the file already exists.

This eliminates the fragile FORCE_REINIT_INI_FILE / INI_CMD_ARGS
machinery and the env vars that duplicated chatmail.ini settings

Also add *.ini and .env to .dockerignore so local config files
don't leak into the image.
2026-02-18 17:05:28 +01:00
j4n
0c746553b3 docker: move cmdeploy into docker image 2026-02-18 17:05:28 +01:00
j4n
ce65866595 docker: make compose work with cgroups (v2), conversion scripts/docs 2026-02-18 17:05:28 +01:00
j4n
557ad2ed3c docker: don't overwrite existing DKIM keys on container start
opendkim-genkey was running unconditionally on every startup,
check if file exists and skip.
2026-02-18 17:05:28 +01:00
j4n
87b1680621 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.
2026-02-18 17:05:28 +01:00
j4n
872fd2d846 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.
2026-02-18 17:05:28 +01:00
j4n
fa2827a07e feat(cmdeploy): guard against non-running systemd
This enables docker image building without systemd running, which would
make pyinfra SystemdEnabled fail.
2026-02-18 17:05:28 +01:00
j4n
c68df8551c docker: remove echobot parts that were lingering in the feature branch 2026-02-18 17:05:28 +01:00
Keonik1
23ddd087ad cmdeploy: Add config parameters change_kernel_settings and fs_inotify_max_user_instances_and_watchers 2026-02-18 17:05:28 +01:00
missytake
4278799f51 cmdeploy: add config (, ) 2026-02-18 17:05:28 +01:00
missytake
ec26ac5dbf docker: use --network=host so chatmail-turn can use any port 2026-02-18 17:05:28 +01:00
missytake
ee4648967e docker: open ports for TURN + STUN 2026-02-18 17:05:28 +01:00
missytake
92c8b83a5e docker: move all configuration to example.env 2026-02-18 17:05:28 +01:00
missytake
c33b5ade30 doc: fix linebreak 2026-02-18 17:05:28 +01:00
missytake
09c0af2c99 docker: disable port check if docker is running. fix #694 2026-02-18 17:05:28 +01:00
missytake
8d76b28a59 Suggestions from @Keonik1
Co-authored-by: Keonik <57857901+Keonik1@users.noreply.github.com>
2026-02-18 17:05:28 +01:00
missytake
ed9c7631bc docker: enable DNS checks before cmdeploy run again 2026-02-18 17:05:28 +01:00
Keonik1
9c0a3a1718 fix unlink if default nginx conf is not exist
- https://github.com/chatmail/relay/pull/614#discussion_r2297828830
2026-02-18 17:05:28 +01:00
Keonik1
bb590bb5ae Fix issue with acmetool
- https://github.com/chatmail/relay/pull/614#discussion_r2279630626
2026-02-18 17:05:28 +01:00
Keonik1
e1c0bffa52 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
2026-02-18 17:05:28 +01:00
Keonik1
e272bb9069 fix docs - nginx "restart" to "reload"
https://github.com/chatmail/relay/pull/614#discussion_r2269896158
2026-02-18 17:05:27 +01:00
Keonik1
87bd0323c2 Fix bug with attaching certs 2026-02-18 17:05:27 +01:00
Keonik1
d2f169af0d pass values to MAIL_DOMAIN and ACME_EMAIL from vars for docker-compose-default
https://github.com/chatmail/relay/pull/614#discussion_r2279591922
2026-02-18 17:05:27 +01:00
Keonik1
0603be8cff change "restart nginx" to "reload nginx"
https://github.com/chatmail/relay/pull/614#discussion_r2269896158
2026-02-18 17:05:27 +01:00
Keonik1
5b66fb9ade add RECREATE_VENV var
https://github.com/chatmail/relay/pull/614#discussion_r2279742769
2026-02-18 17:05:27 +01:00
Keonik1
7f151b368b add 465 port
https://github.com/chatmail/relay/pull/614#discussion_r2279707059
2026-02-18 17:05:27 +01:00
Keonik1
59362b4cf9 add port 80 to docker-compose-default
https://github.com/chatmail/relay/pull/614#discussion_r2279656441
2026-02-18 17:05:27 +01:00
Keonik1
f8af0e2c33 rename dockerfile
https://github.com/chatmail/relay/pull/614#discussion_r2270031966
2026-02-18 17:05:27 +01:00
Keonik1
beef0ecb19 Add installation via docker compose (MVP 1) 2026-02-18 17:05:27 +01:00
Mark Felder
36eb63faa1 feat: Strip Received headers before delivery 2026-02-17 21:16:11 +01:00
35 changed files with 760 additions and 700 deletions

View File

@@ -1,7 +1,18 @@
.git
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

76
.github/workflows/docker-build.yaml vendored Normal file
View File

@@ -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-<hash>
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 }}

1
.gitignore vendored
View File

@@ -168,4 +168,5 @@ chatmail.zone
# docker
/data/
/custom/
docker-compose.override.yaml
.env

View File

@@ -44,15 +44,10 @@ class Config:
)
self.mtail_address = params.get("mtail_address")
self.disable_ipv6 = params.get("disable_ipv6", "false").lower() == "true"
self.noacme = os.environ.get("CHATMAIL_NOACME", "false").lower() == "true"
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:

View File

@@ -69,16 +69,6 @@ disable_ipv6 = False
# 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

View File

@@ -57,10 +57,19 @@ def test_one_mail(
path = str(config._inipath)
popen = make_popen(["filtermail", path, filtermail_mode])
line = popen.stderr.readline().strip()
if b"loop" not in line:
print(line.decode("ascii"), file=sys.stderr)
pytest.fail("starting filtermail failed")
# Wait for filtermail to start accepting connections
import socket
import time
for _ in range(50): # 5 second timeout
try:
sock = socket.create_connection(("127.0.0.1", smtp_inject_port), timeout=0.1)
sock.close()
break
except (ConnectionRefusedError, OSError):
time.sleep(0.1)
else:
pytest.fail("filtermail failed to start accepting connections")
addr = f"user1@{config.mail_domain}"
config.get_user(addr).set_password("l1k2j3l1k2j3l")

View File

@@ -110,7 +110,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"
env["CHATMAIL_NOPORTCHECK"] = "True"
env["CHATMAIL_NOSYSCTL"] = "True"
cmd = f"{pyinf} @local {deploy_path} -y"
if version.parse(pyinfra.__version__) < version.parse("3"):
@@ -330,7 +331,7 @@ def add_config_option(parser):
"--config",
dest="inipath",
action="store",
default=Path("chatmail.ini"),
default=Path(os.environ.get("CHATMAIL_INI", "chatmail.ini")),
type=Path,
help="path to the chatmail.ini file",
)

View File

@@ -2,6 +2,7 @@
Chat Mail pyinfra deploy.
"""
import os
import shutil
import subprocess
import sys
@@ -538,13 +539,12 @@ class GithashDeployer(Deployer):
)
def deploy_chatmail(config_path: Path, disable_mail: bool, website_only: bool, docker: bool) -> None:
def deploy_chatmail(config_path: Path, disable_mail: bool, website_only: 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)
@@ -570,7 +570,7 @@ def deploy_chatmail(config_path: Path, disable_mail: bool, website_only: bool, d
Out().red(f"Deploy failed: mtail_address {config.mtail_address} is not available (VPN up?).\n")
exit(1)
if not docker:
if not os.environ.get("CHATMAIL_NOPORTCHECK"):
port_services = [
(["master", "smtpd"], 25),
("unbound", 53),
@@ -610,7 +610,12 @@ def deploy_chatmail(config_path: Path, disable_mail: bool, website_only: bool, d
UnboundDeployer(config),
TurnDeployer(mail_domain),
IrohDeployer(config.enable_iroh_relay),
AcmetoolDeployer(config.acme_email, tls_domains),
]
if not config.noacme:
all_deployers.append(AcmetoolDeployer(config.acme_email, tls_domains))
all_deployers += [
WebsiteDeployer(config),
ChatmailVenvDeployer(config),
MtastsDeployer(),

View File

@@ -1,3 +1,5 @@
import os
from chatmaild.config import Config
from pyinfra import host
from pyinfra.facts.server import Arch, Sysctl
@@ -118,7 +120,7 @@ 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
if config.change_kernel_settings:
if not os.environ.get("CHATMAIL_NOSYSCTL"):
for name in ("max_user_instances", "max_user_watches"):
key = f"fs.inotify.{name}"
if host.get_fact(Sysctl)[key] > 65535:

View File

@@ -1,2 +1,3 @@
/^DKIM-Signature:/ IGNORE
/^Authentication-Results:/ IGNORE
/^Received:/ IGNORE

View File

@@ -15,9 +15,8 @@ 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)
deploy_chatmail(config_path, disable_mail, website_only)
if pyinfra.is_cli:

View File

@@ -89,6 +89,11 @@ class LocalExec:
self.verbose = verbose
self.docker = docker
def __call__(self, call, kwargs=None, log_callback=None):
if kwargs is None:
kwargs = {}
return call(**kwargs)
def logged(self, call, kwargs: dict):
where = "locally"
if self.docker:

262
doc/source/docker.rst Normal file
View File

@@ -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 <https://docs.docker.com/engine/install/debian/#install-using-the-repository>`_:)
- **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

View File

@@ -83,9 +83,8 @@ steps. Please substitute it with your own domain.
Docker installation
-------------------
We have experimental support for `docker compose <https://github.com/chatmail/relay/blob/docker-rebase/docs/DOCKER_INSTALLATION_EN.md>`_,
but it is not covered by automated tests yet,
so don't expect everything to work.
There is experimental support for running chatmail via Docker Compose.
See :doc:`docker` for full setup instructions.
Other helpful commands
----------------------

View File

@@ -13,6 +13,7 @@ Contributions and feedback welcome through the https://github.com/chatmail/relay
:maxdepth: 5
getting_started
docker
proxy
migrate
overview

View File

@@ -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

View File

@@ -1,8 +1,19 @@
# 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
@@ -20,33 +31,21 @@ services:
max-size: "10m"
max-file: "3"
environment:
CHANGE_KERNEL_SETTINGS: "False"
MAIL_DOMAIN: $MAIL_DOMAIN
ACME_EMAIL: $ACME_EMAIL
RECREATE_VENV: $RECREATE_VENV
MAX_MESSAGE_SIZE: $MAX_MESSAGE_SIZE
DEBUG_COMMANDS_ENABLED: $DEBUG_COMMANDS_ENABLED
FORCE_REINIT_INI_FILE: $FORCE_REINIT_INI_FILE
USE_FOREIGN_CERT_MANAGER: $USE_FOREIGN_CERT_MANAGER
ENABLE_CERTS_MONITORING: $ENABLE_CERTS_MONITORING
CERTS_MONITORING_TIMEOUT: $CERTS_MONITORING_TIMEOUT
IS_DEVELOPMENT_INSTANCE: $IS_DEVELOPMENT_INSTANCE
CMDEPLOY_STAGES: ${CMDEPLOY_STAGES:-}
CHATMAIL_NOSYSCTL: ${CHATMAIL_NOSYSCTL:-True}
CHATMAIL_NOPORTCHECK: ${CHATMAIL_NOPORTCHECK:-True}
CHATMAIL_NOACME: ${CHATMAIL_NOACME:-}
network_mode: "host"
volumes:
## system
- /sys/fs/cgroup:/sys/fs/cgroup:rw # required for systemd
- ./:/opt/chatmail
## 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
## data
- ./data/chatmail:/home
- ./data/chatmail-dkimkeys:/etc/dkimkeys
- ./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
volumes:
chatmail-data:
chatmail-dkimkeys:
chatmail-acme:

9
docker/build.sh Executable file
View File

@@ -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 "$@"

View File

@@ -8,7 +8,7 @@ RUN echo 'APT::Install-Recommends "0";' > /etc/apt/apt.conf.d/01norecommend && \
apt-get install -y \
ca-certificates && \
DEBIAN_FRONTEND=noninteractive \
TZ=Europe/London \
TZ=UTC \
apt-get install -y tzdata && \
apt-get install -y locales && \
sed -i -e "s/# $LANG.*/$LANG UTF-8/" /etc/locale.gen && \
@@ -37,59 +37,64 @@ RUN apt-get update && \
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/*
# --- 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
# --- 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
# 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_DOCKER=True \
/tmp/build-venv/bin/pyinfra @local \
/tmp/chatmail-src/cmdeploy/src/cmdeploy/run.py -y
CHATMAIL_NOSYSCTL=True \
CHATMAIL_NOPORTCHECK=True \
/opt/cmdeploy/bin/pyinfra @local \
/opt/chatmail/cmdeploy/src/cmdeploy/run.py -y
RUN rm -rf /tmp/chatmail-src /tmp/build-venv /tmp/chatmail.ini
RUN cp -a www/ /opt/chatmail-www/
WORKDIR /opt/chatmail
# --- End build-time install stage ---
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/update_ini.sh /update_ini.sh
COPY --chmod=555 ./docker/files/entrypoint.sh /entrypoint.sh
VOLUME ["/sys/fs/cgroup", "/home"]
# 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

View File

@@ -1,84 +0,0 @@
#!/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()

View File

@@ -0,0 +1,116 @@
# Traefik reverse proxy + cert manager for chatmail.
# Use this instead of docker-compose.yaml when Traefik manages TLS certificates.
#
# Required .env vars:
# MAIL_DOMAIN=chat.example.com
# ACME_EMAIL=admin@example.com
#
# Usage:
# cp docker/example-traefik.env .env
# docker compose -f docker/docker-compose-traefik.yaml build
# docker compose -f docker/docker-compose-traefik.yaml up -d
services:
chatmail:
build:
context: ../
dockerfile: docker/chatmail_relay.dockerfile
image: chatmail-relay:latest
restart: unless-stopped
container_name: chatmail
depends_on:
traefik-certs-dumper:
condition: service_started
cgroup: host
tty: true
tmpfs:
- /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_NOACME: "true"
PATH_TO_SSL: /var/lib/acme/live/${MAIL_DOMAIN}
ports:
- "25:25"
- "143:143"
- "465:465"
- "587:587"
- "993:993"
volumes:
- /sys/fs/cgroup:/sys/fs/cgroup:rw
- chatmail-data:/home
- chatmail-dkimkeys:/etc/dkimkeys
- traefik-certs:/var/lib/acme/live:ro
labels:
- traefik.enable=true
- traefik.http.services.chatmail.loadbalancer.server.scheme=https
- traefik.http.services.chatmail.loadbalancer.server.port=443
- traefik.http.services.chatmail.loadbalancer.serverstransport=insecure@file
- traefik.http.routers.chatmail.rule=Host(`${MAIL_DOMAIN}`) || Host(`mta-sts.${MAIL_DOMAIN}`) || Host(`www.${MAIL_DOMAIN}`)
- traefik.http.routers.chatmail.tls=true
- traefik.http.routers.chatmail.tls.certresolver=letsEncrypt
traefik:
image: traefik:v3.3
container_name: traefik
restart: unless-stopped
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
command:
- "--configFile=/config.yaml"
- "--certificatesresolvers.letsEncrypt.acme.email=${ACME_EMAIL}"
network_mode: host
depends_on:
traefik-init:
condition: service_completed_successfully
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./traefik/config.yaml:/config.yaml:ro
- traefik-data:/data
- ./traefik/dynamic-configs:/dynamic/conf:ro
traefik-init:
image: alpine:latest
restart: "no"
entrypoint: sh -c 'touch /data/acme.json && chmod 600 /data/acme.json'
volumes:
- traefik-data:/data
traefik-certs-dumper:
image: ldez/traefik-certs-dumper:v2.10.0
restart: unless-stopped
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
depends_on:
- traefik
entrypoint: sh -c '
apk add openssl
&& while ! [ -e /data/acme.json ] || ! [ "$$(jq ".[] | .Certificates | length" /data/acme.json | jq -s "add")" != "0" ]; do
sleep 1
; done
&& traefik-certs-dumper file --version v3 --watch --domain-subdir=true
--source /data/acme.json --dest /certs --post-hook "sh /post-hook.sh"'
volumes:
- traefik-data:/data:ro
- traefik-certs:/certs
- ./traefik/post-hook.sh:/post-hook.sh:ro
volumes:
chatmail-data:
chatmail-dkimkeys:
traefik-data:
traefik-certs:

View File

@@ -0,0 +1,5 @@
MAIL_DOMAIN="chat.example.com"
ACME_EMAIL="admin@example.com"
# CMDEPLOY_STAGES - default: "configure,activate". Set to "install,configure,activate" to force full reinstall.
# CMDEPLOY_STAGES="configure,activate"

View File

@@ -1,11 +0,0 @@
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"
# CMDEPLOY_STAGES - default: "configure,activate". Set to "install,configure,activate" to force full reinstall.

View File

@@ -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

View File

@@ -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

View File

@@ -0,0 +1,9 @@
[Unit]
Description=Periodically check TLS certificate changes
[Timer]
OnBootSec=120
OnUnitActiveSec=60
[Install]
WantedBy=timers.target

View File

@@ -1,11 +1,12 @@
#!/bin/bash
set -eo pipefail
unlink /etc/nginx/sites-enabled/default || true
SETUP_CHATMAIL_SERVICE_PATH="${SETUP_CHATMAIL_SERVICE_PATH:-/lib/systemd/system/setup_chatmail.service}"
env_vars=$(printenv | cut -d= -f1 | xargs)
sed -i "s|<envs_list>|$env_vars|g" $SETUP_CHATMAIL_SERVICE_PATH
# 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|<envs_list>|$env_vars|g" "$SETUP_CHATMAIL_SERVICE_PATH"
exec /lib/systemd/systemd $@
exec /lib/systemd/systemd "$@"

View File

@@ -1,84 +1,54 @@
#!/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="${PATH_TO_SSL:-/var/lib/acme/live/${MAIL_DOMAIN}}"
export CHANGE_KERNEL_SETTINGS=${CHANGE_KERNEL_SETTINGS:-"False"}
export RECREATE_VENV=${RECREATE_VENV:-"false"}
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
debug_commands() {
echo "Executing debug commands"
# git config --global --add safe.directory /opt/chatmail
# ./scripts/initenv.sh
}
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
}
### MAIN
if [ "$DEBUG_COMMANDS_ENABLED" = true ]; then
debug_commands
fi
if [ "$FORCE_REINIT_INI_FILE" = true ]; then
INI_CMD_ARGS=--force
fi
if [ ! -f /etc/dkimkeys/opendkim.private ]; then
/usr/sbin/opendkim-genkey -D /etc/dkimkeys -d $MAIL_DOMAIN -s opendkim
/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
# Fix ownership for bind-mounted keys (host opendkim UID may differ from container)
chown -R opendkim:opendkim /etc/dkimkeys
# 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
# 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
export CMDEPLOY_STAGES="${CMDEPLOY_STAGES:-configure,activate}"
./scripts/cmdeploy run --ssh-host @docker
echo "ForwardToConsole=yes" >> /etc/systemd/journald.conf
# 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
monitor_certificates &
# 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

View File

@@ -1,79 +0,0 @@
#!/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"

View File

@@ -0,0 +1,30 @@
log:
level: INFO
entryPoints:
web:
address: ":80"
http:
redirections:
entryPoint:
to: websecure
permanent: true
websecure:
address: ":443"
providers:
docker:
endpoint: "unix:///var/run/docker.sock"
exposedByDefault: false
file:
directory: /dynamic/conf
watch: true
certificatesResolvers:
letsEncrypt:
acme:
storage: /data/acme.json
caServer: "https://acme-v02.api.letsencrypt.org/directory"
tlschallenge: true
httpChallenge:
entryPoint: web

View File

@@ -0,0 +1,4 @@
http:
serversTransports:
insecure:
insecureSkipVerify: true

View File

@@ -0,0 +1,12 @@
#!/bin/sh
# Post-hook for traefik-certs-dumper: create symlinks from Traefik's
# cert dump format to the paths chatmail expects (fullchain, privkey).
CERTS_DIR="${CERTS_DIR:-/certs}"
for dir in "$CERTS_DIR"/*/; do
[ -d "$dir" ] || continue
cd "$dir"
[ -f "certificate.crt" ] && ln -sf certificate.crt fullchain
[ -f "privatekey.key" ] && ln -sf privatekey.key privkey
cd - > /dev/null
done

View File

@@ -1,185 +0,0 @@
# Known issues and limitations
- 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.
**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.
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. 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
```
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.
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`)
- `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` 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.
4. Build the Docker image:
```shell
docker compose build chatmail
```
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
```
### 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://<your_domain_name>` 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 wont 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
```
## 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.
To force a full reinstall (e.g., after updating the source), either rebuild the image:
```shell
docker compose build chatmail
docker compose up -d
```
Or override the stages at runtime without rebuilding:
```shell
CMDEPLOY_STAGES="install,configure,activate" docker compose up -d
```

View File

@@ -1,174 +0,0 @@
# Известные проблемы и ограничения
- 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. Настроить параметры ядра, потому что внутри контейнера их нельзя изменить, а конкретно `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
```
2. Скопировать `./docker/example.env` и переименовать в `.env`. Здесь хранятся переменные, которые используются в `docker-compose.yaml`.
```shell
cp ./docker/example.env .env
```
3. Настроить переменные окружения в `.env` файле. Эти переменные используются в `docker-compose.yaml` файле, чтобы передавать повторяющиеся значения.
Ниже перечислен список переменных учавствующих при развертывании:
- `MAIL_DOMAIN` - Доменное имя будущего сервера. (required)
- `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` - Путь где располагаются сертификаты. (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 формате.
4. Собрать docker образ
```shell
docker compose build chatmail
```
5. Запустить docker compose и дождаться завершения установки
```shell
docker compose up -d # запуск сервиса
docker compose logs -f chatmail # просмотр логов контейнера. Для выхода нажать CTRL+C
```
6. По окончанию установки можно открыть в браузер `https://<your_domain_name>`
## Использование кастомных файлов
При использовании 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: <image name from step 1>
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="${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
```
4. Перезапустить сервис
```shell
docker compose down
docker compose up -d
```

7
env.example Normal file
View File

@@ -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"