Skip to content

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.

  • Postfix stops accepting mail — temporarily. Its SMTP server refuses new messages with 452 4.3.1 Insufficient system storage once 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.
  1. Find which filesystem is full, and what is on it.

    Terminal window
    df -h / /opt/mailserver /var/lib/docker
    docker system df -v
    du -sh /opt/mailserver/backups/* 2>/dev/null

    docker system df -v lists every volume with its size; the stack’s are named mailserver_<volume>. Then look inside the large one:

    Volume / path Holds How to measure
    mailserver_vmail Every mailbox (/var/vmail/<domain>/<user>/Maildir), Sieve scripts, and the full-text search indexes (fts-flatcurve directories inside each Maildir) docker exec mailserver-dovecot-1 sh -c 'du -sh /var/vmail/*/* | sort -h | tail -20' and docker exec mailserver-dovecot-1 doveadm quota get -A
    mailserver_mailqueue Postfix’s queue (/var/spool/postfix) docker exec mailserver-postfix-1 du -sh /var/spool/postfix; docker exec mailserver-postfix-1 postqueue -p | tail -1
    mailserver_pgdata The 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" (your DB_ADMIN_USER and DB_NAME if you changed them)
    mailserver_maillog Postfix’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 days docker exec mailserver-postfix-1 du -sh /var/log/mail
    mailserver_migration imapsync’s per-mailbox cache while migrations run docker system df -v
    /opt/mailserver/backups mailctl backup’s default output, and a pre-upgrade backup from every mailctl upgrade. The product never deletes one du -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 until mailctl upgrade recreates its containers du -sh /var/lib/docker/containers/*/*-json.log | sort -h | tail; docker inspect --format '{{.Name}} {{.LogPath}}' $(docker ps -q) maps each to its container

    The remaining volumes — mailconfig, certs, caddy_data, caddy_config — are small. Old images from earlier releases show under Images in docker system df.

  2. 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 --out points), 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 in maillog are 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, then docker image rm <image> for old tags. Never during an upgrade.
    • The oldest rotated mail logs, postfix.log.<date>.gz and dovecot.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 .env and 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 pgdata by hand — pg_wal above 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 rm or docker volume prune / docker system prune --volumes on this host: each can destroy mail, the database or the queue, and the queue is not in any backup;
    • delete mailconfig, certs or caddy_data: they hold the live daemon configuration and the TLS certificates.
  3. 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 with mailctl doctor that every service is running and healthy and that postfix → postgres and dovecot → postgres pass (Doctor). Mail deferred in the meantime is retried on Postfix’s schedule.

  4. 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’s logging: per service overrides Docker’s own log-opts). Set mailbox quotas so one mailbox cannot fill the volume: a quota is per mailbox, with a default per domain.

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.

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.