https://github.com/chatmail/cmlxc
local chatmail container management and testing
https://github.com/chatmail/cmlxc
Last synced: about 1 month ago
JSON representation
local chatmail container management and testing
- Host: GitHub
- URL: https://github.com/chatmail/cmlxc
- Owner: chatmail
- License: mpl-2.0
- Created: 2026-04-12T07:39:56.000Z (about 2 months ago)
- Default Branch: main
- Last Pushed: 2026-04-20T13:30:30.000Z (about 1 month ago)
- Last Synced: 2026-04-20T13:46:05.054Z (about 1 month ago)
- Language: Python
- Size: 186 KB
- Stars: 1
- Watchers: 1
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- License: LICENSE
Awesome Lists containing this project
README
# cmlxc -- local chatmail container management and testing
Manage local [Incus](https://linuxcontainers.org/incus/) containers for
chatmail relay development and testing.
`cmlxc` spins up lightweight LXC containers,
deploys chatmail relay services into them via `cmdeploy` or `madmail`,
and runs integration tests -- all without touching the host system.
See [Architecture](#architecture) for more internal details.
## Prerequisites
[Incus](https://linuxcontainers.org/incus/) installed and configured on the host.
Usually only being part of the "incus" group is necessary,
as containers can run with user privileges.
> [!TIP]
> On Debian or Ubuntu, it is recommended to use the
> [Zabbly Incus repository](https://github.com/zabbly/incus)
> to ensure you have a recent version.
You can verify your incus installation like this:
incus launch images:debian/12 local-my-setup
If this command fails, please check the incus documentation.
If you get an error about "Failed instance creation",
you might be running into https://github.com/lxc/incus/issues/916
and need to ensure there is no component (mullvad) for example,
that messes up container networking.
## Installation
With pip:
python -m venv venv
source venv/bin/activate
pip install cmlxc
Or with [uv](https://docs.astral.sh/uv/):
uv venv venv
source venv/bin/activate
uv pip install cmlxc
## Usage
**Initialize the environment** (base image, DNS container, builder container):
cmlxc init
Re-initialize from scratch (destroys everything first):
cmlxc init --reset
**Deploy chatmail relays** (creates containers if needed, then deploys).
The `--source` argument controls where the code comes from:
cmlxc deploy-cmdeploy --source @main cm0
cmlxc deploy-madmail --source @main mad1
cmlxc deploy-madmail --source @main --with-webadmin mad1
cmlxc deploy-madmail --source @main --ipv4-only mad1
| Form | Meaning |
|---------|---------|
| `@ref` | Clone default remote at branch/tag `ref` |
| `/path` or `./path` | Sync from a local checkout |
| `URL@ref` | Clone a custom remote at `ref` |
Examples with local checkouts or feature branches:
cmlxc deploy-cmdeploy --source ../relay cm0
cmlxc deploy-madmail --source @lmtp-rework mad0
cmlxc deploy-cmdeploy --source @fix-dovecot cm1
Each `deploy-*` invocation initialises the driver's source in the
builder (wipe-and-reclone).
**Run integration tests** inside the builder:
cmlxc test-mini cm0
cmlxc test-mini cm0 cm1 # cross-relay tests (domain-based)
cmlxc test-mini cm0 mad1 # cross-relay tests (mixed)
cmlxc test-cmdeploy cm0 cm1
cmlxc test-madmail mad1
**SSH into a deployed relay:**
ssh -F ~/.config/cmlxc/ssh-config cm0
**Lifecycle commands:**
cmlxc status # show all containers
cmlxc status cm0 # show only cm0
cmlxc status cm0 mad1 # show multiple containers
cmlxc status --host # show DNS/SSH setup instructions
cmlxc start cm0 # restart a stopped relay
cmlxc stop cm0 cm1 # stop relays
cmlxc destroy cm0 # stop + delete
cmlxc destroy --all # destroy relays, keep DNS/builder
**Increase verbosity** with `-v` or `-vv`:
cmlxc deploy-cmdeploy --source @main -vv cm1
## Shell Completion
`cmlxc` supports Bash tab-completion for subcommands, options, and container names.
Enable for the **current session**:
```bash
eval "$(register-python-argcomplete cmlxc)"
```
Enable **permanently**:
```bash
activate-global-python-argcomplete --user
```
## Architecture
`cmlxc` manages four kinds of containers, each with a distinct role:
```
cmlxc init / deploy-* / test-*
|
v
+-----------------+ +------------------------+ +--------------------+
| ns-localchat | | builder-localchat | | relay containers |
| (PowerDNS) | | (repos, venvs, builds) | | (cm0, mad1, ...) |
+-----------------+ +------------------------+ +--------------------+
^ | ^
| DNS zones | SSH / SCP |
+------------------------+---------------------------+
```
**Base image** (`localchat-base`) -- a Debian 12 image with SSH and
Python pre-installed.
All other containers are launched from this image (or from a cached
relay image).
**DNS container** (`ns-localchat`) -- runs PowerDNS authoritative + recursor.
Provides `.localchat` DNS resolution so containers can reach each other by name.
**Builder container** (`builder-localchat`) -- the central workhorse.
Holds repository templates and per-relay checkouts,
Python virtualenvs for `cmdeploy` and mini-tests, and the compiled `maddy` binary.
All deployment and test operations are executed *inside* the builder --
the host only needs `cmlxc` itself.
**Relay containers** (e.g. `cm0-localchat`, `mad1-localchat`) --
ephemeral containers that receive a deployed chatmail service.
Each relay is locked to a single deployment driver (`cmdeploy` or
`madmail`); switching requires destroying and re-creating the container.
### Deployment drivers
Drivers live in `driver_cmdeploy.py` and `driver_madmail.py`.
Each driver module exports its CLI subcommand metadata,
builder init, and deploy orchestration.
`cli.py` generates the `deploy-*` subcommands from a `DRIVER_BY_NAME` mapping.
- **cmdeploy** -- runs `cmdeploy run` from the builder container over SSH
into the relay.
Generates DNS zones, loads them into PowerDNS, and verifies records.
After the first successful deploy the relay image is cached as
`localchat-cmdeploy` so subsequent containers start pre-populated.
- **madmail** -- builds the `maddy` Go binary inside the builder,
pushes it via SCP and runs `madmail install --simple --ip `.
No DNS entries are needed.
## Releasing
Versions are derived from git tags via `setuptools-git-versioning`.
The changelog is generated with [git-cliff](https://git-cliff.org/)
using the `cliff.toml` config in the repo root.
To make a new release, use the provided script:
./make_new_release.py
The script automates the following steps:
1. **Test** the codebase by running a full `tox` suite and functional
tests (`pytest tests/fullrun.py`).
2. **Preview** unreleased changes with `git cliff`.
3. **Tag** the release (suggesting automatic, micro, or minor bump).
4. **Generate** the full changelog into `CHANGELOG.md`.
5. **Edit** the changelog manually (opens your `$EDITOR`).
6. **Amend** the tag commit to include the changelog update.
7. **Force-tag** the amended commit.
After the script finishes, push the changes:
git push origin main --tags
The `release.yml` GitHub workflow triggers on pushed `v*` tags,
builds the sdist + wheel, and publishes to PyPI via trusted publishing (OIDC).