- Python 82%
- Smarty 11.2%
- Shell 3.5%
- Dockerfile 3.3%
| files | ||
| mail-sample | ||
| tests | ||
| tools | ||
| Containerfile | ||
| mailcontainer-sample.container | ||
| README.md | ||
| roundcube-sample.container | ||
Mail Container with Podman Quadlet
This repository builds a mail container with:
- OpenSMTPD for SMTP submission and relay
- Dovecot for IMAPS and ManageSieve
- Rspamd for filtering and DKIM signing
It is designed to run with Podman Quadlet. Sample Quadlet units are included in:
mailcontainer-sample.containerroundcube-sample.container
Table of contents
- What the container expects
- TLS certificate naming
- Getting certificates with Let's Encrypt
- Build the image
- Prepare the mail volume
- Mail users, aliases, and sender permissions
- Install the Quadlet unit
- Start with Quadlet
- DNS records
- Roundcube sample
- Notes
What the container expects
The mail container mounts a host directory on /etc/mail and keeps mailboxes in /var/vmail.
The sample Quadlet uses:
/opt/containers/mail/volumes/mail:/etc/mail/opt/containers/mail/volumes/vmail:/var/vmail
Inside the /etc/mail volume, create at least:
passwdvirtualsaliasessenderscerts/
On first start, the container will:
- require
MAIL_DOMAIN - create
/etc/mail/smtpd.conffromfiles/templates/smtpd.conf.tpl - create
/etc/mail/dovecot.conffromfiles/templates/dovecot.conf.tpl - generate a DKIM key in
/etc/mail/dkim/${MAIL_DOMAIN}.keyif it does not exist
TLS certificate naming
The SMTP and IMAP configs are generated from templates and require these exact file names:
/etc/mail/certs/${MAIL_DOMAIN}.pem/etc/mail/certs/${MAIL_DOMAIN}-key.pem
This is taken directly from files/templates/smtpd.conf.tpl and files/templates/dovecot.conf.tpl.
For MAIL_DOMAIN=example.com, the files must therefore be:
/etc/mail/certs/example.com.pem/etc/mail/certs/example.com-key.pem
If you use Let's Encrypt, the usual mapping is:
fullchain.pem->${MAIL_DOMAIN}.pemprivkey.pem->${MAIL_DOMAIN}-key.pem
You can copy or symlink them into the mounted mail volume. Example:
install -d /opt/containers/mail/volumes/mail/certs
cp /etc/letsencrypt/live/example.com/fullchain.pem \
/opt/containers/mail/volumes/mail/certs/example.com.pem
cp /etc/letsencrypt/live/example.com/privkey.pem \
/opt/containers/mail/volumes/mail/certs/example.com-key.pem
chmod 600 /opt/containers/mail/volumes/mail/certs/example.com-key.pem
If you prefer symlinks:
install -d /opt/containers/mail/volumes/mail/certs
ln -sf /etc/letsencrypt/live/example.com/fullchain.pem \
/opt/containers/mail/volumes/mail/certs/example.com.pem
ln -sf /etc/letsencrypt/live/example.com/privkey.pem \
/opt/containers/mail/volumes/mail/certs/example.com-key.pem
Getting certificates with Let's Encrypt
How you obtain the certificate is up to your environment. A common approach is certbot.
Example with standalone validation:
certbot certonly --standalone -d example.com
If your MX, IMAP, and submission hostnames are separate, request certificates for the names clients will use, then place the resulting files under the names expected by MAIL_DOMAIN.
Build the image
Build the local image referenced by the sample Quadlet:
podman build -t mail-container .
Prepare the mail volume
Create the required directories:
install -d /opt/containers/mail/volumes/mail
install -d /opt/containers/mail/volumes/mail/certs
install -d /opt/containers/mail/volumes/vmail
touch /opt/containers/mail/volumes/mail/passwd
touch /opt/containers/mail/volumes/mail/virtuals
touch /opt/containers/mail/volumes/mail/aliases
touch /opt/containers/mail/volumes/mail/senders
You can also start from the example files in mail-sample/.
Mail users, aliases, and sender permissions
This setup uses flat files mounted into /etc/mail:
passwdfor SMTP/IMAP authenticationvirtualsfor mailbox delivery and aliasessendersfor submission sender authorization
For routine management, use tools/mailctl.py:
uv run tools/mailctl.py --mail-dir /opt/containers/mail/volumes/mail add-user user@example.com
uv run tools/mailctl.py --mail-dir /opt/containers/mail/volumes/mail set-password user@example.com
uv run tools/mailctl.py --mail-dir /opt/containers/mail/volumes/mail add-alias alias@example.com user@example.com
uv run tools/mailctl.py --mail-dir /opt/containers/mail/volumes/mail remove-alias alias@example.com
uv run tools/mailctl.py --mail-dir /opt/containers/mail/volumes/mail allow-sender user@example.com @example.com
uv run tools/mailctl.py --mail-dir /opt/containers/mail/volumes/mail remove-sender user@example.com @example.com
If you do not want to install anything into the host Python environment, use uv run or uvx so the inline script dependency metadata pulls bcrypt automatically.
Warning: tools/mailctl.py is vibecoded. Read it before using it on production mail data and keep backups of your /etc/mail directory.
mailctl.py uses Python bcrypt with cost 12 to generate password hashes for passwd.
add-user prompts for a password, writes the bcrypt hash to passwd, creates the real mailbox mapping in virtuals, and grants the user permission to send as their own address in senders.
For example:
uv run tools/mailctl.py --mail-dir /opt/containers/mail/volumes/mail add-user paolo@example.com
This creates or updates:
passwd:paolo@example.com:<smtpctl-hash>virtuals:paolo@example.com vmailsenders:paolo@example.com paolo@example.com
passwd
The helper script hashes passwords directly with Python bcrypt:
uv run tools/mailctl.py --mail-dir /opt/containers/mail/volumes/mail add-user user@example.com
uv run tools/mailctl.py --mail-dir /opt/containers/mail/volumes/mail set-password user@example.com
If you want to manage the file manually, put the mailbox and the resulting bcrypt hash into /etc/mail/passwd.
Example:
user@example.com:$2b$12$.....................................................
admin@example.com:$2b$12$....................................................
Format:
email-address:encrypted-password
These users are used for authenticated SMTP submission and IMAP login. The hashes written by mailctl.py look like $2b$12$....
virtuals
Use /etc/mail/virtuals to map recipient addresses.
Examples:
user@example.com vmail
alias@example.com user@example.com
Meaning:
user@example.com vmailcreates a real mailbox account delivered into the virtual mail storealias@example.com user@example.commakesalias@example.coman alias that forwards locally touser@example.com
You can define multiple aliases pointing to the same destination mailbox.
With the helper script:
uv run tools/mailctl.py --mail-dir /opt/containers/mail/volumes/mail add-alias ciao@example.com paolo@example.com
uv run tools/mailctl.py --mail-dir /opt/containers/mail/volumes/mail remove-alias ciao@example.com
uv run tools/mailctl.py --mail-dir /opt/containers/mail/volumes/mail remove-alias ciao@example.com paolo@example.com
senders
Use /etc/mail/senders to define which authenticated user may send with which envelope sender addresses on the submission ports.
Examples:
user@example.com user@example.com
user@example.com @example.com
paolo@example.com paolo@example.com
paolo@example.com info@example.com
paolo@example.com hello@example.com
Meaning:
user@example.com user@example.comallowsuser@example.comto send asuser@example.comuser@example.com @example.comallowsuser@example.comto send as any address in@example.com- the three
paolo@example.com ...entries allowpaolo@example.comto send only as that explicit list of accounts:paolo@example.com,info@example.com, andhello@example.com
For a user limited to a small set of sender identities, add one line per allowed address.
With the helper script:
uv run tools/mailctl.py --mail-dir /opt/containers/mail/volumes/mail allow-sender paolo@example.com paolo@example.com
uv run tools/mailctl.py --mail-dir /opt/containers/mail/volumes/mail allow-sender paolo@example.com @example.com
uv run tools/mailctl.py --mail-dir /opt/containers/mail/volumes/mail allow-sender paolo@example.com info@example.com hello@example.com ciao@example.com
uv run tools/mailctl.py --mail-dir /opt/containers/mail/volumes/mail remove-sender paolo@example.com hello@example.com
Install the Quadlet unit
Install the unit system-wide:
sudo install -d /etc/containers/systemd
sudo cp mailcontainer-sample.container /etc/containers/systemd/mail.container
Edit the file and set at least:
Environment=MAIL_DOMAIN=example.comVolume=paths for your host- ports you want to publish
The sample publishes privileged ports (25, 465, 587, 993), so the documented setup assumes a system-wide Quadlet in /etc/containers/systemd/.
The sample unit exposes:
25SMTP587submission465SMTPS993IMAPS4190ManageSieve
Start with Quadlet
Reload system units and start the container:
sudo systemctl daemon-reload
sudo systemctl enable --now mail.service
Check logs:
sudo systemctl status mail.service
sudo journalctl -u mail.service -f
DNS records
This container signs outgoing mail with DKIM using selector main. That comes from files/templates/dkim_signing.conf.tpl.
On first boot, the container generates:
/etc/mail/dkim/${MAIL_DOMAIN}.key
You must publish the matching DKIM public key in DNS under:
main._domainkey.${MAIL_DOMAIN}
For MAIL_DOMAIN=example.com, the DNS record name is:
main._domainkey.example.com
The TXT value must look like this:
main._domainkey.example.com. IN TXT "v=DKIM1; k=rsa; p=BASE64_PUBLIC_KEY"
To derive BASE64_PUBLIC_KEY from the generated private key:
openssl rsa -in /opt/containers/mail/volumes/mail/dkim/example.com.key -pubout \
| grep -v '-----' \
| tr -d '\n'
Then insert that single-line value after p=.
Example:
main._domainkey.example.com. IN TXT "v=DKIM1; k=rsa; p=MIIBIjANBgkq..."
SPF
At minimum, publish an SPF record that authorizes your mail host to send mail for the domain.
If the same host that serves mail for example.com is also the MX, a simple starter record is:
example.com. IN TXT "v=spf1 mx -all"
If you prefer to authorize a specific IPv4 address directly:
example.com. IN TXT "v=spf1 ip4:203.0.113.10 -all"
Choose one SPF policy for the domain, not both in separate TXT SPF records.
DMARC
A conservative DMARC starting point is:
_dmarc.example.com. IN TXT "v=DMARC1; p=none; adkim=s; aspf=s; rua=mailto:dmarc@example.com"
That records failures without immediately quarantining or rejecting mail. Once DKIM and SPF alignment are verified, you can tighten the policy, for example:
_dmarc.example.com. IN TXT "v=DMARC1; p=quarantine; adkim=s; aspf=s; rua=mailto:dmarc@example.com"
Roundcube sample
If you also want webmail, copy roundcube-sample.container into /etc/containers/systemd/ and adjust:
ROUNDCUBEMAIL_DEFAULT_HOSTROUNDCUBEMAIL_SMTP_SERVER- published port
- persistent volume paths
Notes
MAIL_DOMAINis mandatory. The container exits if it is missing.- The TLS files must already exist before first successful startup.
- DKIM keys are generated automatically on first boot under
/etc/mail/dkim/. - The generated configs are only created if they do not already exist, so local edits in the mounted
/etc/mailvolume are preserved.