The disk is full
When to use: the dashboard shows a disk alert; df -h shows a filesystem at or near 100 %; mailctl doctor reports a disk ⚠; mailctl backup or mailctl upgrade fails for want of space; senders get 452 4.3.1 Insufficient system storage.
Preconditions: root on the host.
The worker checks the disk holding the volumes every 10 minutes and raises a disk_low alert on the dashboard: a warning below 10 % or 2 GiB free, critical below 5 % or 512 MiB, whichever trips first. It closes by itself once space is freed. It measures from inside its own container, through one of the stack’s named volumes, so it answers for every volume on a default install, but not for a volume moved to another filesystem by hand. It also notifies nobody, so it is seen when somebody opens the dashboard. The alert arrives with 0.12.1. mailctl doctor warns below 10 GiB free on the filesystem holding the install directory (/opt/mailserver) and, when it is another one, on Docker’s data root (/var/lib/docker unless configured otherwise), where the stack’s volumes and container logs live: its docker disk row. Before 0.12.1 it measured only the install directory, so check both by hand there.
What happens when the disk fills
Section titled “What happens when the disk fills”- Postfix stops accepting mail — temporarily. Its SMTP server refuses new messages with
452 4.3.1 Insufficient system storageonce free space on the queue’s filesystem falls below 1.5 × the message size limit (75 MiB with the default 50 MiB limit). Senders retry, so inbound mail is delayed, not lost, for as long as their own retry window lasts (usually days). Mail already in the queue stays there. - Dovecot cannot store new mail or write its indexes and logs, so delivery from Postfix fails and IMAP sessions may error. Check the Delivery log for inbound bounces from the period once space is back.
- Postgres cannot write. Postfix and Dovecot read domains and mailboxes from it, so if Postgres stops, mail stops: Postfix answers with temporary lookup failures and Dovecot cannot authenticate anybody. The API and the worker (delivery log, alerts, containment) fail with it.
- Redis writes nothing to disk (
--save '',--appendonly no); it is unaffected.
-
Find which filesystem is full, and what is on it.
Terminal window df -h / /opt/mailserver /var/lib/dockerdocker system df -vdu -sh /opt/mailserver/backups/* 2>/dev/nulldocker system df -vlists every volume with its size; the stack’s are namedmailserver_<volume>. Then look inside the large one:Volume / path Holds How to measure mailserver_vmailEvery mailbox ( /var/vmail/<domain>/<user>/Maildir), Sieve scripts, and the full-text search indexes (fts-flatcurvedirectories inside each Maildir)docker exec mailserver-dovecot-1 sh -c 'du -sh /var/vmail/*/* | sort -h | tail -20'anddocker exec mailserver-dovecot-1 doveadm quota get -Amailserver_mailqueuePostfix’s queue ( /var/spool/postfix)docker exec mailserver-postfix-1 du -sh /var/spool/postfix;docker exec mailserver-postfix-1 postqueue -p | tail -1mailserver_pgdataThe database: domains, mailboxes, the delivery log, the audit log, configuration history docker exec mailserver-postgres-1 psql -U mail -d mail -c "SELECT relname, pg_size_pretty(pg_total_relation_size(relid)) FROM pg_statio_user_tables ORDER BY pg_total_relation_size(relid) DESC LIMIT 10"(yourDB_ADMIN_USERandDB_NAMEif you changed them)mailserver_maillogPostfix’s and Dovecot’s logs ( /var/log/mail), rotated daily (Dovecot’s also past 64 MiB), rotations pruned by the postfix and dovecot containers after 7 daysdocker exec mailserver-postfix-1 du -sh /var/log/mailmailserver_migrationimapsync’s per-mailbox cache while migrations run docker system df -v/opt/mailserver/backupsmailctl backup’s default output, and a pre-upgrade backup from everymailctl upgrade. The product never deletes onedu -sh /opt/mailserver/backups/*Container logs ( /var/lib/docker/containers/…)Each container’s stdout. Postfix and Dovecot mirror their log files there. Each is capped at 3 files of 10 MB since 0.12.1; a stack installed earlier keeps growing untilmailctl upgraderecreates its containersdu -sh /var/lib/docker/containers/*/*-json.log | sort -h | tail;docker inspect --format '{{.Name}} {{.LogPath}}' $(docker ps -q)maps each to its containerThe remaining volumes —
mailconfig,certs,caddy_data,caddy_config— are small. Old images from earlier releases show under Images indocker system df. -
Free space with what is safe to remove, roughly in order of how much it usually gives back:
- Old backups in
/opt/mailserver/backups(or wherever--outpoints), once copies of the ones you keep are off the host. A backup on the same disk as the server protects against little anyway (Backup & restore). - Container logs. Truncate a large one in place:
truncate -s 0 <path from docker inspect>. Postfix’s and Dovecot’s own log files inmaillogare kept separately, so this loses no mail log the delivery log depends on — but in a security incident, copy them first (Security incident). - Full-text search indexes. They are derived from the mail and rebuilt on demand:
docker exec mailserver-dovecot-1 doveadm fts-flatcurve remove -A '*'deletes all of them; search keeps working, unindexed and slower, until they are rebuilt (Webmail search and list performance). Rebuilding needs the space back, so do this to recover, not as a fix. - Spam in the queue. If one sender filled the queue, delete their messages on the Mail queue page after you are sure (Outgoing mail is deferring → step 6).
- Images of releases you no longer run:
docker image ls, thendocker image rm <image>for old tags. Never during an upgrade. - The oldest rotated mail logs,
postfix.log.<date>.gzanddovecot.log.<date>in/var/log/mail. Keep the newest rotation of each: the worker may still be reading it into the delivery log and the sign-in ban counter. - The delivery log’s retention. Lower
DELIVERY_LOG_RETENTION_DAYS(default 30) in.envand recreate the worker (docker compose -f /opt/mailserver/compose/compose.yaml --env-file /opt/mailserver/.env up -d worker); it prunes on its next run. Postgres reuses the freed space rather than returning it to the filesystem. - AI’s records. Lower
AI_LOG_RETENTION_DAYS(default 30) the same way: the AI call log, the copilot’s tool calls and its proposals are pruned past it, hourly.
Never:
- delete files inside
pgdataby hand —pg_walabove all; a database with missing WAL does not start; - delete files from a Maildir by hand to make room — that is a user’s mail. Have the owner delete or archive mail, lower quotas for the future, or move the data to a bigger disk;
- delete files from the queue directory by hand — use the Mail queue page or
postsuper; - run
docker compose down -v,docker volume rmordocker volume prune/docker system prune --volumeson this host: each can destroy mail, the database or the queue, and the queue is not in any backup; - delete
mailconfig,certsorcaddy_data: they hold the live daemon configuration and the TLS certificates.
- Old backups in
-
Check the daemons came back. Postfix resumes accepting mail by itself once space is free. Postgres, if it stopped, is restarted by Docker (
restart: unless-stopped); confirm withmailctl doctorthat every service is running and healthy and thatpostfix → postgresanddovecot → postgrespass (Doctor). Mail deferred in the meantime is retried on Postfix’s schedule. -
Stop it happening again. Grow the disk, or move Docker’s data root to a larger one. Rotate and ship backups off the host on a schedule. Upgrade, which caps every container’s log at 3 files of 10 MB (
compose.yaml’slogging:per service overrides Docker’s ownlog-opts). Set mailbox quotas so one mailbox cannot fill the volume: a quota is per mailbox, with a default per domain.
Verification
Section titled “Verification”df -h shows headroom on every filesystem in step 1; mailctl doctor has no ✖ and no disk ⚠; a test message from outside arrives; mailctl backup completes.
Rollback
Section titled “Rollback”Deleted backups, logs and queued messages cannot be restored. Search indexes rebuild with docker exec mailserver-dovecot-1 doveadm index -A '*' (Webmail search and list performance). A changed DELIVERY_LOG_RETENTION_DAYS or AI_LOG_RETENTION_DAYS is reverted in .env with the same recreate, but rows already pruned are gone.