Skip to content

Commit 3826de8

Browse files
author
Keonik1
committed
Add installation via docker compose (MVP 1)
- Add markdown tabs blocks - Fix [Issue 604](#604) - Add `--skip-dns-check` argument to `cmdeploy run` command - Add `--force` argument to `cmdeploy init` command - Add startup for `fcgiwrap.service` - Add extended check when installing `unbound.service` - Add configuration parameters (`is_development_instance`, `use_foreign_cert_manager`, `acme_email`, `change_kernel_settings`, `fs_inotify_max_user_instances_and_watchers`)
1 parent 577c04d commit 3826de8

24 files changed

+1535
-39
lines changed

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,3 +164,9 @@ cython_debug/
164164
#.idea/
165165

166166
chatmail.zone
167+
168+
# docker
169+
/data/
170+
/custom/
171+
docker-compose.yaml
172+
.env

CHANGELOG.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,35 @@
22

33
## untagged
44

5+
- Add installation via docker compose (MVP 1). The instructions, known issues and limitations are located in `/docs`
6+
([#614](https://github.com/chatmail/relay/pull/614))
7+
8+
- Add markdown tabs blocks for rendering multilingual pages. Add russian language support to `index.md`, `privacy.md`, and `info.md`.
9+
([#614](https://github.com/chatmail/relay/pull/614))
10+
11+
- Fix [Issue 604](https://github.com/chatmail/relay/issues/604), now the `--ssh_host` argument of the `cmdeploy run` command works correctly and does not depend on `config.mail_domain`.
12+
([#614](https://github.com/chatmail/relay/pull/614))
13+
14+
- Add `--skip-dns-check` argument to `cmdeploy run` command, which disables DNS record checking before installation.
15+
([#614](https://github.com/chatmail/relay/pull/614))
16+
17+
- Add `--force` argument to `cmdeploy init` command, which recreates the `config.ini` file.
18+
([#614](https://github.com/chatmail/relay/pull/614))
19+
20+
- Add startup for `fcgiwrap.service` because sometimes it did not start automatically.
21+
([#614](https://github.com/chatmail/relay/pull/614))
22+
23+
- Add extended check when installing `unbound.service`. Now, if it is not shown who exactly is occupying port 53, but `unbound.service` is running, it is considered that the port is occupied by `unbound.service`.
24+
([#614](https://github.com/chatmail/relay/pull/614))
25+
26+
- Add configuration parameters
27+
([#614](https://github.com/chatmail/relay/pull/614)):
28+
- `is_development_instance` - Indicates that this instance is installed as a temporary/test one (default: `True`)
29+
- `use_foreign_cert_manager` - Use a third-party certificate manager instead of acmetool (default: `False`)
30+
- `acme_email` - Email address used by acmetool to obtain Let's Encrypt certificates (default: empty)
31+
- `change_kernel_settings` - Whether to change kernel parameters during installation (default: `True`)
32+
- `fs_inotify_max_user_instances_and_watchers` - Value for kernel parameters `fs.inotify.max_user_instances` and `fs.inotify.max_user_watches` (default: `65535`)
33+
534
- Expire push notification tokens after 90 days
635
([#583](https://github.com/chatmail/relay/pull/583))
736

README.md

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,22 +74,23 @@ Please substitute it with your own domain.
7474
```
7575
git clone https://github.com/chatmail/relay
7676
cd relay
77-
scripts/initenv.sh
7877
```
7978

80-
3. On your local PC, create chatmail configuration file `chatmail.ini`:
79+
### Manual installation
80+
1. On your local PC, create chatmail configuration file `chatmail.ini`:
8181

8282
```
83+
scripts/initenv.sh
8384
scripts/cmdeploy init chat.example.org # <-- use your domain
8485
```
8586

86-
4. Verify that SSH root login to your remote server works:
87+
2. Verify that SSH root login to your remote server works:
8788

8889
```
8990
ssh root@chat.example.org # <-- use your domain
9091
```
9192

92-
5. From your local PC, deploy the remote chatmail relay server:
93+
3. From your local PC, deploy the remote chatmail relay server:
9394

9495
```
9596
scripts/cmdeploy run
@@ -99,6 +100,9 @@ Please substitute it with your own domain.
99100
which you should configure at your DNS provider
100101
(it can take some time until they are public).
101102

103+
### Docker installation
104+
Installation using docker compose is presented [here](./docs/DOCKER_INSTALLATION_EN.md)
105+
102106
### Other helpful commands
103107

104108
To check the status of your remotely running chatmail service:

chatmaild/src/chatmaild/config.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ def __init__(self, inipath, params):
3333
self.password_min_length = int(params["password_min_length"])
3434
self.passthrough_senders = params["passthrough_senders"].split()
3535
self.passthrough_recipients = params["passthrough_recipients"].split()
36+
self.is_development_instance = params.get("is_development_instance", "true").lower() == "true"
3637
self.filtermail_smtp_port = int(params["filtermail_smtp_port"])
3738
self.filtermail_smtp_port_incoming = int(
3839
params["filtermail_smtp_port_incoming"]
@@ -43,6 +44,12 @@ def __init__(self, inipath, params):
4344
)
4445
self.mtail_address = params.get("mtail_address")
4546
self.disable_ipv6 = params.get("disable_ipv6", "false").lower() == "true"
47+
self.use_foreign_cert_manager = params.get("use_foreign_cert_manager", "false").lower() == "true"
48+
self.acme_email = params["acme_email"]
49+
self.change_kernel_settings = params.get("change_kernel_settings", "true").lower() == "true"
50+
self.fs_inotify_max_user_instances_and_watchers = int(
51+
params["fs_inotify_max_user_instances_and_watchers"]
52+
)
4653
self.imap_rawlog = params.get("imap_rawlog", "false").lower() == "true"
4754
if "iroh_relay" not in params:
4855
self.iroh_relay = "https://" + params["mail_domain"]

chatmaild/src/chatmaild/ini/chatmail.ini.f

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@
4949
# Deployment Details
5050
#
5151

52+
# if set to "True" on main page will be showed dev banner
53+
is_development_instance = True
54+
5255
# SMTP outgoing filtermail and reinjection
5356
filtermail_smtp_port = 10080
5457
postfix_reinject_port = 10025
@@ -60,6 +63,22 @@
6063
# if set to "True" IPv6 is disabled
6164
disable_ipv6 = False
6265

66+
# if you set "True", acmetool will not be installed and you will have to manage certificates yourself.
67+
use_foreign_cert_manager = False
68+
69+
# Your email adress, which will be used in acmetool to manage Let's Encrypt SSL certificates. Required if `use_foreign_cert_manager` param set as "False".
70+
acme_email =
71+
72+
#
73+
# Kernel settings
74+
#
75+
76+
# if you set "True", the kernel settings will be configured according to the values below
77+
change_kernel_settings = True
78+
79+
# change fs.inotify.max_user_instances and fs.inotify.max_user_watches kernel settings
80+
fs_inotify_max_user_instances_and_watchers = 65535
81+
6382
# Defaults to https://iroh.{{mail_domain}} and running `iroh-relay` on the chatmail
6483
# service.
6584
# If you set it to anything else, the service will be disabled

cmdeploy/pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ dependencies = [
2020
"pytest-xdist",
2121
"execnet",
2222
"imap_tools",
23+
"pymdown-extensions",
2324
]
2425

2526
[project.scripts]

cmdeploy/src/cmdeploy/__init__.py

Lines changed: 31 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from pyinfra.api import FactBase
1616
from pyinfra.facts.files import File
1717
from pyinfra.facts.server import Sysctl
18-
from pyinfra.facts.systemd import SystemdEnabled
18+
from pyinfra.facts.systemd import SystemdEnabled, SystemdStatus
1919
from pyinfra.operations import apt, files, pip, server, systemd
2020

2121
from .acmetool import deploy_acmetool
@@ -395,20 +395,21 @@ def _configure_dovecot(config: Config, debug: bool = False) -> bool:
395395
config=config,
396396
)
397397

398-
# as per https://doc.dovecot.org/configuration_manual/os/
398+
# as per https://doc.dovecot.org/2.3/configuration_manual/os/
399399
# it is recommended to set the following inotify limits
400-
for name in ("max_user_instances", "max_user_watches"):
401-
key = f"fs.inotify.{name}"
402-
if host.get_fact(Sysctl)[key] > 65535:
403-
# Skip updating limits if already sufficient
404-
# (enables running in incus containers where sysctl readonly)
405-
continue
406-
server.sysctl(
407-
name=f"Change {key}",
408-
key=key,
409-
value=65535,
410-
persist=True,
411-
)
400+
if config.change_kernel_settings:
401+
for name in ("max_user_instances", "max_user_watches"):
402+
key = f"fs.inotify.{name}"
403+
if host.get_fact(Sysctl)[key] == config.fs_inotify_max_user_instances_and_watchers:
404+
# Skip updating limits if already sufficient
405+
# (enables running in incus containers where sysctl readonly)
406+
continue
407+
server.sysctl(
408+
name=f"Change {key}",
409+
key=key,
410+
value=config.fs_inotify_max_user_instances_and_watchers,
411+
persist=True,
412+
)
412413

413414
timezone_env = files.line(
414415
name="Set TZ environment variable",
@@ -676,8 +677,10 @@ def deploy_chatmail(config_path: Path, disable_mail: bool) -> None:
676677
from cmdeploy.cmdeploy import Out
677678

678679
process_on_53 = host.get_fact(Port, port=53)
680+
if host.get_fact(SystemdStatus, services="unbound").get("unbound.service"):
681+
process_on_53 = "unbound"
679682
if process_on_53 not in (None, "unbound"):
680-
Out().red(f"Can't install unbound: port 53 is occupied by: {process_on_53}")
683+
Out().red(f"Can't install unbound: port 53 is occupied by: {process_on_53}")
681684
exit(1)
682685
apt.packages(
683686
name="Install unbound",
@@ -700,10 +703,12 @@ def deploy_chatmail(config_path: Path, disable_mail: bool) -> None:
700703
deploy_iroh_relay(config)
701704

702705
# Deploy acmetool to have TLS certificates.
703-
tls_domains = [mail_domain, f"mta-sts.{mail_domain}", f"www.{mail_domain}"]
704-
deploy_acmetool(
705-
domains=tls_domains,
706-
)
706+
if not config.use_foreign_cert_manager:
707+
tls_domains = [mail_domain, f"mta-sts.{mail_domain}", f"www.{mail_domain}"]
708+
deploy_acmetool(
709+
email = config.acme_email,
710+
domains=tls_domains,
711+
)
707712

708713
apt.packages(
709714
# required for setfacl for echobot
@@ -783,6 +788,13 @@ def deploy_chatmail(config_path: Path, disable_mail: bool) -> None:
783788
enabled=True,
784789
restarted=nginx_need_restart,
785790
)
791+
792+
systemd.service(
793+
name="Start and enable fcgiwrap",
794+
service="fcgiwrap.service",
795+
running=True,
796+
enabled=True,
797+
)
786798

787799
# This file is used by auth proxy.
788800
# https://wiki.debian.org/EtcMailName

cmdeploy/src/cmdeploy/cmdeploy.py

Lines changed: 43 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -32,17 +32,28 @@ def init_cmd_options(parser):
3232
action="store",
3333
help="fully qualified DNS domain name for your chatmail instance",
3434
)
35+
parser.add_argument(
36+
"--force",
37+
dest="recreate_ini",
38+
action="store_true",
39+
help="force reacreate ini file",
40+
)
3541

3642

3743
def init_cmd(args, out):
3844
"""Initialize chatmail config file."""
3945
mail_domain = args.chatmail_domain
46+
inipath = args.inipath
4047
if args.inipath.exists():
41-
print(f"Path exists, not modifying: {args.inipath}")
42-
return 1
43-
else:
44-
write_initial_config(args.inipath, mail_domain, overrides={})
45-
out.green(f"created config file for {mail_domain} in {args.inipath}")
48+
if not args.recreate_ini:
49+
out.green(f"[WARNING] Path exists, not modifying: {inipath}")
50+
return 0
51+
else:
52+
out.yellow(f"[WARNING] Force argument was provided, deleting config file: {inipath}")
53+
inipath.unlink()
54+
55+
write_initial_config(inipath, mail_domain, overrides={})
56+
out.green(f"created config file for {mail_domain} in {inipath}")
4657

4758

4859
def run_cmd_options(parser):
@@ -63,16 +74,23 @@ def run_cmd_options(parser):
6374
dest="ssh_host",
6475
help="specify an SSH host to deploy to; uses mail_domain from chatmail.ini by default",
6576
)
77+
parser.add_argument(
78+
"--skip-dns-check",
79+
dest="dns_check_disabled",
80+
action="store_true",
81+
help="disable checks nslookup for dns",
82+
)
6683

6784

6885
def run_cmd(args, out):
6986
"""Deploy chatmail services on the remote server."""
7087

7188
sshexec = args.get_sshexec()
7289
require_iroh = args.config.enable_iroh_relay
73-
remote_data = dns.get_initial_remote_data(sshexec, args.config.mail_domain)
74-
if not dns.check_initial_remote_data(remote_data, print=out.red):
75-
return 1
90+
if not args.dns_check_disabled:
91+
remote_data = dns.get_initial_remote_data(sshexec, args.config.mail_domain)
92+
if not dns.check_initial_remote_data(remote_data, print=out.red):
93+
return 1
7694

7795
env = os.environ.copy()
7896
env["CHATMAIL_INI"] = args.inipath
@@ -89,6 +107,9 @@ def run_cmd(args, out):
89107
try:
90108
retcode = out.check_call(cmd, env=env)
91109
if retcode == 0:
110+
server_deployed_message = f"Chatmail server started: https://{args.config.mail_domain}/"
111+
delimiter_line = "=" * len(server_deployed_message)
112+
out.green(f"{delimiter_line}\n{server_deployed_message}\n{delimiter_line}")
92113
out.green("Deploy completed, call `cmdeploy dns` next.")
93114
elif not remote_data["acme_account_url"]:
94115
out.red("Deploy completed but letsencrypt not configured")
@@ -251,8 +272,17 @@ def red(self, msg, file=sys.stderr):
251272
def green(self, msg, file=sys.stderr):
252273
print(colored(msg, "green"), file=file)
253274

254-
def __call__(self, msg, red=False, green=False, file=sys.stdout):
255-
color = "red" if red else ("green" if green else None)
275+
def yellow(self, msg, file=sys.stderr):
276+
print(colored(msg, "yellow"), file=file)
277+
278+
def __call__(self, msg, red=False, green=False, yellow=False, file=sys.stdout):
279+
color = None
280+
if red:
281+
color = "red"
282+
elif green:
283+
color = "green"
284+
elif yellow:
285+
color = "yellow"
256286
print(colored(msg, color), file=file)
257287

258288
def check_call(self, arg, env=None, quiet=False):
@@ -331,8 +361,9 @@ def main(args=None):
331361
return parser.parse_args(["-h"])
332362

333363
def get_sshexec():
334-
print(f"[ssh] login to {args.config.mail_domain}")
335-
return SSHExec(args.config.mail_domain, verbose=args.verbose)
364+
host = args.ssh_host if hasattr(args, "ssh_host") and args.ssh_host else args.config.mail_domain
365+
print(f"[ssh] login to {host}")
366+
return SSHExec(host, verbose=args.verbose)
336367

337368
args.get_sshexec = get_sshexec
338369

cmdeploy/src/cmdeploy/www.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ def prepare_template(source):
2525
assert source.exists(), source
2626
render_vars = {}
2727
render_vars["pagename"] = "home" if source.stem == "index" else source.stem
28-
render_vars["markdown_html"] = markdown.markdown(source.read_text())
28+
# tabs usage for multiple languages https://facelessuser.github.io/pymdown-extensions/extensions/blocks/plugins/tab/
29+
render_vars["markdown_html"] = markdown.markdown(source.read_text(), extensions=['pymdownx.blocks.tab'])
2930
page_layout = source.with_name("page-layout.html").read_text()
3031
return render_vars, page_layout
3132

0 commit comments

Comments
 (0)