Compare commits

..

2 Commits

Author SHA1 Message Date
missytake
5399ea1f59 doc: cmdeploy command makes manual configuration obsolete 2025-10-08 12:01:16 +02:00
missytake
f7d0a9150d proxy: untested draft about deploying a reverse proxy 2025-10-08 12:01:16 +02:00
26 changed files with 255 additions and 1098 deletions

7
.gitignore vendored
View File

@@ -164,10 +164,3 @@ cython_debug/
#.idea/ #.idea/
chatmail.zone chatmail.zone
# docker
/data/
/custom/
docker-compose.yaml
.env
/traefik/data/

View File

@@ -20,15 +20,6 @@
- Allow ports 143 and 993 to be used by `dovecot` process - Allow ports 143 and 993 to be used by `dovecot` process
([#639](https://github.com/chatmail/relay/pull/639)) ([#639](https://github.com/chatmail/relay/pull/639))
- Add installation via docker compose (MVP 1). The instructions, known issues and limitations are located in `/docs`
([#614](https://github.com/chatmail/relay/pull/614))
- Add configuration parameters
([#614](https://github.com/chatmail/relay/pull/614)):
- `use_foreign_cert_manager` - Use a third-party certificate manager instead of acmetool (default: `False`)
- `change_kernel_settings` - Whether to change kernel parameters during installation (default: `True`)
- `fs_inotify_max_user_instances_and_watchers` - Value for kernel parameters `fs.inotify.max_user_instances` and `fs.inotify.max_user_watches` (default: `65535`)
## 1.7.0 2025-09-11 ## 1.7.0 2025-09-11
- Make www upload path configurable - Make www upload path configurable

105
README.md
View File

@@ -74,23 +74,22 @@ Please substitute it with your own domain.
``` ```
git clone https://github.com/chatmail/relay git clone https://github.com/chatmail/relay
cd relay cd relay
```
### Manual installation
1. On your local PC, create chatmail configuration file `chatmail.ini`:
```
scripts/initenv.sh scripts/initenv.sh
```
3. On your local PC, create chatmail configuration file `chatmail.ini`:
```
scripts/cmdeploy init chat.example.org # <-- use your domain scripts/cmdeploy init chat.example.org # <-- use your domain
``` ```
2. Verify that SSH root login to your remote server works: 4. Verify that SSH root login to your remote server works:
``` ```
ssh root@chat.example.org # <-- use your domain ssh root@chat.example.org # <-- use your domain
``` ```
3. From your local PC, deploy the remote chatmail relay server: 5. From your local PC, deploy the remote chatmail relay server:
``` ```
scripts/cmdeploy run scripts/cmdeploy run
@@ -100,9 +99,6 @@ Please substitute it with your own domain.
which you should configure at your DNS provider which you should configure at your DNS provider
(it can take some time until they are public). (it can take some time until they are public).
### Docker installation
Installation using docker compose is presented [here](./docs/DOCKER_INSTALLATION_EN.md)
### Other helpful commands ### Other helpful commands
To check the status of your remotely running chatmail service: To check the status of your remotely running chatmail service:
@@ -460,94 +456,15 @@ to send messages outside.
To setup a reverse proxy To setup a reverse proxy
(or rather Destination NAT, DNAT) (or rather Destination NAT, DNAT)
for your chatmail relay, for your chatmail relay, run:
put the following configuration in `/etc/nftables.conf`:
```
#!/usr/sbin/nft -f
flush ruleset
define wan = eth0
# Which ports to proxy.
#
# Note that SSH is not proxied
# so it is possible to log into the proxy server
# and not the original one.
define ports = { smtp, http, https, imap, imaps, submission, submissions }
# The host we want to proxy to.
define ipv4_address = AAA.BBB.CCC.DDD
define ipv6_address = [XXX::1]
table ip nat {
chain prerouting {
type nat hook prerouting priority dstnat; policy accept;
iif $wan tcp dport $ports dnat to $ipv4_address
}
chain postrouting {
type nat hook postrouting priority 0;
oifname $wan masquerade
}
}
table ip6 nat {
chain prerouting {
type nat hook prerouting priority dstnat; policy accept;
iif $wan tcp dport $ports dnat to $ipv6_address
}
chain postrouting {
type nat hook postrouting priority 0;
oifname $wan masquerade
}
}
table inet filter {
chain input {
type filter hook input priority filter; policy drop;
# Accept ICMP.
# It is especially important to accept ICMPv6 ND messages,
# otherwise IPv6 connectivity breaks.
icmp type { echo-request } accept
icmpv6 type { echo-request, nd-neighbor-solicit, nd-router-advert, nd-neighbor-advert } accept
# Allow incoming SSH connections.
tcp dport { ssh } accept
ct state established accept
}
chain forward {
type filter hook forward priority filter; policy drop;
ct state established accept
ip daddr $ipv4_address counter accept
ip6 daddr $ipv6_address counter accept
}
chain output {
type filter hook output priority filter;
}
}
```
Run `systemctl enable nftables.service`
to ensure configuration is reloaded when the proxy relay reboots.
Uncomment in `/etc/sysctl.conf` the following two lines:
``` ```
net.ipv4.ip_forward=1 scripts/cmdeploy proxy <proxy_ip_address> --relay-ipv4 <relay_ipv4_address> --relay-ipv6 <relay_ipv6_address>
net.ipv6.conf.all.forwarding=1
``` ```
Then reboot the relay or do `sysctl -p` and `nft -f /etc/nftables.conf`.
Once proxy relay is set up, Once proxy relay is set up,
you can add its IP address to the DNS. you can add its IP address to the DNS,
or distribute it as you wish.
## Neighbors and Acquaintances ## Neighbors and Acquaintances

View File

@@ -44,15 +44,6 @@ class Config:
) )
self.mtail_address = params.get("mtail_address") self.mtail_address = params.get("mtail_address")
self.disable_ipv6 = params.get("disable_ipv6", "false").lower() == "true" self.disable_ipv6 = params.get("disable_ipv6", "false").lower() == "true"
self.use_foreign_cert_manager = (
params.get("use_foreign_cert_manager", "false").lower() == "true"
)
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_rawlog = params.get("imap_rawlog", "false").lower() == "true"
if "iroh_relay" not in params: if "iroh_relay" not in params:
self.iroh_relay = "https://" + params["mail_domain"] self.iroh_relay = "https://" + params["mail_domain"]

View File

@@ -49,7 +49,7 @@ passthrough_recipients = xstore@testrun.org echo@{mail_domain}
# Deployment Details # Deployment Details
# #
# SMTP outgoing filtermail and reinjection # SMTP outgoing filtermail and reinjection
filtermail_smtp_port = 10080 filtermail_smtp_port = 10080
postfix_reinject_port = 10025 postfix_reinject_port = 10025
@@ -60,19 +60,6 @@ postfix_reinject_port_incoming = 10026
# if set to "True" IPv6 is disabled # if set to "True" IPv6 is disabled
disable_ipv6 = False disable_ipv6 = False
# if you set "True", acmetool will not be installed and you will have to manage certificates yourself.
use_foreign_cert_manager = False
#
# 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 # Defaults to https://iroh.{{mail_domain}} and running `iroh-relay` on the chatmail
# service. # service.
# If you set it to anything else, the service will be disabled # If you set it to anything else, the service will be disabled

View File

@@ -395,21 +395,20 @@ def _configure_dovecot(config: Config, debug: bool = False) -> bool:
config=config, config=config,
) )
# as per https://doc.dovecot.org/2.3/configuration_manual/os/ # as per https://doc.dovecot.org/configuration_manual/os/
# it is recommended to set the following inotify limits # it is recommended to set the following inotify limits
if config.change_kernel_settings: for name in ("max_user_instances", "max_user_watches"):
for name in ("max_user_instances", "max_user_watches"): key = f"fs.inotify.{name}"
key = f"fs.inotify.{name}" if host.get_fact(Sysctl)[key] > 65535:
if host.get_fact(Sysctl)[key] == config.fs_inotify_max_user_instances_and_watchers: # Skip updating limits if already sufficient
# Skip updating limits if already sufficient # (enables running in incus containers where sysctl readonly)
# (enables running in incus containers where sysctl readonly) continue
continue server.sysctl(
server.sysctl( name=f"Change {key}",
name=f"Change {key}", key=key,
key=key, value=65535,
value=config.fs_inotify_max_user_instances_and_watchers, persist=True,
persist=True, )
)
timezone_env = files.line( timezone_env = files.line(
name="Set TZ environment variable", name="Set TZ environment variable",
@@ -726,11 +725,10 @@ def deploy_chatmail(config_path: Path, disable_mail: bool) -> None:
deploy_iroh_relay(config) deploy_iroh_relay(config)
# Deploy acmetool to have TLS certificates. # Deploy acmetool to have TLS certificates.
if not config.use_foreign_cert_manager: tls_domains = [mail_domain, f"mta-sts.{mail_domain}", f"www.{mail_domain}"]
tls_domains = [mail_domain, f"mta-sts.{mail_domain}", f"www.{mail_domain}"] deploy_acmetool(
deploy_acmetool( domains=tls_domains,
domains=tls_domains, )
)
apt.packages( apt.packages(
# required for setfacl for echobot # required for setfacl for echobot

View File

@@ -61,7 +61,7 @@ def run_cmd_options(parser):
parser.add_argument( parser.add_argument(
"--ssh-host", "--ssh-host",
dest="ssh_host", dest="ssh_host",
help="Deploy to 'localhost', via 'docker', or to a specific SSH host", help="Deploy to 'localhost' or to a specific SSH host",
) )
@@ -83,7 +83,7 @@ def run_cmd(args, out):
pyinf = "pyinfra --dry" if args.dry_run else "pyinfra" pyinf = "pyinfra --dry" if args.dry_run else "pyinfra"
cmd = f"{pyinf} --ssh-user root {ssh_host} {deploy_path} -y" cmd = f"{pyinf} --ssh-user root {ssh_host} {deploy_path} -y"
if ssh_host in ["localhost", "docker"]: if ssh_host == "localhost":
cmd = f"{pyinf} @local {deploy_path} -y" cmd = f"{pyinf} @local {deploy_path} -y"
if version.parse(pyinfra.__version__) < version.parse("3"): if version.parse(pyinfra.__version__) < version.parse("3"):
@@ -101,9 +101,6 @@ def run_cmd(args, out):
kwargs=dict(command="cat /var/lib/echobot/invite-link.txt"), kwargs=dict(command="cat /var/lib/echobot/invite-link.txt"),
) )
) )
server_deployed_message = f"Chatmail server started: https://{args.config.mail_domain}/"
delimiter_line = "=" * len(server_deployed_message)
out.green(f"{delimiter_line}\n{server_deployed_message}\n{delimiter_line}")
out.green("Deploy completed, call `cmdeploy dns` next.") out.green("Deploy completed, call `cmdeploy dns` next.")
elif not remote_data["acme_account_url"]: elif not remote_data["acme_account_url"]:
out.red("Deploy completed but letsencrypt not configured") out.red("Deploy completed but letsencrypt not configured")
@@ -128,7 +125,7 @@ def dns_cmd_options(parser):
parser.add_argument( parser.add_argument(
"--ssh-host", "--ssh-host",
dest="ssh_host", dest="ssh_host",
help="Run the DNS queries on 'localhost', in the chatmail 'docker' container, or on a specific SSH host", help="Run the DNS queries on 'localhost' or on a specific SSH host",
) )
@@ -211,6 +208,61 @@ def test_cmd(args, out):
return ret return ret
def proxy_cmd_options(parser: argparse.ArgumentParser):
parser.add_argument(
"ip_address",
help="specify a server to deploy to; can also be an inventory.py file",
)
parser.add_argument(
"--relay-ipv4",
dest="relay_ipv4",
help="The ipv4 address of the relay you want to forward traffic to",
)
parser.add_argument(
"--relay-ipv6",
dest="relay_ipv6",
help="The ipv6 address of the relay you want to forward traffic to",
)
parser.add_argument(
"--dry-run",
dest="dry_run",
action="store_true",
help="don't actually modify the server",
)
def proxy_cmd(args, out):
"""Deploy reverse proxy on a second server."""
env = os.environ.copy()
env["RELAY_IPV4"] = args.relay_ipv4
env["RELAY_IPV6"] = args.relay_ipv6
deploy_path = importlib.resources.files(__package__).joinpath("proxy-deploy.py").resolve()
pyinf = "pyinfra --dry" if args.dry_run else "pyinfra"
sshexec = args.get_sshexec()
# :todo make sure relay is deployed to args.relay_ipv4 and args.relay_ipv6
# abort if IP address == the chatmail relay itself: if port 22 is open AND /etc/chatmail-version exists
if sshexec.logged(call=remote.rshell.get_port_service, args=[22]):
if sshexec.logged(call=remote.rshell.chatmail_version):
out.red("Can not deploy proxy on the chatmail relay itself, use another server")
return 1
cmd = f"{pyinf} --ssh-user root {args.ip_address} {deploy_path} -y"
out.check_call(cmd, env=env) # during first try, only set SSH port to 2222
cmd = f"{pyinf} --ssh-port 2222 --ssh-user root {args.ip_address} {deploy_path} -y"
try:
retcode = out.check_call(cmd, env=env)
if retcode == 0:
out.green("Reverse proxy deployed - you can distribute the IP address now.")
else:
out.red("Deploying reverse proxy failed")
except subprocess.CalledProcessError:
out.red("Deploying reverse proxy failed")
retcode = 1
return retcode
def fmt_cmd_options(parser): def fmt_cmd_options(parser):
parser.add_argument( parser.add_argument(
"--check", "--check",
@@ -347,8 +399,6 @@ def get_parser():
def get_sshexec(ssh_host: str, verbose=True): def get_sshexec(ssh_host: str, verbose=True):
if ssh_host in ["localhost", "@local"]: if ssh_host in ["localhost", "@local"]:
return "localhost" return "localhost"
elif ssh_host == "docker":
return "docker"
if verbose: if verbose:
print(f"[ssh] login to {ssh_host}") print(f"[ssh] login to {ssh_host}")
return SSHExec(ssh_host, verbose=verbose) return SSHExec(ssh_host, verbose=verbose)

View File

@@ -9,8 +9,6 @@ from . import remote
def get_initial_remote_data(sshexec, mail_domain): def get_initial_remote_data(sshexec, mail_domain):
if sshexec == "localhost": if sshexec == "localhost":
result = remote.rdns.perform_initial_checks(mail_domain) result = remote.rdns.perform_initial_checks(mail_domain)
elif sshexec == "docker":
result = remote.rdns.perform_initial_checks(mail_domain, pre_command="docker exec chatmail ")
else: else:
result = sshexec.logged( result = sshexec.logged(
call=remote.rdns.perform_initial_checks, kwargs=dict(mail_domain=mail_domain) call=remote.rdns.perform_initial_checks, kwargs=dict(mail_domain=mail_domain)
@@ -50,7 +48,7 @@ def check_full_zone(sshexec, remote_data, out, zonefile) -> int:
"""Check existing DNS records, optionally write them to zone file """Check existing DNS records, optionally write them to zone file
and return (exitcode, remote_data) tuple.""" and return (exitcode, remote_data) tuple."""
if sshexec in ["localhost", "docker"]: if sshexec == "localhost":
required_diff, recommended_diff = remote.rdns.check_zonefile( required_diff, recommended_diff = remote.rdns.check_zonefile(
zonefile=zonefile, verbose=False zonefile=zonefile, verbose=False
) )

View File

@@ -0,0 +1,19 @@
import os
import pyinfra
from pyinfra import host
from proxy import configure_ssh, configure_proxy
def main():
ipv4_relay = os.getenv("IPV4_RELAY")
ipv6_relay = os.getenv("IPV6_RELAY")
configure_ssh()
if host.data.get("ssh_port") not in (None, 22):
configure_proxy(ipv4_relay, ipv6_relay)
if pyinfra.is_cli:
main()

View File

@@ -0,0 +1,63 @@
import importlib
from pyinfra import host
from pyinfra.operations import files, server, apt, systemd
def configure_ssh():
files.replace(
name="Configure sshd to use port 2222",
path="/etc/ssh/sshd_config",
text="Port 22\n",
replace="Port 2222\n",
)
systemd.service(
name="apply SSH config",
service="ssh",
reloaded=True,
)
apt.update()
def configure_proxy(ipv4_relay, ipv6_relay):
files.put(
name="Configure nftables",
src=importlib.resources.files(__package__).joinpath("proxy_files/nftables.conf.j2"),
dest="/etc/nftables.conf",
ipv4_address=ipv4_relay, # :todo what if only one of them is specified?
ipv6_address=ipv6_relay,
)
server.sysctl(name="enable IPv4 forwarding", key="net.ipv4.ip_forward", value=1, persist=True)
server.sysctl(
name="enable IPv6 forwarding",
key="net.ipv6.conf.all.forwarding",
value=1,
persist=True,
)
server.shell(
name="apply forwarding configuration",
commands=[
"sysctl -p",
"nft -f /etc/nftables.conf",
],
)
if host.data.get("floating_ips"):
i = 0
for floating_ip in host.data.get("floating_ips"):
i += 1
files.template(
name="Add floating IPs",
src="servers/proxy-nine/files/60-floating.ip.cfg.j2",
dest=f"/etc/network/interfaces.d/{59 + i}-floating.ip.cfg",
ip_address=floating_ip,
i=i,
)
systemd.service(
name="apply floating IPs",
service="networking",
restarted=True,
)

View File

@@ -0,0 +1,4 @@
auto eth0:{{ i }}
iface eth0:{{ i }} inet static
address {{ ip_address }}
netmask 32

View File

@@ -0,0 +1,67 @@
#!/usr/sbin/nft -f
flush ruleset
define wan = eth0
# which ports to proxy
define ports = { smtp, http, https, imap, imaps, submission, submissions }
# the host we want to proxy to
define ipv4_address = {{ ipv4_address }}
define ipv6_address = [{{ ipv6_address }}]
table ip nat {
chain prerouting {
type nat hook prerouting priority dstnat; policy accept;
iif $wan tcp dport $ports dnat to $ipv4_address
}
chain postrouting {
type nat hook postrouting priority 0;
oifname $wan masquerade
}
}
table ip6 nat {
chain prerouting {
type nat hook prerouting priority dstnat; policy accept;
iif $wan tcp dport $ports dnat to $ipv6_address
}
chain postrouting {
type nat hook postrouting priority 0;
oifname $wan masquerade
}
}
table inet filter {
chain input {
type filter hook input priority filter; policy drop;
# Accept ICMP.
# It is especially important to accept ICMPv6 ND messages,
# otherwise IPv6 connectivity breaks.
icmp type { echo-request } accept
icmpv6 type { echo-request, nd-neighbor-solicit, nd-router-advert, nd-neighbor-advert } accept
# Allow incoming SSH connections.
tcp dport { 22, 2222 } accept
# Allow incoming shadowsocks connections.
tcp dport { 8388 } accept
ct state established accept
}
chain forward {
type filter hook forward priority filter; policy drop;
ct state established accept
ip daddr $ipv4_address counter accept
ip6 daddr $ipv6_address counter accept
}
chain output {
type filter hook output priority filter;
}
}

View File

@@ -21,6 +21,20 @@ def shell(command, fail_ok=False, print=print):
return "" return ""
def get_port_service(port: int) -> str:
return shell(
"ss -lptn 'src :%d' | awk 'NR>1 {print $6,$7}' | sed 's/users:((\"//;s/\".*//'"
% (port,)
)
def chatmail_version():
version = shell("cat /etc/chatmail-version")
if "cat: /etc/chatmail-version:" in version:
version = None
return version
def get_systemd_running(): def get_systemd_running():
lines = shell("systemctl --type=service --state=running").split("\n") lines = shell("systemctl --type=service --state=running").split("\n")
return [line for line in lines if line.startswith(" ")] return [line for line in lines if line.startswith(" ")]

View File

@@ -1,83 +0,0 @@
FROM jrei/systemd-debian:12 AS base
ENV LANG=en_US.UTF-8
RUN echo 'APT::Install-Recommends "0";' > /etc/apt/apt.conf.d/01norecommend && \
echo 'APT::Install-Suggests "0";' >> /etc/apt/apt.conf.d/01norecommend && \
apt-get update && \
apt-get install -y \
ca-certificates && \
DEBIAN_FRONTEND=noninteractive \
TZ=Europe/London \
apt-get install -y tzdata && \
apt-get install -y locales && \
sed -i -e "s/# $LANG.*/$LANG UTF-8/" /etc/locale.gen && \
dpkg-reconfigure --frontend=noninteractive locales && \
update-locale LANG=$LANG \
&& rm -rf /var/lib/apt/lists/*
RUN apt-get update && \
apt-get install -y \
git \
python3 \
python3-venv \
python3-virtualenv \
gcc \
python3-dev \
opendkim \
opendkim-tools \
curl \
rsync \
unbound \
unbound-anchor \
dnsutils \
postfix \
acl \
nginx \
libnginx-mod-stream \
fcgiwrap \
cron \
&& for pkg in core imapd lmtpd; do \
case "$pkg" in \
core) sha256="43f593332e22ac7701c62d58b575d2ca409e0f64857a2803be886c22860f5587" ;; \
imapd) sha256="8d8dc6fc00bbb6cdb25d345844f41ce2f1c53f764b79a838eb2a03103eebfa86" ;; \
lmtpd) sha256="2f69ba5e35363de50962d42cccbfe4ed8495265044e244007d7ccddad77513ab" ;; \
esac; \
url="https://download.delta.chat/dovecot/dovecot-${pkg}_2.3.21%2Bdfsg1-3_amd64.deb"; \
file="/tmp/$(basename "$url")"; \
curl -fsSL "$url" -o "$file"; \
echo "$sha256 $file" | sha256sum -c -; \
apt-get install -y "$file"; \
rm -f "$file"; \
done \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /opt/chatmail
ARG SETUP_CHATMAIL_SERVICE_PATH=/lib/systemd/system/setup_chatmail.service
COPY ./files/setup_chatmail.service "$SETUP_CHATMAIL_SERVICE_PATH"
RUN ln -sf "$SETUP_CHATMAIL_SERVICE_PATH" "/etc/systemd/system/multi-user.target.wants/setup_chatmail.service"
COPY --chmod=555 ./files/setup_chatmail_docker.sh /setup_chatmail_docker.sh
COPY --chmod=555 ./files/update_ini.sh /update_ini.sh
COPY --chmod=555 ./files/entrypoint.sh /entrypoint.sh
## TODO: add git clone.
## Problem: how correct save only required files inside container....
# RUN git clone https://github.com/chatmail/relay.git -b master . \
# && ./scripts/initenv.sh
# EXPOSE 443 25 587 143 993
VOLUME ["/sys/fs/cgroup", "/home"]
STOPSIGNAL SIGRTMIN+3
ENTRYPOINT ["/entrypoint.sh"]
CMD [ "--default-standard-output=journal+console", \
"--default-standard-error=journal+console" ]
## TODO: Add installation and configuration of chatmaild inside the Dockerfile.
## This is required to ensure repeatable deployment.
## In the current MVP, the chatmaild server is updated on every container restart.

View File

@@ -1,59 +0,0 @@
services:
chatmail:
build:
context: ./docker
dockerfile: chatmail_relay.dockerfile
tags:
- chatmail-relay:latest
image: chatmail-relay:latest
restart: unless-stopped
container_name: chatmail
cgroup: host # required for systemd
tty: true # required for logs
tmpfs: # required for systemd
- /tmp
- /run
- /run/lock
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
environment:
MAIL_DOMAIN: $MAIL_DOMAIN
CHANGE_KERNEL_SETTINGS: "False"
ACME_EMAIL: $ACME_EMAIL
# RECREATE_VENV: "false"
# MAX_MESSAGE_SIZE: "50M"
# DEBUG_COMMANDS_ENABLED: "true"
# FORCE_REINIT_INI_FILE: "true"
# USE_FOREIGN_CERT_MANAGER: "True"
# ENABLE_CERTS_MONITORING: "true"
# CERTS_MONITORING_TIMEOUT: 10
# IS_DEVELOPMENT_INSTANCE: "True"
ports:
- "80:80"
- "443:443"
- "25:25"
- "587:587"
- "143:143"
- "465:465"
- "993:993"
volumes:
## system
- /sys/fs/cgroup:/sys/fs/cgroup:rw # required for systemd
- ./:/opt/chatmail
## data
- ./data/chatmail:/home
- ./data/chatmail-dkimkeys:/etc/dkimkeys
- ./data/chatmail-echobot:/run/echobot
- ./data/chatmail-acme:/var/lib/acme
## custom resources
# - ./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

View File

@@ -1,136 +0,0 @@
services:
chatmail:
build:
context: ./docker
dockerfile: chatmail_relay.dockerfile
tags:
- chatmail-relay:latest
image: chatmail-relay:latest
restart: unless-stopped
container_name: chatmail
depends_on:
- traefik-certs-dumper
cgroup: host # required for systemd
tty: true # required for logs
tmpfs: # required for systemd
- /tmp
- /run
- /run/lock
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
environment: #all possible variables you can check inside README and /chatmaild/src/chatmaild/ini/chatmail.ini.f
MAIL_DOMAIN: $MAIL_DOMAIN
# MAX_MESSAGE_SIZE: "50M"
# DEBUG_COMMANDS_ENABLED: "true"
# FORCE_REINIT_INI_FILE: "true"
# RECREATE_VENV: "false"
USE_FOREIGN_CERT_MANAGER: "true"
CHANGE_KERNEL_SETTINGS: "false"
PATH_TO_SSL: "${CERTS_ROOT_DIR_CONTAINER}/${MAIL_DOMAIN}"
ENABLE_CERTS_MONITORING: "true"
# CERTS_MONITORING_TIMEOUT: 60
# IS_DEVELOPMENT_INSTANCE: "true"
ports:
- "25:25"
- "587:587"
- "143:143"
- "465:465"
- "993:993"
volumes:
## system
- /sys/fs/cgroup:/sys/fs/cgroup:rw # required for systemd
- ./:/opt/chatmail
- ${CERTS_ROOT_DIR_HOST}:${CERTS_ROOT_DIR_CONTAINER}:ro
## data
- ./data/chatmail:/home
# - ./data/chatmail-dkimkeys:/etc/dkimkeys
# - ./data/chatmail-echobot:/run/echobot
# - ./data/chatmail-acme:/var/lib/acme
## custom resources
# - ./custom/www/src/index.md:/opt/chatmail/www/src/index.md
## debug
# - ./docker/files/setup_chatmail_docker.sh:/setup_chatmail_docker.sh
# - ./docker/files/entrypoint.sh:/entrypoint.sh
# - ./docker/files/update_ini.sh:/update_ini.sh
labels:
- traefik.enable=true
- traefik.http.services.chatmail-relay.loadbalancer.server.scheme=https
- traefik.http.services.chatmail-relay.loadbalancer.server.port=443
- traefik.http.services.chatmail-relay.loadbalancer.serverstransport=insecure@file
- traefik.http.routers.chatmail-relay.rule=Host(`${MAIL_DOMAIN}`) || Host(`mta-sts.${MAIL_DOMAIN}`) || Host(`www.${MAIL_DOMAIN}`)
- traefik.http.routers.chatmail-relay.service=chatmail-relay
- traefik.http.routers.chatmail-relay.tls=true
- traefik.http.routers.chatmail-relay.tls.certresolver=letsEncrypt
traefik_init:
image: alpine:latest
restart: on-failure
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
working_dir: /app
entrypoint: sh -c '
touch acme.json &&
chown 0:0 ./acme.json &&
chmod 600 ./acme.json'
volumes:
- ./traefik/data:/app
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}"
# ports:
# - "80:80"
# - "443:443"
network_mode: host
depends_on:
traefik_init:
condition: service_completed_successfully
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ./traefik/config.yaml:/config.yaml
- ./traefik/data/acme.json:/acme.json
- ./traefik/dynamic-configs:/dynamic/conf
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 /data/letsencrypt/certs --post-hook "sh /post-hook.sh"'
environment:
CERTS_DIR: /data/letsencrypt/certs
volumes:
- ./traefik/data/letsencrypt:/data/letsencrypt
- ./traefik/data/acme.json:/data/acme.json
- ./traefik/post-hook.sh:/post-hook.sh

View File

@@ -1,5 +0,0 @@
MAIL_DOMAIN="chat.example.com"
ACME_EMAIL="my.email@gmail.com"
CERTS_ROOT_DIR_HOST="./traefik/data/letsencrypt/certs"
CERTS_ROOT_DIR_CONTAINER="/var/lib/acme/live"

View File

@@ -1,24 +0,0 @@
#!/bin/bash
set -eo pipefail
unlink /etc/nginx/sites-enabled/default || true
if [ "${USE_FOREIGN_CERT_MANAGER,,}" == "true" ]; then
if [ ! -f "$PATH_TO_SSL/fullchain" ]; then
echo "Error: file '$PATH_TO_SSL/fullchain' does not exist. Exiting..." > /dev/stderr
sleep 2
exit 1
fi
if [ ! -f "$PATH_TO_SSL/privkey" ]; then
echo "Error: file '$PATH_TO_SSL/privkey' does not exist. Exiting..." > /dev/stderr
sleep 2
exit 1
fi
fi
SETUP_CHATMAIL_SERVICE_PATH="${SETUP_CHATMAIL_SERVICE_PATH:-/lib/systemd/system/setup_chatmail.service}"
env_vars=$(printenv | cut -d= -f1 | xargs)
sed -i "s|<envs_list>|$env_vars|g" $SETUP_CHATMAIL_SERVICE_PATH
exec /lib/systemd/systemd $@

View File

@@ -1,14 +0,0 @@
[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=<envs_list>
[Install]
WantedBy=multi-user.target

View File

@@ -1,78 +0,0 @@
#!/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"}
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
/usr/sbin/opendkim-genkey -D /etc/dkimkeys -d $MAIL_DOMAIN -s opendkim
chown opendkim:opendkim /etc/dkimkeys/opendkim.private
chown opendkim:opendkim /etc/dkimkeys/opendkim.txt
# TODO: Move to debug_commands after git clone is moved to dockerfile.
git config --global --add safe.directory /opt/chatmail
if [ "$RECREATE_VENV" == "true" ]; then
rm -rf venv
fi
./scripts/initenv.sh
./scripts/cmdeploy init --config "${INI_FILE}" $INI_CMD_ARGS $MAIL_DOMAIN
bash /update_ini.sh
./scripts/cmdeploy run --ssh-host docker
echo "ForwardToConsole=yes" >> /etc/systemd/journald.conf
systemctl restart systemd-journald
monitor_certificates &

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

@@ -1,216 +0,0 @@
# Known issues and limitations
- Chatmail will be reinstalled every time the container is started (longer the first time, faster on subsequent starts). This is how the original installer works because it wasnt designed for Docker. At the end of the documentation, theres a [proposed solution](#locking-the-chatmail-version).
- Requires cgroups v2 configured in the system. Operation with cgroups v1 has not been tested.
- Yes, of course, using systemd inside a container is a hack, and it would be better to split it into several services, but since this is an MVP, it turned out to be easier to do it this way initially than to rewrite the entire deployment system.
- The Docker image is only suitable for amd64. If you need to run it on a different architecture, try modifying the Dockerfile (specifically the part responsible for installing dovecot).
# Docker installation
This section provides instructions for installing Chatmail using docker-compose.
## Preliminary setup
We use `chat.example.org` as the Chatmail domain in the following steps.
Please substitute it with your own domain.
1. Setup the initial DNS records.
The following is an example in the familiar BIND zone file format with
a TTL of 1 hour (3600 seconds).
Please substitute your domain and IP addresses.
```
chat.example.com. 3600 IN A 198.51.100.5
chat.example.com. 3600 IN AAAA 2001:db8::5
www.chat.example.com. 3600 IN CNAME chat.example.com.
mta-sts.chat.example.com. 3600 IN CNAME chat.example.com.
```
2. clone the repository on your server.
```shell
git clone https://github.com/chatmail/relay
cd relay
```
## Installation
When installing via Docker, there are several options:
- Use the built-in nginx and acmetool in Chatmail container to host the chat and manage certificates.
- Use third-party tools for certificate management.
For the third-party certificate manager example, traefik will be used, but you can use whatever is more convenient for you.
1. Copy the file `./docker/docker-compose-default.yaml` or `./docker/docker-compose-traefik.yaml` and rename it to `docker-compose.yaml`. This is necessary because `docker-compose.yaml` is in `.gitignore` and wont cause conflicts when updating the git repository.
```shell
cp ./docker/docker-compose-default.yaml docker-compose.yaml
## or
# cp ./docker/docker-compose-traefik.yaml docker-compose.yaml
```
2. Copy `./docker/example.env` and rename it to `.env`. This file stores variables used in `docker-compose.yaml`.
```shell
cp ./docker/example.env .env
```
3. Configure environment variables in the `.env` file. These variables are used in the `docker-compose.yaml` file to pass repeated values.
4. Configure kernel parameters because they cannot be changed inside the container, specifically `fs.inotify.max_user_instances` and `fs.inotify.max_user_watches`. Run the following:
```shell
echo "fs.inotify.max_user_instances=65536" | sudo tee -a /etc/sysctl.d/99-inotify.conf
echo "fs.inotify.max_user_watches=65536" | sudo tee -a /etc/sysctl.d/99-inotify.conf
sudo sysctl --system
```
5. Configure container environment variables. Below is the list of variables used during deployment:
- `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'`)
You can also use any variables from the [ini configuration file](https://github.com/chatmail/relay/blob/main/chatmaild/src/chatmaild/ini/chatmail.ini.f); they must be in uppercase.
Mandatory variables for deployment via Docker:
- `CHANGE_KERNEL_SETTINGS` Change kernel settings (`fs.inotify.max_user_instances` and `fs.inotify.max_user_watches`) on startup. Changing kernel settings inside the container is not possible! (default: `False`)
6. Build the Docker image:
```shell
docker compose build chatmail
```
7. Start docker compose and wait for the installation to finish:
```shell
docker compose up -d # start service
docker compose logs -f chatmail # view container logs, press CTRL+C to exit
```
8. After installation is complete, you can open `https://<your_domain_name>` in your browser.
9. To send messages to other chatmail relays,
you need to set additional DNS records.
Run `docker exec chatmail scripts/cmdeploy.sh dns --ssh-host localhost`
to see recommended DNS records and check whether they are correct.
## 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
```
## Locking the Chatmail version
> [!note]
> These steps are optional and should only be done if you are not satisfied that the service is installed each time the container starts.
Since the current Docker version installs the Chatmail service every time the container starts, you can lock the container version after installation as follows:
1. Commit the current state of the configured container:
```shell
docker container commit chatmail configured-chatmail:$(date +'%Y-%m-%d')
docker image ls | grep configured-chatmail
```
2. Change the entrypoint for the container in `docker-compose.yaml` to:
```yaml
services:
chatmail:
image: <image name from step 1>
volumes:
...
## custom resources
- ./custom/setup_chatmail_docker.sh:/setup_chatmail_docker.sh
```
3. Create the file `./custom/setup_chatmail_docker.sh` with the new configuration:
```shell
mkdir -p ./custom
cat > ./custom/setup_chatmail_docker.sh << 'EOF'
#!/bin/bash
set -eo pipefail
export ENABLE_CERTS_MONITORING="${ENABLE_CERTS_MONITORING:-true}"
export CERTS_MONITORING_TIMEOUT="${CERTS_MONITORING_TIMEOUT:-60}"
export PATH_TO_SSL="${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. Restart the service:
```shell
docker compose down
docker compose up -d
```

View File

@@ -1,189 +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
При установке через docker есть несколько вариантов:
- использовать встроенный в chatmail контейнер nginx и acmetool для хостинга чата и управления сертификатами.
- использовать сторонние инструменты для менеджмента сертификатов
В качестве примера для стороннего менеджера сертификатов будет использоваться traefik, но вы можете использовать то что удобнее вам.
1. Скопировать файл `./docker/docker-compose-default.yaml` или `./docker/docker-compose-traefik.yaml` и переименовать в `docker-compose.yaml`. Это нужно потому что `docker-compose.yaml` находится в `.gitignore` и не будет создавать конфликты при обновлении гит репозитория.
```shell
cp ./docker/docker-compose-default.yaml docker-compose.yaml
## or
# cp ./docker/docker-compose-traefik.yaml docker-compose.yaml
```
2. Скопировать `./docker/example.env` и переименовать в `.env`. Здесь хранятся переменные, которые используятся в `docker-compose.yaml`.
```shell
cp ./docker/example.env .env
```
3. Настроить переменные окружения в `.env` файле. Эти переменные используются в `docker-compose.yaml` файле, чтобы передавать повторяющиеся значения.
4. Настроить параметры ядра, потому что внутри контейнера их нельзя изменить, а конкретно `fs.inotify.max_user_instances` и `fs.inotify.max_user_watches`. Для этого выполнить следующее:
```shell
echo "fs.inotify.max_user_instances=65536" | sudo tee -a /etc/sysctl.d/99-inotify.conf
echo "fs.inotify.max_user_watches=65536" | sudo tee -a /etc/sysctl.d/99-inotify.conf
sudo sysctl --system
```
5. Настроить переменные окружения контейнера. Ниже перечислен список переменных учавствующих при развертывании.
- `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 формате.
Ниже перечислены переменные, которые обязательны быть выставлены при развертывании через docker:
- `CHANGE_KERNEL_SETTINGS` - Менять настройки ядра (`fs.inotify.max_user_instances` и `fs.inotify.max_user_watches`) при запуске. При запуске в контейнере смена настроек ядра не может быть выполнена! (default: `False`)
6. Собрать docker образ
```shell
docker compose build chatmail
```
7. Запустить docker compose и дождаться завершения установки
```shell
docker compose up -d # запуск сервиса
docker compose logs -f chatmail # просмотр логов контейнера. Для выхода нажать CTRL+C
```
8. По окончанию установки можно открыть в браузер `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
```

View File

@@ -1,33 +0,0 @@
log:
level: TRACE
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
serverstransport:
insecureskipverify: true
certificatesResolvers:
letsEncrypt:
acme:
storage: /acme.json
caServer: "https://acme-v02.api.letsencrypt.org/directory"
tlschallenge: true
httpChallenge:
entryPoint: web

View File

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

View File

@@ -1,15 +0,0 @@
CERTS_DIR=${CERTS_DIR:-"/data/letsencrypt/certs"}
echo "CERTS_DIR: $CERTS_DIR"
for dir in "$CERTS_DIR"/*/; do
echo "Processing: $dir"
cd "$dir"
if [ -f "certificate.crt" ]; then
ln -sf certificate.crt fullchain
fi
if [ -f "privatekey.key" ]; then
ln -sf privatekey.key privkey
fi
cd -
done