vortos / vortos-backup
Vortos Backup — enterprise-grade database backups: streamed pg_dump/pg_basebackup + WAL PITR and mongodump to an object store (R2), integrity-verified at creation, append-only catalog, GFS retention with hard safety floors, encryption-ready stream seam (Block 20), and a Block-17-ready alerting event
Requires
- php: >=8.2
- ext-hash: *
- doctrine/dbal: ^4.0
- psr/clock: ^1.0
- psr/log: ^3.0
- symfony/console: ^7.3
- symfony/dependency-injection: ^7.3
- vortos/vortos-foundation: ^1.0
- vortos/vortos-object-store: ^1.0
- vortos/vortos-ops-kit: ^1.0
Requires (Dev)
- phpunit/phpunit: ^11.0
Suggests
- ext-mongodb: For Mongo backups (requires the mongodump binary on the host)
- ext-pgsql: For Postgres backups (requires pg_dump/pg_basebackup binaries on the host)
- vortos/vortos-health: Surfaces the catalog-derived backup-freshness dead-man check as an off-gate /health/monitor probe, so stale backups are visible to an external uptime monitor
Provides
None
Conflicts
None
Replaces
None
- dev-main
- v1.0.0-alpha-428
- v1.0.0-alpha-427
- v1.0.0-alpha-426
- v1.0.0-alpha-425
- v1.0.0-alpha-424
- v1.0.0-alpha-423
- v1.0.0-alpha-422
- v1.0.0-alpha-421
- v1.0.0-alpha-420
- v1.0.0-alpha-419
- v1.0.0-alpha-418
- v1.0.0-alpha-417
- v1.0.0-alpha-416
- v1.0.0-alpha-415
- v1.0.0-alpha-414
- v1.0.0-alpha-413
- v1.0.0-alpha-412
- v1.0.0-alpha-411
- v1.0.0-alpha-410
- v1.0.0-alpha-409
- v1.0.0-alpha-408
- v1.0.0-alpha-407
- v1.0.0-alpha-406
- v1.0.0-alpha-405
- v1.0.0-alpha-404
- v1.0.0-alpha-403
- v1.0.0-alpha-402
- v1.0.0-alpha-401
- v1.0.0-alpha-400
- v1.0.0-alpha-399
- v1.0.0-alpha-398
- v1.0.0-alpha-397
- v1.0.0-alpha-396
- v1.0.0-alpha-395
- v1.0.0-alpha-394
- v1.0.0-alpha-393
- v1.0.0-alpha-392
- v1.0.0-alpha-391
- v1.0.0-alpha-390
- v1.0.0-alpha-389
- v1.0.0-alpha-388
- v1.0.0-alpha-387
- v1.0.0-alpha-386
- v1.0.0-alpha-385
- v1.0.0-alpha-384
- v1.0.0-alpha-383
- v1.0.0-alpha-382
- v1.0.0-alpha-381
- v1.0.0-alpha-379
- v1.0.0-alpha-378
- v1.0.0-alpha-377
- v1.0.0-alpha-376
- v1.0.0-alpha-375
- v1.0.0-alpha-374
- v1.0.0-alpha-373
- v1.0.0-alpha-371
- v1.0.0-alpha-370
- v1.0.0-alpha-369
- v1.0.0-alpha-368
- v1.0.0-alpha-367
- v1.0.0-alpha-366
- v1.0.0-alpha-365
- v1.0.0-alpha-364
- v1.0.0-alpha-363
- v1.0.0-alpha-362
- v1.0.0-alpha-361
- v1.0.0-alpha-360
- v1.0.0-alpha-359
- v1.0.0-alpha-358
- v1.0.0-alpha-357
- v1.0.0-alpha-356
- v1.0.0-alpha-355
- v1.0.0-alpha-354
- v1.0.0-alpha-353
- v1.0.0-alpha-351
- v1.0.0-alpha-350
- v1.0.0-alpha-349
- v1.0.0-alpha-348
- v1.0.0-alpha-347
- v1.0.0-alpha-346
- v1.0.0-alpha-345
- v1.0.0-alpha-344
- v1.0.0-alpha-343
- v1.0.0-alpha-342
- v1.0.0-alpha-341
- v1.0.0-alpha-340
- v1.0.0-alpha-339
- v1.0.0-alpha-338
- v1.0.0-alpha-336
- v1.0.0-alpha-335
- v1.0.0-alpha-334
- v1.0.0-alpha-333
- v1.0.0-alpha-332
- v1.0.0-alpha-331
- v1.0.0-alpha-330
- v1.0.0-alpha-329
- v1.0.0-alpha-328
- v1.0.0-alpha-325
- v1.0.0-alpha-324
- v1.0.0-alpha-323
- v1.0.0-alpha-322
- v1.0.0-alpha-321
- v1.0.0-alpha-320
- v1.0.0-alpha-319
- v1.0.0-alpha-318
- v1.0.0-alpha-317
- v1.0.0-alpha-316
- v1.0.0-alpha-315
- v1.0.0-alpha-314
- v1.0.0-alpha-313
- v1.0.0-alpha-312
- v1.0.0-alpha-311
- v1.0.0-alpha-310
- v1.0.0-alpha-309
- v1.0.0-alpha-308
- v1.0.0-alpha-307
- v1.0.0-alpha-306
- v1.0.0-alpha-305
- v1.0.0-alpha-304
- v1.0.0-alpha-303
- v1.0.0-alpha-302
- v1.0.0-alpha-301
- v1.0.0-alpha-300
- v1.0.0-alpha-299
- v1.0.0-alpha-298
- v1.0.0-alpha-297
- v1.0.0-alpha-296
- v1.0.0-alpha-295
- v1.0.0-alpha-294
- v1.0.0-alpha-293
- v1.0.0-alpha-292
- v1.0.0-alpha-291
- v1.0.0-alpha-290
- v1.0.0-alpha-289
- v1.0.0-alpha-288
- v1.0.0-alpha-287
- v1.0.0-alpha-286
- v1.0.0-alpha-285
- v1.0.0-alpha-284
- v1.0.0-alpha-283
- v1.0.0-alpha-282
- v1.0.0-alpha-281
- v1.0.0-alpha-280
- v1.0.0-alpha-279
- v1.0.0-alpha-278
- v1.0.0-alpha-277
- v1.0.0-alpha-276
- v1.0.0-alpha-275
- v1.0.0-alpha-274
- v1.0.0-alpha-273
- v1.0.0-alpha-272
- v1.0.0-alpha-271
- v1.0.0-alpha-270
- v1.0.0-alpha-269
- v1.0.0-alpha-268
- v1.0.0-alpha-267
- v1.0.0-alpha-266
- v1.0.0-alpha-265
- v1.0.0-alpha-264
- v1.0.0-alpha-263
- v1.0.0-alpha-262
- v1.0.0-alpha-261
- v1.0.0-alpha-260
- v1.0.0-alpha-259
- v1.0.0-alpha-258
- v1.0.0-alpha-257
- v1.0.0-alpha-256
- v1.0.0-alpha-255
- v1.0.0-alpha-254
- v1.0.0-alpha-253
- v1.0.0-alpha-252
- v1.0.0-alpha-250
- v1.0.0-alpha-249
- v1.0.0-alpha-248
- v1.0.0-alpha-247
- v1.0.0-alpha-246
- v1.0.0-alpha-245
- v1.0.0-alpha-244
- v1.0.0-alpha-242
- v1.0.0-alpha-241
- v1.0.0-alpha-240
- v1.0.0-alpha-239
- v1.0.0-alpha-238
- v1.0.0-alpha-237
- v1.0.0-alpha-236
- v1.0.0-alpha-235
- v1.0.0-alpha-234
- v1.0.0-alpha-233
- v1.0.0-alpha-232
- v1.0.0-alpha-231
- v1.0.0-alpha-230
- v1.0.0-alpha-229
- v1.0.0-alpha-228
- v1.0.0-alpha-227
- v1.0.0-alpha-226
- v1.0.0-alpha-225
- v1.0.0-alpha-224
- v1.0.0-alpha-223
- v1.0.0-alpha-222
- v1.0.0-alpha-221
- v1.0.0-alpha-220
- v1.0.0-alpha-219
- v1.0.0-alpha-218
- v1.0.0-alpha-217
- v1.0.0-alpha-216
- v1.0.0-alpha-215
- v1.0.0-alpha-214
- v1.0.0-alpha-213
- v1.0.0-alpha-212
- v1.0.0-alpha-211
- v1.0.0-alpha-210
- v1.0.0-alpha-209
- v1.0.0-alpha-208
- v1.0.0-alpha-207
- v1.0.0-alpha-206
- v1.0.0-alpha-205
- v1.0.0-alpha-204
- v1.0.0-alpha-203
- v1.0.0-alpha-202
- v1.0.0-alpha-201
- v1.0.0-alpha-200
- v1.0.0-alpha-199
- v1.0.0-alpha-198
- v1.0.0-alpha-197
- v1.0.0-alpha-196
- v1.0.0-alpha-195
- v1.0.0-alpha-194
- v1.0.0-alpha-193
- v1.0.0-alpha-192
- v1.0.0-alpha-191
- v1.0.0-alpha-190
- v1.0.0-alpha-189
- v1.0.0-alpha-188
- v1.0.0-alpha-187
- v1.0.0-alpha-185
- v1.0.0-alpha-184
- v1.0.0-alpha-183
- v1.0.0-alpha-182
- v1.0.0-alpha-181
- v1.0.0-alpha-180
- v1.0.0-alpha-179
- v1.0.0-alpha-178
- v1.0.0-alpha-177
- v1.0.0-alpha-176
- v1.0.0-alpha-175
- v1.0.0-alpha-174
- v1.0.0-alpha-173
- v1.0.0-alpha-172
- v1.0.0-alpha-170
- v1.0.0-alpha-169
- v1.0.0-alpha-167
- v1.0.0-alpha-166
- v1.0.0-alpha-165
- v1.0.0-alpha-164
- v1.0.0-alpha-163
- v1.0.0-alpha-162
- v1.0.0-alpha-161
- v1.0.0-alpha-160
- v1.0.0-alpha-159
- v1.0.0-alpha-158
- v1.0.0-alpha-153
- v1.0.0-alpha-152
- v1.0.0-alpha-151
- v1.0.0-alpha-150
This package is auto-updated.
Last update: 2026-10-13 06:46:54 UTC
README
Enterprise-grade database backups for the Vortos platform (Deploy/CI-CD Block 19).
dump → store → verify → catalog → retain, versioned and scheduled, behind the
OpsKit swappable-driver pattern. The reference stack backs up Postgres + Mongo to
Cloudflare R2 via the existing object store — but no provider name appears outside a
Driver/ namespace.
Concerns (two ports)
| Port | Drivers (in-core) | Selected by |
|---|---|---|
Port\BackupTargetInterface (dump source) |
postgres, mongo |
#[AsDriver] key |
Port\BackupStoreInterface (destination) |
object-store |
#[AsDriver] key |
Both extend OpsKit\DriverInterface, so every driver reports a CapabilityDescriptor
validated at config time and asserted by the TCK (Testing\*ConformanceTestCase).
Guarantees
- Streamed, bounded memory. Dumps flow process → store via multipart; the checksum
is computed in the same pass (
Service\ChecksumStreamFilter). No whole-artifact buffering; no plaintext dump on a tracked/persistent disk path. - Verified at creation.
Service\IntegrityVerifierreads the stored object back, re-checks the checksum (constant-time) and sniffs the format magic. A corrupt or truncated dump fails loudly and is never cataloged as good. A mid-stream subprocess failure is caught byService\Process\ProcessGuardafter the bytes are consumed. - Append-only catalog.
Catalog\Dbal*INSERTs once; a DB trigger rejects UPDATE (DELETE is permitted only for retention). - Safe retention.
Domain\RetentionPolicyis GFS with a hardminKeepFloorand a "never delete the most-recent" guard; dry-run is the default (backup:retention),--applyrequired to delete. WAL is pruned by the PITR invariant (kept iff ≥ the oldest retained base backup). - Full PITR.
pg_basebackupbase backups + WAL shipping (Pitr\PostgresWalArchiver, idempotent).Pitr\PitrPreflightfails closed when the host isn't configured.
Block boundaries
- Block 17 (
vortos-alerts) — not required. Backup events broadcast onEvent\BackupEventSinkInterface(logging default). A failed/integrity-failed backup is aCriticalevent; Block 17 registers an alerting sink via thevortos.backup.event_sinktag — zero changes here. - Block 20 (encryption/3-2-1/object-lock/drills) — additive. At-rest envelope
encryption slots into
Service\EncryptionSeam\StreamTransformInterface(today an identity transform); immutability/cross-region are already declared store capabilities (honestlyfalsefor now).
Host wiring (operator)
- Schedule:
backup:scheduleemits a managed cron fragment that invokesbackup:run. The framework runs no scheduler. - PITR: set
archive_mode=on,wal_level=replica, andarchive_command = 'vortos backup:wal-archive %p --env=prod'on the host. Verify withPitr\PitrPreflight.
Environment
| Var | Default | Purpose |
|---|---|---|
VORTOS_BACKUP_STORE |
object-store |
store driver key |
VORTOS_BACKUP_KEY_PREFIX |
backups |
object key prefix |
VORTOS_BACKUP_LOCK_DIR |
<project>/var/backup-locks |
single-flight locks |
VORTOS_BACKUP_MONGO_URI |
(empty) | mongodump --uri |
pgBackRest (physical backups and the WAL archive)
config/backup.php declares the repository and its schedules; the repository itself (bucket, cipher,
retention, compression) is pgBackRest's own configuration, baked into the database image.
->pgBackRest(stanza: 'main', databaseService: 'write_db', repositoryEnvFile: '/opt/app/pgbackrest.env') ->schedule(fn ($s) => $s ->pgBackRestFull('0 2 * * 0') // weekly full ->pgBackRestDifferential('0 2 * * 1-6') // daily, changed blocks only ->pgBackRestVerify('30 4 * * 0') // re-read the repository against its checksums ->pgBackRestDrill('30 1 * * *')) // restore + replay to the end of the archive
- pgBackRest runs inside the database container; the backup node drives it with Docker exec through
the least-privilege proxy (
VORTOS_BACKUP_DRILL_DOCKER_HOST) and never holds the repository credential. Exec is detached and polled, so no proxy idle timeout can cut a long backup. - Expiry is pgBackRest's own, run at the end of every backup — there is no retention schedule.
- The drill starts a disposable container from the database's own image, mounts the credential
read-only, restores as the server user with
archive_mode=off(a promoted drill never pushes a timeline into the production repository), waits for promotion, then runs the same invariants as every drill pluswal_replayed, which must reach the newest segment archived when the drill began. - The
pgbackrest-repositoryprobe (Monitoring) pages when the repository is unreadable or not ok, when the newest backup or full is older than its schedule allows, or when the server reports segments archived that the repository never stored (archive-push-queue-maxoverflow). - The recovery-time projection anchors on the newest pgBackRest backup when pgBackRest is declared.
- Operator:
backup:pgbackrest status|info|check|backup --type=full|diff|verify|drill. - Cutover. Remove the framework
physical_basebackup and drill from the schedule, ship the image with pgBackRest as the onlyarchive_command, thenbackup:pitr:retire(dry run) and--apply: it deletes every framework base backup and WAL segment, store and catalog, and refuses while either is still scheduled, while the repository is not healthy, or when the newest point-in-time drill did not restore. Logical dumps are untouched. Thewal-efficiencyprobe passes as not-in-use once no base is scheduled.