Running yarilo with Docker Compose
A single-host standalone deployment for local development, evaluation and small self-hosted installs. It mirrors the standalone topology from the Helm chart, on one host, with Docker Compose.
Clients connect to login proxies, which terminate TLS and forward to the session backends. The backends share a Maildir store and coordinate through yarilo-auth, yarilo-warden and yarilo-locks (Redis-backed). One image serves every role — each container picks its component via YARILO_.
WARNING
Single-host, not highly available. For HA or multi-node, use the Helm chart — see Deployment.
Everything lives in deploy/.
Services
| Group | Containers |
|---|---|
| Infra | redis, yarilo-auth (userdb), yarilo-warden, yarilo-locks |
| Session backends | yarilo-imap, yarilo-pop3, yarilo-lmtp, yarilo-submission, yarilo-managesieve, yarilo-jmap |
| Full-text search | yarilo-fts, the search index service the sessions dial at yarilo-fts:9106 |
| Login proxies (TLS) | yarilo-imap-login, yarilo-pop3-login, yarilo-submission-login, yarilo-lmtp-login, yarilo-managesieve-login, yarilo-jmap-login |
| MTA integration | yarilo-sasl-login (SASL auth for Postfix), yarilo-quota-status (quota policy) |
| Operator API | yarilo-backend-api, the API yarctl backend talks to (not published on the host) |
The userdb is SQLite — yarilo-auth owns it in the state volume; mail lives in the shared mail volume. No external database is required.
Prerequisites
- Docker Engine 24+ with the Compose v2 plugin (
docker compose version) - Free host ports: 143/993, 110/995, 587/465, 4190, 8443, and — loopback-only by default — 24, 12345, 12340. Override in
..env - ~512 MB RAM
Quickstart
cd deploy/compose
cp .env.example .env
sed -i.bak "s/^BACKEND_API_TOKEN=.*/BACKEND_API_TOKEN=$(openssl rand -hex 32)/" .env
./gen-certs.sh mail.example.test
docker compose up -d
docker compose psThis sequence:
- Copies the default environment; adjust the image tag or ports in
.if neededenv - Sets
BACKEND_, the bearer token ofAPI_ TOKEN yarilo-backend-api; compose refuses to start without it - Generates a self-signed TLS certificate for local use
- Starts all containers;
docker compose psshould report every one ashealthy
Creating the first user
The SQLite userdb lives in the state volume at /. yarilo-auth creates the yarilo_ table on first start.
Insert a user with a one-off sqlite container:
docker run --rm -v yarilo_state:/data nouchka/sqlite3 /data/users.db \
"INSERT INTO yarilo_users (username,password,home,mail,enabled) VALUES \
('[email protected]','{PLAIN}changeit', \
'/var/mail/vhosts/example.test/[email protected]', \
'maildir:/var/mail/vhosts/example.test/[email protected]',1);"The mail column is the per-user mail_ (maildir: / sdbox: / mdbox:).
TIP
{PLAIN} is fine for local testing only. Use a hashed scheme for real use.
Sending and reading a test message
Deliver a message via LMTP on the loopback port, as your MTA would:
printf 'LHLO t\r\nMAIL FROM:<[email protected]>\r\nRCPT TO:<[email protected]>\r\nDATA\r\nSubject: hi\r\n\r\nhello\r\n.\r\nQUIT\r\n' \
| nc 127.0.0.1 24Read it back over IMAPS — with the self-signed certificate the verify warning is expected:
printf 'a login [email protected] changeit\r\nb select INBOX\r\nc logout\r\n' \
| openssl s_client -quiet -connect 127.0.0.1:993Listening ports
| Service | Host port | Notes |
|---|---|---|
| IMAP / IMAPS | 143 / 993 | login proxy, STARTTLS / implicit TLS |
| POP3 / POP3S | 110 / 995 | login proxy |
| Submission | 587 / 465 | login proxy |
| ManageSieve | 4190 | login proxy |
| JMAP | 8443 | login proxy, HTTPS |
| LMTP | 24 (loopback) | unauthenticated — for your MTA only |
| SASL auth | 12345 (loopback) | Postfix smtpd_ |
| Quota policy | 12340 (loopback) | Postfix check_ |
TLS
For local use, . writes a self-signed tls/ and tls/. The key is world-readable so the uid-1000 container can load it.
For production, drop a real certificate and key (e.g. Let's Encrypt fullchain. / privkey.) into deploy/ as cert. / key. and restart the login proxies. The tls/ directory is git-ignored.
Fronting MTA (Postfix)
Inbound mail reaches yarilo over LMTP (port 24); outbound is your MTA's job.
Wire Postfix to yarilo:
- Deliver local recipients:
mailbox_transport = lmtp:inet:127. 0. 0. 1:24 - SMTP AUTH against yarilo:
smtpd_,sasl_ type = dovecot smtpd_sasl_ path = inet:127. 0. 0. 1:12345 - Reject over-quota recipients:
check_inpolicy_ service inet:127. 0. 0. 1:12340 smtpd_recipient_ restrictions - Mail that Sieve sends (redirect, vacation, notify, report): set
sieve.to your MTA. The shippedsieve_ submission_ host localhost:25points inside the container, where no MTA listens, so those messages fail until it is changed.
::: note For a public deployment you also need MX, SPF, DKIM (signed at the MTA) and PTR records. :::
Verifying
Check that every container reports healthy:
docker compose psCheck the IMAP/POP3/Submission/ManageSieve greetings with openssl s_ or nc, or run the smoke test. The entrypoint dispatches on YARILO_, so invoke the binary directly:
docker compose exec yarilo-imap \
yarilo-smoketest -host yarilo-imap-login -imap-port 10993 \
-telemetry http://yarilo-imap-login:8080 -insecure=trueCheck the operator API. Once the user exists, this answers with the user's home, mail paths and namespaces:
docker compose exec yarilo-backend-api yarctl backend user info [email protected]Operations
To follow logs of any service:
docker compose logs -f yarilo-imapSet LOG_ in . for verbose output.
To upgrade, bump YARILO_ in ., then:
docker compose pull && docker compose up -dTo back up, snapshot the mail (messages + indexes) and state (userdb) volumes.
For the admin CLI, run yarctl inside yarilo-backend-api. Its environment already points yarctl backend … at the API and carries the token:
docker compose exec yarilo-backend-api yarctl backend ...Migrate mailbox formats with yarilo-migrate.
Troubleshooting
| Symptom | Check |
|---|---|
| a container is unhealthy | docker compose logs <svc>; is tls/ present and readable? |
| client connects but gets no greeting | the matching login proxy AND its backend are up; both share the compose network |
| auth always fails | user row exists with enabled=1; password has a {SCHEME} prefix |
| 0 messages for a user | the mail column driver (maildir/sdbox/mdbox) matches how mail was delivered |
DNS / network is unreachable between services | recreate cleanly: docker compose down && docker compose up -d (stale network) |