Maintenance¶
Routine tasks that keep Mailyte healthy — schedule them and forget about them (mostly).
Maintenance Schedule¶
| Task | Frequency | Automated? | Downtime? |
|---|---|---|---|
| Log rotation | Daily | Yes | No |
| Database cleanup | Weekly | Yes (cron) | No |
| Queue flushing | As needed | No | No |
| Certificate renewal | Every 60-90 days | Yes (certbot) | No |
| Docker image updates | Monthly | No | Brief |
| OS security patches | Weekly | Yes (unattended-upgrades) | Maybe (reboot) |
| Disk usage check | Daily | Yes (alert) | No |
| Backup verification | Monthly | No | No |
Log Rotation¶
Docker logs can grow fast, especially for Postfix. Configure Docker's log driver to handle this automatically.
Docker Daemon Config¶
JSON
// /etc/docker/daemon.json
{
"log-driver": "json-file",
"log-opts": {
"max-size": "50m",
"max-file": "5"
}
}
This limits each container to 5 log files of 50MB each (250MB max per container).
Per-Service Log Limits¶
Override in Docker Compose for chatty services:
YAML
services:
postfix:
logging:
driver: json-file
options:
max-size: "100m"
max-file: "10"
api:
logging:
driver: json-file
options:
max-size: "50m"
max-file: "5"
Manual Log Cleanup¶
Bash
# See how much space logs are using
sudo du -sh /var/lib/docker/containers/*/
# Truncate a specific container's log (doesn't stop logging)
sudo truncate -s 0 /var/lib/docker/containers/<container-id>/<container-id>-json.log
Database Cleanup¶
Over time, MySQL accumulates data that's no longer needed — old logs, expired sessions, soft-deleted records.
Automated Cleanup Script¶
Bash
#!/bin/bash
# scripts/db-cleanup.sh
MYSQL_CMD="docker compose exec -T mysql mysql -u root -p${MYSQL_ROOT_PASSWORD} ${MYSQL_DATABASE}"
echo "$(date): Starting database cleanup"
# Remove expired sessions (older than 30 days)
$MYSQL_CMD -e "DELETE FROM sessions WHERE expires_at < NOW() - INTERVAL 30 DAY;"
# Clean up old API logs (older than 90 days)
$MYSQL_CMD -e "DELETE FROM api_logs WHERE created_at < NOW() - INTERVAL 90 DAY;"
# Remove old email tracking data (older than 180 days)
$MYSQL_CMD -e "DELETE FROM email_events WHERE created_at < NOW() - INTERVAL 180 DAY;"
# Clean up soft-deleted records (older than 30 days)
$MYSQL_CMD -e "DELETE FROM emails WHERE deleted_at IS NOT NULL AND deleted_at < NOW() - INTERVAL 30 DAY;"
# Optimize tables after large deletes
$MYSQL_CMD -e "OPTIMIZE TABLE sessions, api_logs, email_events, emails;"
echo "$(date): Database cleanup complete"
Schedule it:
Bash
# Run weekly on Sundays at 3 AM
echo "0 3 * * 0 root /opt/mailyte/scripts/db-cleanup.sh >> /var/log/mailyte-cleanup.log 2>&1" \
| sudo tee /etc/cron.d/mailyte-cleanup
Queue Management¶
Viewing the Queue¶
Bash
# See what's in the Postfix queue
docker compose exec postfix postqueue -p
# Count queued messages
docker compose exec postfix postqueue -p | grep -c "^[A-F0-9]"
# See the deferred queue
docker compose exec postfix postqueue -p | grep -c "MAILER-DAEMON"
Flushing the Queue¶
Bash
# Retry all deferred messages now
docker compose exec postfix postqueue -f
# Delete all messages in the queue (nuclear option)
docker compose exec postfix postsuper -d ALL
# Delete only deferred messages
docker compose exec postfix postsuper -d ALL deferred
# Delete messages to a specific domain
docker compose exec postfix mailq | grep "example.com" | awk '{print $1}' | \
xargs -I {} docker compose exec postfix postsuper -d {}
Warning: Only flush the full queue if you know what you're doing. If delivery is failing for a reason (DNS issue, blocked IP), flushing just generates more bounces.
Worker Queue Management¶
Bash
# Check worker queue depth
docker compose exec redis redis-cli -a $REDIS_PASSWORD llen email_queue
docker compose exec redis redis-cli -a $REDIS_PASSWORD llen retry_queue
docker compose exec redis redis-cli -a $REDIS_PASSWORD llen dead_letter_queue
# Clear the dead letter queue
docker compose exec redis redis-cli -a $REDIS_PASSWORD del dead_letter_queue
# Requeue dead letters for retry
docker compose exec redis redis-cli -a $REDIS_PASSWORD --pipe <<'EOF'
RPOPLPUSH dead_letter_queue retry_queue
EOF
Certificate Renewal¶
Automated (Recommended)¶
Bash
# Certbot auto-renewal with post-hook to reload services
sudo certbot renew --deploy-hook "
docker compose -f /opt/mailyte/docker-compose.yml exec postfix postfix reload
docker compose -f /opt/mailyte/docker-compose.yml exec dovecot doveadm reload
"
Manual Renewal¶
Bash
# Renew the certificate
sudo certbot renew
# Restart services to pick up the new cert
docker compose restart postfix dovecot
# Verify the new certificate
echo | openssl s_client -connect mail.yourdomain.com:993 2>/dev/null \
| openssl x509 -noout -dates
Docker Updates¶
Updating Mailyte Images¶
Bash
# Pull the latest images
docker compose pull
# Restart with new images (one at a time for zero downtime)
docker compose up -d --no-deps postfix
docker compose up -d --no-deps dovecot
docker compose up -d --no-deps api
docker compose up -d --no-deps worker
# Or all at once (brief downtime)
docker compose up -d
Cleaning Up Old Images¶
Bash
# Remove unused images
docker image prune -a --filter "until=168h" # Older than 7 days
# Full cleanup (images, containers, volumes, networks)
docker system prune -a --volumes # CAREFUL: removes unused volumes too
# Safer: just dangling images and stopped containers
docker system prune
OS Updates¶
Bash
# Install security updates only
sudo apt update && sudo apt upgrade -y --only-upgrade
# Set up automatic security updates
sudo apt install -y unattended-upgrades
sudo dpkg-reconfigure -plow unattended-upgrades
Disk Space Management¶
Bash
# Check overall disk usage
df -h
# Docker-specific usage
docker system df
# Find the biggest offenders
sudo du -sh /var/lib/docker/volumes/* | sort -rh | head -10
# Mail storage per user (if using Maildir)
du -sh /var/mail/*/Maildir/ | sort -rh | head -20
When Disk Gets Low¶
- Clean Docker:
docker system prune - Rotate logs: truncate or delete old log files
- Clean mail queue:
docker compose exec postfix postsuper -d ALL deferred - Run database cleanup:
./scripts/db-cleanup.sh - Check mail storage: enforce user quotas if not already set
- Add more disk: expand the volume or add a new one
Health Check Script¶
Run this as part of your weekly maintenance:
Bash
#!/bin/bash
# scripts/weekly-check.sh
echo "=== Mailyte Weekly Health Check ==="
echo "Date: $(date)"
echo ""
echo "--- Services ---"
docker compose ps --format "table {{.Name}}\t{{.Status}}"
echo ""
echo "--- Disk Usage ---"
df -h / /var/lib/docker 2>/dev/null
echo ""
echo "--- Docker Resources ---"
docker system df
echo ""
echo "--- Mail Queue ---"
docker compose exec -T postfix postqueue -p | tail -1
echo ""
echo "--- Database Size ---"
docker compose exec -T mysql mysql -u root -p"${MYSQL_ROOT_PASSWORD}" -e "
SELECT table_schema AS 'Database',
ROUND(SUM(data_length + index_length) / 1024 / 1024, 2) AS 'Size (MB)'
FROM information_schema.tables
GROUP BY table_schema;" 2>/dev/null
echo ""
echo "--- Certificate Expiry ---"
echo | openssl s_client -connect localhost:993 2>/dev/null \
| openssl x509 -noout -enddate
echo ""
echo "--- Backup Status ---"
ls -lh /opt/mailyte/backups/ | tail -5