feat: with an off-site destination set up, database backups stream to it
CI and images / lint (push) Successful in 2s
CI and images / extension-version (push) Successful in 1s
CI and images / extension-test (push) Successful in 20s
CI and images / frontend-build (push) Successful in 23s
CI and images / backend-lint-and-test (push) Successful in 41s
CI and images / integration (push) Successful in 2m25s
CI and images / sign-extension (push) Successful in 2s
CI and images / build-agent (push) Successful in 5s
CI and images / build-web (push) Successful in 1m50s
CI and images / smoke-web (push) Successful in 49s
CI and images / promote (push) Successful in 2s
CI and images / refresh-dev (push) Skipped
CI and images / unattended-failure (push) Skipped

The nightly database backup (and "Run DB backup now") now sends pg_dump's
output straight to the off-site bucket as a multipart upload, with no copy
staged on this server. The object is named database/fabledcurator-<UTC
time>.dump and appears only once pg_dump has exited cleanly and every part
has landed; a failed dump aborts the upload and records pg_dump's last line.
Without a destination, dumps are written to /images/_backups as before.

- backup_s3: Upload (start / add part / complete / abort), with a busy or
  silent bucket rested and asked again (15 s, 60 s, Retry-After obeyed), and
  a Pacer that holds uploads to the new bandwidth limit.
- Settings: "Upload limit (Mbit/s)" on the destination card, 200 by default,
  0 for none (migration 0120).
- pg_dump runs under nice (and ionice where installed), locally too.
- Backups card says where dumps go; the runs table gains "Saved to" and
  shows a failed run's error. Off-site rows have no restore/tag/delete here:
  restoring from the bucket is a later step (#6880).

Task #6878, milestone 586.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-06 21:11:12 -05:00
co-authored by Claude Opus 5.5
parent bb10d30b2d
commit 772482d794
14 changed files with 749 additions and 29 deletions
@@ -4,11 +4,18 @@
title="Backups"
blurb="Database + image backups, schedules and restore."
>
<p class="fc-muted text-body-2 mb-4">
<p v-if="offsite" class="fc-muted text-body-2 mb-4">
Database dumps stream to the <strong>{{ offsite.bucket }}</strong> bucket
at {{ offsite.endpoint }}, with no copy kept on this server; the bucket's
own retention thins old dumps. The images backup below still writes a
tar+zstd to <code>/images/_backups/</code>, by hand only.
</p>
<p v-else class="fc-muted text-body-2 mb-4">
pg_dump for the database (fast, nightly-schedulable);
tar+zstd for the images (slow, manual only). Files live in
<code>/images/_backups/</code>. Tag a backup to protect it
from autoprune.
from autoprune. Set up an off-site destination above to send
database dumps off this server instead.
</p>
<v-alert
@@ -40,6 +47,9 @@
@update:model-value="onSettingChange('backup_db_keep_last_n', Number($event))"
/>
</div>
<p v-if="offsite" class="fc-muted text-caption mb-3">
Keep last applies to this list and to any dumps still on this server.
</p>
<v-btn
color="accent" variant="flat" rounded="pill"
prepend-icon="mdi-database-arrow-up" class="mb-3"
@@ -110,6 +120,11 @@ const settings = computed(() => store.settings || {
backup_images_keep_last_n: 3,
})
// The off-site destination, once it is fully set up: database dumps go there.
const offsite = computed(() => (
store.destination?.configured ? store.destination : null
))
const dbTriggering = ref(false)
const imagesTriggering = ref(false)
const confirmOpen = ref(false)
@@ -127,6 +142,7 @@ function pollOnce() {
onMounted(() => {
store.loadSettings()
if (!store.destination) store.loadDestination().catch(() => {})
pollOnce()
pollId = setInterval(pollOnce, 5000)
})