Skip to content
Closed
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
3826de8
Add installation via docker compose (MVP 1)
Aug 9, 2025
5aef295
Merge branch 'main' into docker
Aug 17, 2025
b81e471
Merge pull request #1 from Keonik1/docker
Keonik1 Aug 17, 2025
4a92e50
Update Changelog
Aug 17, 2025
6425a83
Fix description for is_development_instance option
Aug 17, 2025
1c4c7b9
revert page-layout logo link
Aug 17, 2025
aea6366
rename dockerfile
Aug 23, 2025
b6dce61
add port 80 to docker-compose-default
Aug 23, 2025
a6e5b9e
add 465 port
Aug 23, 2025
a01eebe
add RECREATE_VENV var
Aug 23, 2025
d545fc8
Add traefik config files
Aug 23, 2025
9037409
change "restart nginx" to "reload nginx"
Aug 23, 2025
dc6d8b4
pass values to `MAIL_DOMAIN` and `ACME_EMAIL` from vars for docker-co…
Aug 23, 2025
4fc672c
Fix bug with attaching certs
Aug 23, 2025
4c42d0f
fix for lint test
Aug 23, 2025
87615b6
fix docs - nginx "restart" to "reload"
Aug 23, 2025
1b3f419
Delete ssh connection from docker installation
Aug 23, 2025
d5329fa
Fix issue with acmetool
Aug 24, 2025
5dcb002
delete default value for ACME_EMAIL
Aug 25, 2025
f027afd
delete sudo from traefik init container cmd
Aug 25, 2025
e1ca74e
fix unlink if default nginx conf is not exist
Aug 25, 2025
c372c55
try to fix tests
Aug 25, 2025
929383d
fix docs; revert tests
Aug 25, 2025
910eeea
Fix colored output file; return original exit code
Aug 26, 2025
a1301fa
add a workaround to pass the test_init_not_overwrite test
Aug 26, 2025
3d51c08
set dafault value for fs_inotify_max_user_instances_and_watchers param
Sep 5, 2025
a3c5c73
delete after a decision has been made during the discussion
Sep 5, 2025
3c14692
fix wording
Sep 5, 2025
657a00d
make posix compattable if statements
Sep 5, 2025
490776e
Add comment why variables are passed in setup_chatmail.service
Sep 5, 2025
ed3cba7
delete CERTS_ROOT_DIR_HOST. Add hardcoded paths to certificates
Sep 5, 2025
87ac465
Add the ability to render the site only in the preferred languages
Sep 5, 2025
b86c977
- pedantic fix
Sep 6, 2025
74faefa
delete ancestral legacy
Sep 6, 2025
dfe2c00
delete tabs if only one language selected
Sep 6, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -170,3 +170,4 @@ chatmail.zone
/custom/
docker-compose.yaml
.env
/traefik/data/
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
- Add `--skip-dns-check` argument to `cmdeploy run` command, which disables DNS record checking before installation.
([#614](https://github.com/chatmail/relay/pull/614))

- Add `--force` argument to `cmdeploy init` command, which recreates the `config.ini` file.
- Add `--force` argument to `cmdeploy init` command, which recreates the `chatmail.ini` file.
([#614](https://github.com/chatmail/relay/pull/614))

- Add startup for `fcgiwrap.service` because sometimes it did not start automatically.
Expand All @@ -31,6 +31,9 @@
- `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`)

- Check whether GCC is installed in initenv.sh
([#608](https://github.com/chatmail/relay/pull/608))

- Expire push notification tokens after 90 days
([#583](https://github.com/chatmail/relay/pull/583))

Expand Down
12 changes: 9 additions & 3 deletions chatmaild/src/chatmaild/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@ def __init__(self, inipath, params):
self.password_min_length = int(params["password_min_length"])
self.passthrough_senders = params["passthrough_senders"].split()
self.passthrough_recipients = params["passthrough_recipients"].split()
self.is_development_instance = params.get("is_development_instance", "true").lower() == "true"
self.is_development_instance = (
params.get("is_development_instance", "true").lower() == "true"
)
self.filtermail_smtp_port = int(params["filtermail_smtp_port"])
self.filtermail_smtp_port_incoming = int(
params["filtermail_smtp_port_incoming"]
Expand All @@ -44,9 +46,13 @@ def __init__(self, inipath, params):
)
self.mtail_address = params.get("mtail_address")
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.use_foreign_cert_manager = (
params.get("use_foreign_cert_manager", "false").lower() == "true"
)
self.acme_email = params["acme_email"]
self.change_kernel_settings = params.get("change_kernel_settings", "true").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"]
)
Expand Down
2 changes: 1 addition & 1 deletion chatmaild/src/chatmaild/ini/chatmail.ini.f
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
# Deployment Details
#

# if set to "True" on main page will be showed dev banner
# set to "False" to remove the "development instance" banner on the main page.
is_development_instance = True

# SMTP outgoing filtermail and reinjection
Expand Down
2 changes: 1 addition & 1 deletion cmdeploy/src/cmdeploy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -680,7 +680,7 @@ def deploy_chatmail(config_path: Path, disable_mail: bool) -> None:
if host.get_fact(SystemdStatus, services="unbound").get("unbound.service"):
process_on_53 = "unbound"
if process_on_53 not in (None, "unbound"):
Out().red(f"Can't install unbound: port 53 is occupied by: {process_on_53}")
Out().red(f"Can't install unbound: port 53 is occupied by: {process_on_53}")
exit(1)
apt.packages(
name="Install unbound",
Expand Down
7 changes: 7 additions & 0 deletions cmdeploy/src/cmdeploy/cmdeploy.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,11 @@ def run_cmd(args, out):
deploy_path = importlib.resources.files(__package__).joinpath("deploy.py").resolve()
pyinf = "pyinfra --dry" if args.dry_run else "pyinfra"
ssh_host = args.config.mail_domain if not args.ssh_host else args.ssh_host

cmd = f"{pyinf} --ssh-user root {ssh_host} {deploy_path} -y"
if sshexec == "localhost":
cmd = f"{pyinf} @local {deploy_path} -y"
Comment on lines +106 to +107
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a good quick fix, maybe we can add something like this to the cmdeploy dns calls as well, so they don't try to request DNS records from the chatmail relay itself, but from the local (docker) host.


if version.parse(pyinfra.__version__) < version.parse("3"):
out.red("Please re-run scripts/initenv.sh to update pyinfra to version 3.")
return 1
Expand Down Expand Up @@ -362,6 +366,9 @@ def main(args=None):

def get_sshexec():
host = args.ssh_host if hasattr(args, "ssh_host") and args.ssh_host else args.config.mail_domain
if host in [ "@local", "localhost" ]:
return "localhost"

print(f"[ssh] login to {host}")
return SSHExec(host, verbose=args.verbose)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,6 @@ RUN echo 'APT::Install-Recommends "0";' > /etc/apt/apt.conf.d/01norecommend && \

RUN apt-get update && \
apt-get install -y \
openssh-client \
openssh-server \
git \
python3 \
python3-venv \
Expand Down Expand Up @@ -54,23 +52,6 @@ RUN apt-get update && \
done \
&& rm -rf /var/lib/apt/lists/*

RUN systemctl enable \
ssh \
fcgiwrap

RUN sed -i 's/^#PasswordAuthentication .*/PasswordAuthentication no/' /etc/ssh/sshd_config && \
sed -i 's/^#PermitRootLogin .*/PermitRootLogin prohibit-password/' /etc/ssh/sshd_config && \
ssh-keygen -P "" -t rsa -b 2048 -f /root/.ssh/id_rsa && \
mkdir -p /root/.ssh && \
cat /root/.ssh/id_rsa.pub >> /root/.ssh/authorized_keys && \
SSH_USER_CONFIG="/root/.ssh/config" && \
echo "Host localhost" > "$SSH_USER_CONFIG" && \
echo " HostName localhost" >> "$SSH_USER_CONFIG" && \
echo " User root" >> "$SSH_USER_CONFIG" && \
echo " StrictHostKeyChecking no" >> "$SSH_USER_CONFIG" && \
echo " UserKnownHostsFile /dev/null" >> "$SSH_USER_CONFIG"
## TODO: deny access for all insteed root form 127.0.0.1 https://unix.stackexchange.com/a/406264

WORKDIR /opt/chatmail

ARG SETUP_CHATMAIL_SERVICE_PATH=/lib/systemd/system/setup_chatmail.service
Expand Down
15 changes: 8 additions & 7 deletions docker/docker-compose-default.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ services:
chatmail:
build:
context: ./docker
dockerfile: chatmail_server.dockerfile
dockerfile: chatmail_relay.dockerfile
tags:
- chatmail-relay:latest
image: chatmail-relay:latest
Expand All @@ -20,28 +20,29 @@ services:
max-size: "10m"
max-file: "3"
environment:
MAIL_DOMAIN: <your_domain>
MAIL_DOMAIN: $MAIL_DOMAIN
CHANGE_KERNEL_SETTINGS: "False"
ACME_EMAIL: <your_email>

MAX_MESSAGE_SIZE: "50M"
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"
- "443:443"
volumes:
## system
- /sys/fs/cgroup:/sys/fs/cgroup:rw # required for systemd
- ./:/opt/chatmail
- ./data/acme:/var/lib/acme

## data
- ./data/chatmail:/home
Expand Down
45 changes: 33 additions & 12 deletions docker/docker-compose-traefik.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ services:
chatmail:
build:
context: ./docker
dockerfile: chatmail_server.dockerfile
dockerfile: chatmail_relay.dockerfile
tags:
- chatmail-relay:latest
image: chatmail-relay:latest
Expand All @@ -26,22 +26,24 @@ services:
# 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_CONTAINER: $PATH_TO_SSL_CONTAINER
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
- ${PATH_TO_SSL_HOST}:${PATH_TO_SSL_CONTAINER}:ro
- ${CERTS_ROOT_DIR_HOST}:${CERTS_ROOT_DIR_CONTAINER}:ro

## data
- ./data/chatmail:/home
Expand All @@ -67,6 +69,22 @@ services:
- 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 &&
sudo chown 0:0 ./acme.json &&
sudo chmod 600 ./acme.json'
volumes:
- ./traefik/data:/app

traefik:
image: traefik:v3.3
container_name: traefik
Expand All @@ -77,17 +95,20 @@ services:
max-size: "10m"
max-file: "3"
command:
- --configFile=/config.yaml
- "--configFile=/config.yaml"
- "--certificatesresolvers.letsEncrypt.acme.email=${ACME_EMAIL:[email protected]}"
# 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
- ./data/traefik/config.yaml:/config.yaml
- ./data/traefik/acme.json:/acme.json
- ./data/traefik/dynamic-configs:/dynamic/conf

network_mode: host
- ./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
Expand All @@ -110,6 +131,6 @@ services:
environment:
CERTS_DIR: /data/letsencrypt/certs
volumes:
- ./data/traefik/letsencrypt:/data/letsencrypt
- ./data/traefik/acme.json:/data/acme.json
- ./data/traefik/post-hook.sh:/post-hook.sh
- ./traefik/data/letsencrypt:/data/letsencrypt
- ./traefik/data/acme.json:/data/acme.json
- ./traefik/post-hook.sh:/post-hook.sh
5 changes: 3 additions & 2 deletions docker/example.env
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
MAIL_DOMAIN="chat.example.com"
ACME_EMAIL="[email protected]"

PATH_TO_SSL_HOST="/opt/traefik/data/letsencrypt/certs/${MAIL_DOMAIN}"
PATH_TO_SSL_CONTAINER="/var/lib/acme/live/${MAIL_DOMAIN}"
CERTS_ROOT_DIR_HOST="./traefik/data/letsencrypt/certs"
CERTS_ROOT_DIR_CONTAINER="/var/lib/acme/live"
12 changes: 8 additions & 4 deletions docker/files/entrypoint.sh
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
#!/bin/bash
set -eo pipefail

unlink /etc/nginx/sites-enabled/default

if [ "${USE_FOREIGN_CERT_MANAGER,,}" == "true" ]; then
if [ ! -f "$PATH_TO_SSL_CONTAINER/fullchain" ]; then
echo "Error: file '$PATH_TO_SSL_CONTAINER/fullchain' does not exist. Exiting..." > /dev/stderr
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_CONTAINER/privkey" ]; then
echo "Error: file '$PATH_TO_SSL_CONTAINER/privkey' does not exist. Exiting..." > /dev/stderr
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
Expand Down
14 changes: 9 additions & 5 deletions docker/files/setup_chatmail_docker.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ 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_CONTAINER="${PATH_TO_SSL_CONTAINER:-/var/lib/acme/live/${MAIL_DOMAIN}}"
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
Expand All @@ -19,7 +20,7 @@ debug_commands() {
}

calculate_hash() {
find "$PATH_TO_SSL_CONTAINER" -type f -exec sha1sum {} \; | sort | sha1sum | awk '{print $1}'
find "$PATH_TO_SSL" -type f -exec sha1sum {} \; | sort | sha1sum | awk '{print $1}'
}

monitor_certificates() {
Expand All @@ -35,8 +36,8 @@ monitor_certificates() {
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, restarting nginx, dovecot and postfix services."
systemctl restart nginx.service
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
Expand All @@ -61,12 +62,15 @@ 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 localhost --skip-dns-check
./scripts/cmdeploy run --ssh-host @local --skip-dns-check

echo "ForwardToConsole=yes" >> /etc/systemd/journald.conf
systemctl restart systemd-journald
Expand Down
Loading
Loading