Skip to content

Add installation using Docker Compose and many other improvements for implementation in Docker. #614

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -164,3 +164,9 @@ cython_debug/
#.idea/

chatmail.zone

# docker
/data/
/custom/
docker-compose.yaml
.env
29 changes: 29 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,35 @@

## untagged

- 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 markdown tabs blocks for rendering multilingual pages. Add russian language support to `index.md`, `privacy.md`, and `info.md`.
([#614](https://github.com/chatmail/relay/pull/614))

- 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`.
([#614](https://github.com/chatmail/relay/pull/614))

- 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.
([#614](https://github.com/chatmail/relay/pull/614))

- Add startup for `fcgiwrap.service` because sometimes it did not start automatically.
([#614](https://github.com/chatmail/relay/pull/614))

- 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`.
([#614](https://github.com/chatmail/relay/pull/614))

- Add configuration parameters
([#614](https://github.com/chatmail/relay/pull/614)):
- `is_development_instance` - Indicates that this instance is installed as a temporary/test one (default: `True`)
- `use_foreign_cert_manager` - Use a third-party certificate manager instead of acmetool (default: `False`)
- `acme_email` - Email address used by acmetool to obtain Let's Encrypt certificates (default: empty)
- `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`)

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

Expand Down
12 changes: 8 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,22 +74,23 @@ Please substitute it with your own domain.
```
git clone https://github.com/chatmail/relay
cd relay
scripts/initenv.sh
```

3. On your local PC, create chatmail configuration file `chatmail.ini`:
### Manual installation
1. On your local PC, create chatmail configuration file `chatmail.ini`:

```
scripts/initenv.sh
scripts/cmdeploy init chat.example.org # <-- use your domain
```

4. Verify that SSH root login to your remote server works:
2. Verify that SSH root login to your remote server works:

```
ssh [email protected] # <-- use your domain
```

5. From your local PC, deploy the remote chatmail relay server:
3. From your local PC, deploy the remote chatmail relay server:

```
scripts/cmdeploy run
Expand All @@ -99,6 +100,9 @@ Please substitute it with your own domain.
which you should configure at your DNS provider
(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

To check the status of your remotely running chatmail service:
Expand Down
7 changes: 7 additions & 0 deletions chatmaild/src/chatmaild/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ 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.filtermail_smtp_port = int(params["filtermail_smtp_port"])
self.filtermail_smtp_port_incoming = int(
params["filtermail_smtp_port_incoming"]
Expand All @@ -43,6 +44,12 @@ 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.acme_email = params["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"
if "iroh_relay" not in params:
self.iroh_relay = "https://" + params["mail_domain"]
Expand Down
19 changes: 19 additions & 0 deletions chatmaild/src/chatmaild/ini/chatmail.ini.f
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@
# Deployment Details
#

# if set to "True" on main page will be showed dev banner
is_development_instance = True

# SMTP outgoing filtermail and reinjection
filtermail_smtp_port = 10080
postfix_reinject_port = 10025
Expand All @@ -60,6 +63,22 @@
# if set to "True" IPv6 is disabled
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

# 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".
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
Expand Down
1 change: 1 addition & 0 deletions cmdeploy/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ dependencies = [
"pytest-xdist",
"execnet",
"imap_tools",
"pymdown-extensions",
]

[project.scripts]
Expand Down
50 changes: 31 additions & 19 deletions cmdeploy/src/cmdeploy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from pyinfra.api import FactBase
from pyinfra.facts.files import File
from pyinfra.facts.server import Sysctl
from pyinfra.facts.systemd import SystemdEnabled
from pyinfra.facts.systemd import SystemdEnabled, SystemdStatus
from pyinfra.operations import apt, files, pip, server, systemd

from .acmetool import deploy_acmetool
Expand Down Expand Up @@ -395,20 +395,21 @@ def _configure_dovecot(config: Config, debug: bool = False) -> bool:
config=config,
)

# as per https://doc.dovecot.org/configuration_manual/os/
# as per https://doc.dovecot.org/2.3/configuration_manual/os/
# it is recommended to set the following inotify limits
for name in ("max_user_instances", "max_user_watches"):
key = f"fs.inotify.{name}"
if host.get_fact(Sysctl)[key] > 65535:
# Skip updating limits if already sufficient
# (enables running in incus containers where sysctl readonly)
continue
server.sysctl(
name=f"Change {key}",
key=key,
value=65535,
persist=True,
)
if config.change_kernel_settings:
for name in ("max_user_instances", "max_user_watches"):
key = f"fs.inotify.{name}"
if host.get_fact(Sysctl)[key] == config.fs_inotify_max_user_instances_and_watchers:
# Skip updating limits if already sufficient
# (enables running in incus containers where sysctl readonly)
continue
server.sysctl(
name=f"Change {key}",
key=key,
value=config.fs_inotify_max_user_instances_and_watchers,
persist=True,
)

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

process_on_53 = host.get_fact(Port, port=53)
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 All @@ -700,10 +703,12 @@ def deploy_chatmail(config_path: Path, disable_mail: bool) -> None:
deploy_iroh_relay(config)

# Deploy acmetool to have TLS certificates.
tls_domains = [mail_domain, f"mta-sts.{mail_domain}", f"www.{mail_domain}"]
deploy_acmetool(
domains=tls_domains,
)
if not config.use_foreign_cert_manager:
tls_domains = [mail_domain, f"mta-sts.{mail_domain}", f"www.{mail_domain}"]
deploy_acmetool(
email = config.acme_email,
domains=tls_domains,
)

apt.packages(
# required for setfacl for echobot
Expand Down Expand Up @@ -783,6 +788,13 @@ def deploy_chatmail(config_path: Path, disable_mail: bool) -> None:
enabled=True,
restarted=nginx_need_restart,
)

systemd.service(
name="Start and enable fcgiwrap",
service="fcgiwrap.service",
running=True,
enabled=True,
)

# This file is used by auth proxy.
# https://wiki.debian.org/EtcMailName
Expand Down
55 changes: 43 additions & 12 deletions cmdeploy/src/cmdeploy/cmdeploy.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,17 +32,28 @@ def init_cmd_options(parser):
action="store",
help="fully qualified DNS domain name for your chatmail instance",
)
parser.add_argument(
"--force",
dest="recreate_ini",
action="store_true",
help="force reacreate ini file",
)


def init_cmd(args, out):
"""Initialize chatmail config file."""
mail_domain = args.chatmail_domain
inipath = args.inipath
if args.inipath.exists():
print(f"Path exists, not modifying: {args.inipath}")
return 1
else:
write_initial_config(args.inipath, mail_domain, overrides={})
out.green(f"created config file for {mail_domain} in {args.inipath}")
if not args.recreate_ini:
out.green(f"[WARNING] Path exists, not modifying: {inipath}")
return 0
else:
out.yellow(f"[WARNING] Force argument was provided, deleting config file: {inipath}")
inipath.unlink()

write_initial_config(inipath, mail_domain, overrides={})
out.green(f"created config file for {mail_domain} in {inipath}")


def run_cmd_options(parser):
Expand All @@ -63,16 +74,23 @@ def run_cmd_options(parser):
dest="ssh_host",
help="specify an SSH host to deploy to; uses mail_domain from chatmail.ini by default",
)
parser.add_argument(
"--skip-dns-check",
dest="dns_check_disabled",
action="store_true",
help="disable checks nslookup for dns",
)


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

sshexec = args.get_sshexec()
require_iroh = args.config.enable_iroh_relay
remote_data = dns.get_initial_remote_data(sshexec, args.config.mail_domain)
if not dns.check_initial_remote_data(remote_data, print=out.red):
return 1
if not args.dns_check_disabled:
remote_data = dns.get_initial_remote_data(sshexec, args.config.mail_domain)
if not dns.check_initial_remote_data(remote_data, print=out.red):
return 1

env = os.environ.copy()
env["CHATMAIL_INI"] = args.inipath
Expand All @@ -89,6 +107,9 @@ def run_cmd(args, out):
try:
retcode = out.check_call(cmd, env=env)
if retcode == 0:
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.")
elif not remote_data["acme_account_url"]:
out.red("Deploy completed but letsencrypt not configured")
Expand Down Expand Up @@ -251,8 +272,17 @@ def red(self, msg, file=sys.stderr):
def green(self, msg, file=sys.stderr):
print(colored(msg, "green"), file=file)

def __call__(self, msg, red=False, green=False, file=sys.stdout):
color = "red" if red else ("green" if green else None)
def yellow(self, msg, file=sys.stderr):
print(colored(msg, "yellow"), file=file)

def __call__(self, msg, red=False, green=False, yellow=False, file=sys.stdout):
color = None
if red:
color = "red"
elif green:
color = "green"
elif yellow:
color = "yellow"
print(colored(msg, color), file=file)

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

def get_sshexec():
print(f"[ssh] login to {args.config.mail_domain}")
return SSHExec(args.config.mail_domain, verbose=args.verbose)
host = args.ssh_host if hasattr(args, "ssh_host") and args.ssh_host else args.config.mail_domain
print(f"[ssh] login to {host}")
return SSHExec(host, verbose=args.verbose)

args.get_sshexec = get_sshexec

Expand Down
3 changes: 2 additions & 1 deletion cmdeploy/src/cmdeploy/www.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ def prepare_template(source):
assert source.exists(), source
render_vars = {}
render_vars["pagename"] = "home" if source.stem == "index" else source.stem
render_vars["markdown_html"] = markdown.markdown(source.read_text())
# tabs usage for multiple languages https://facelessuser.github.io/pymdown-extensions/extensions/blocks/plugins/tab/
render_vars["markdown_html"] = markdown.markdown(source.read_text(), extensions=['pymdownx.blocks.tab'])
page_layout = source.with_name("page-layout.html").read_text()
return render_vars, page_layout

Expand Down
Loading