Runbook: Fixing Disk Space Issues on a Discourse Multisite Server

Outcome: Identify and remove the causes of disk space exhaustion on a Discourse multisite server, restore automated backups, and prevent recurrence.

When to use: Automated backups are failing, or df -h shows disk usage above ~75%.


:magnifying_glass_tilted_left: Step 1: Assess Overall Disk Usage

SSH into your multisite server, then run:

df -h

Shows all filesystems and their usage. Focus on the /dev/sda1 line — that’s your main disk. If it’s above 80% you have a problem.

du -sh /* 2>/dev/null | sort -rh | head -20

Finds the top space consumers at the root level. On a Discourse multisite, /var will almost always be the biggest by far.


:magnifying_glass_tilted_left: Step 2: Drill Into Discourse’s Shared Folder

du -sh /var/discourse/shared/*/

Shows space used by each subfolder under shared/. You’ll typically see web-only/ dominating.

du -sh /var/discourse/shared/web-only/backups/*/
du -sh /var/discourse/shared/web-only/uploads/*/

Breaks down space per site, by database name (not domain name — see Runbook 2 for the mapping). Backups are almost always the culprit.

docker system df

Shows how much space Docker images, containers, and build cache are using.


:world_map: Step 3: Map DB Names to Domains

The folder names under backups/ and uploads/ correspond to the database key names in your multisite.yml, not the domain names. Check:

cat /var/discourse/containers/app.yml

Look for the before_bundle_exec block containing the multisite.yml contents. Each entry looks like:

opensource:           # ← this is the folder name you'll see in backups/ and uploads/
  database: opensource_discourse
  host_names:
    - digitallysovereign.org   # ← this is the actual domain

:information_source: See Auditing and Removing Sites on a Discourse Multisite for a full process of mapping and auditing all sites.


:wastebasket: Step 4: Delete Files for Removed/Dead Sites

Once you’ve confirmed a site is no longer needed (see Runbook 2), delete its files immediately — no rebuild required, space is freed instantly:

rm -rf /var/discourse/shared/web-only/backups/DBNAME/
rm -rf /var/discourse/shared/web-only/uploads/DBNAME/

Then verify:

df -h /

:warning: Before deleting backups for a site you’ve migrated elsewhere, confirm the new server has a working recent backup first!


:file_cabinet: Step 5: Drop Orphaned Databases

After removing a site’s files, drop its PostgreSQL database to recover additional space. Enter the data container:

cd /var/discourse
./launcher enter data

Switch to the postgres OS user (required due to peer authentication — you cannot just pass -U postgres as root):

su - postgres
psql

Then drop the databases:

\l   -- list all databases first to confirm
DROP DATABASE dbname_discourse;
\l   -- confirm it's gone
\q

Then exit twice to get back to the host.


:spouting_whale: Step 6: Prune Docker

Remove unused images, stopped containers, and build cache:

docker system prune -a

Typically recovers 1-2GB. Note: all images marked as “reclaimable” will be deleted, but they’ll be re-pulled automatically on the next ./launcher rebuild app.


:wrench: Step 7: Rebuild the App Container

After editing app.yml (to remove dead sites from the multisite config), rebuild:

cd /var/discourse && ./launcher rebuild app

This applies your config changes, re-pulls Docker images, and runs database migrations for remaining sites. Expect 5-15 minutes downtime.


:shield: Best Practices to Prevent Recurrence

Practice How
Set backup retention limits In each site’s Admin → Backups → “Maximum backups” — 5 to 7 is a good default
Offload backups to external storage Configure S3-compatible backup destination, or rsync to your Hetzner BX11 storage box after each backup via cron
Monitor disk usage Set up a cron job or use Discourse’s built-in admin dashboard warnings
Clean up test sites promptly When you’re done with a test/sandbox site, remove it fully (see Runbook 2) — don’t leave it accumulating backups
Check disk usage monthly Just run df -h and du -sh /var/discourse/shared/web-only/backups/*/ — takes 30 seconds

(created with AI)