Most "run your own help desk" guides stop at docker compose up. The interesting part starts three months later, when the disk fills up or a restore has to actually work.
Here is the unglamorous version, for a team of three to ten.
What actually consumes disk
| Component | Growth | Notes |
|---|---|---|
| Message bodies + metadata | slow | thousands of tickets ≈ a few hundred MB/year |
| Attachments | fast | screenshots and PDFs dominate |
| Search index | medium | rebuildable, can be excluded from backups |
| Logs | slow but relentless | cap at 30 days, rotate |
| Database WAL / binlogs | bursty | the usual suspect when disk "suddenly" fills |
Rule of thumb: plan for attachments to be 10× the database. A 2 GB database usually comes with a 20 GB attachment store.
A sizing that survives a year
For 5 agents, ~60 tickets/day, average 1.5 MB of attachments per ticket:
60 × 1.5 MB × 365 ≈ 33 GB/year of attachments
database + index ≈ 3-5 GB/year
logs (capped) ≈ 2 GB
------------------------------------------
one year ≈ 38-40 GB
So: 80 GB of disk with retention pruning, or 200 GB if you keep attachments forever. Both are one cheap VPS.
The three cron jobs that matter
# 1. logical backup, compressed, off the same disk
0 2 * * * pg_dump -Fc app | gzip > /backup/db-$(date +%F).dump.gz
# 2. attachments: rsync to a second location, not a tarball
30 2 * * * rsync -a --delete /data/attachments/ /backup/attachments/
# 3. prune, with a count in the log so you can prove it ran
0 3 * * * find /backup -name 'db-*.dump.gz' -mtime +14 -delete | wc -l
Three details that save you later:
-
pg_dump -Fc, not a raw file copy. A copied data directory from a running server is not a backup. - rsync attachments, do not tarball them. Restoring one file should not require unpacking 20 GB.
- Log the count. When someone asks "is the backup running?", the log answers.
The restore drill
Once a quarter, on a scratch machine:
- Create an empty database,
pg_restorelast night's dump - Point the app at it, start it
- Open a six-month-old thread, download one attachment, send one test reply
- Note the elapsed time
If that takes more than an hour, your restore procedure is a project, not a procedure.
Mail is the part people forget
The app is not the only thing that breaks. If inbound mail flows through the same box, an outage means customers get bounces and you learn about it two days later.
Two options that both work:
- MX at an independent provider, forwarding into the app over IMAP/SMTP. Mail keeps arriving during app downtime.
- SMTP relay for outbound only, so deliverability does not depend on your server's IP reputation.
Do not put inbound mail on a hostname whose DNS lives inside the app server.
Backups are not the same as portability
A database dump restores your installation. Portability means you can also leave:
- A per-ticket export (JSON or CSV with bodies and attachment paths)
- Attachments stored under stable, guessable paths
- No proprietary blob format for the search index
Test the export once. If it produces a zip you can open on a laptop, you are not locked in.
What I would not bother with at this size
- Kubernetes. One compose file and a systemd unit is easier to debug at 2 a.m.
- Read replicas. At 10 agents, the database is idle most of the day.
- Object storage for attachments unless you already run it — a mounted disk with rsync is fine, and simpler to restore.
- Elasticsearch. Postgres full-text search handles a few hundred thousand messages without complaint.
The minimum viable runbook
One page, printed and taped inside a cupboard:
- Where the server is, and its IP (not only the hostname)
- Where DNS is managed, and who can change it
- How to restart the app, and how to tell it worked
- Where backups land, and how to restore one
- The escalation channel when the app is down
If the person who set it up is on holiday, that page is the difference between a 20-minute fix and a lost day.
Onsites AI ships as a compose stack — app, Postgres, queue worker, optional mail fetcher — with the self-hosting notes covering exactly the parts above. Free for the first three seats if you want to measure your own ticket volume before sizing anything.
Top comments (0)