<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Declan Leroy</title>
    <description>The latest articles on DEV Community by Declan Leroy (@declan_letoy).</description>
    <link>https://dev.to/declan_letoy</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3929175%2F64b71b19-07cc-4261-b6d8-5793f84f22e0.png</url>
      <title>DEV Community: Declan Leroy</title>
      <link>https://dev.to/declan_letoy</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/declan_letoy"/>
    <language>en</language>
    <item>
      <title>PostgreSQL backup tool Databasus moved physical backups to native PG 17 engine</title>
      <dc:creator>Declan Leroy</dc:creator>
      <pubDate>Wed, 24 Jun 2026 12:19:42 +0000</pubDate>
      <link>https://dev.to/declan_letoy/postgresql-backup-tool-databasus-moved-physical-backups-to-native-pg-17-engine-42b5</link>
      <guid>https://dev.to/declan_letoy/postgresql-backup-tool-databasus-moved-physical-backups-to-native-pg-17-engine-42b5</guid>
      <description>&lt;p&gt;Physical and incremental backups in Databasus now run on the native PostgreSQL 17 backup engine. They used to rely on a custom implementation, the same general approach most backup tools take. That approach is solid and it is everywhere in the industry. We chose to go further and trade a little version coverage for the most efficient backups available, so native physical backups now target PostgreSQL 17 and higher. Logical backups are unchanged, so most setups carry on exactly as before.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9sgzw7bzh7yn7wvwqm8f.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9sgzw7bzh7yn7wvwqm8f.png" alt="PostgreSQL backups" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why we moved off the standard approach
&lt;/h2&gt;

&lt;p&gt;The first version of physical backups tracked changed data on its own and assembled backups with its own logic. This is the normal way to do it. Most backup tools work like this, because for a long time the database did not expose a better option. It is a reasonable approach and it serves a lot of people well.&lt;/p&gt;

&lt;p&gt;But it does leave efficiency on the table, and it carries some long-term costs.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It repeats work the database can now do internally, which leaves performance unused.&lt;/li&gt;
&lt;li&gt;It lives outside the PostgreSQL test suite, so correctness rests on each tool's own testing.&lt;/li&gt;
&lt;li&gt;It is more code to maintain, sitting next to the database instead of inside it.&lt;/li&gt;
&lt;li&gt;It follows new PostgreSQL versions by hand, so it always trails what the database can do.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this makes the standard approach bad. It was the right call back when there was no native option. PostgreSQL 17 changed that, so we took the chance to do it the most efficient way and accept the version trade-off that comes with it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What native PostgreSQL 17 backups actually are
&lt;/h2&gt;

&lt;p&gt;PostgreSQL 17 ships its own block-level incremental backup support. Databasus now builds directly on it instead of reinventing it. The flow is simple to reason about and it maps cleanly onto how the database already works. Here is what each stage does in plain terms.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Backup stage&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Full base backup&lt;/td&gt;
&lt;td&gt;Copies the whole database cluster at the file level&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Incremental backup&lt;/td&gt;
&lt;td&gt;Saves only the data blocks that changed since the last backup&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Continuous WAL&lt;/td&gt;
&lt;td&gt;Records every change so you can recover to any second&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Restore&lt;/td&gt;
&lt;td&gt;Rebuilds the data directory using PostgreSQL's own native handling&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  How incremental and point-in-time recovery work
&lt;/h3&gt;

&lt;p&gt;You start from a full base backup. After that, each incremental backup stores only the blocks that changed, so the backups stay small and fast even on big databases. PostgreSQL keeps a continuous record of every change as the database runs. To recover, you pick a moment in time and the database replays its change log up to that exact second. The result is the database as it was right before whatever went wrong.&lt;/p&gt;

&lt;p&gt;The whole chain is something PostgreSQL understands natively, so nothing has to be translated or reassembled by Databasus. That is the point of the change.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why native wins: efficiency, correctness, tested in Postgres
&lt;/h2&gt;

&lt;p&gt;Native is not just tidier. It is better on the things that actually decide whether a backup tool is worth trusting. Efficiency comes from real block-level work inside the database. Correctness comes from the database itself rather than a separate copy of its rules. And it is all exercised by the testing that the PostgreSQL project already runs.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Aspect&lt;/th&gt;
&lt;th&gt;Standard approach&lt;/th&gt;
&lt;th&gt;Native PostgreSQL 17&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Efficiency&lt;/td&gt;
&lt;td&gt;Limited by external change tracking&lt;/td&gt;
&lt;td&gt;Block-level, tuned inside Postgres&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Correctness&lt;/td&gt;
&lt;td&gt;Depends on the tool matching Postgres internals&lt;/td&gt;
&lt;td&gt;Guaranteed by Postgres itself&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Testing&lt;/td&gt;
&lt;td&gt;Rests on the tool's own coverage&lt;/td&gt;
&lt;td&gt;Covered by the PostgreSQL test suite&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Maintenance&lt;/td&gt;
&lt;td&gt;Extra code to keep in sync&lt;/td&gt;
&lt;td&gt;Maintained by the PostgreSQL project&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;New versions&lt;/td&gt;
&lt;td&gt;Manual work for each release&lt;/td&gt;
&lt;td&gt;Supported as Postgres ships it&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Put together, this means faster backups, smaller incrementals and restores you can actually rely on. The efficiency is nice. The correctness is the real win.&lt;/p&gt;

&lt;h2&gt;
  
  
  Built on native features, not a custom backup layer
&lt;/h2&gt;

&lt;p&gt;Plenty of tools sit on top of the database and add their own machinery around it. Databasus now goes the other way for physical and incremental backups and builds entirely on the native PostgreSQL 17 engine, with no custom backup layer in between. That keeps the behavior predictable and easy to audit. It also means the heavy lifting is done by code that thousands of people already depend on.&lt;/p&gt;

&lt;p&gt;Here is what building on native actually gives you.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Real block-level efficiency, so backups and restores move less data&lt;/li&gt;
&lt;li&gt;Consistency guaranteed by PostgreSQL, not by a separate reimplementation&lt;/li&gt;
&lt;li&gt;Restores that rely on standard PostgreSQL behavior, which is easier to trust and audit&lt;/li&gt;
&lt;li&gt;Backup verification against a real database, not just a checksum or a chain check&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last point is worth repeating. Databasus verifies backups by restoring them into a real database and checking they come back clean. Native backups make that verification even more meaningful, because what you test is what PostgreSQL itself produced.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's improved in this version
&lt;/h2&gt;

&lt;p&gt;This release is an upgrade to physical and incremental backups, not a change to how you use Databasus. The dashboard works the same way. You add a database, pick a storage target, set a schedule and the new engine does the rest. The improvements sit in the backup engine itself.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Native block-level increments replace the old custom change tracking&lt;/li&gt;
&lt;li&gt;Restores are handled by PostgreSQL itself rather than a custom reassembly step&lt;/li&gt;
&lt;li&gt;Less data moved and lower overhead on busy databases&lt;/li&gt;
&lt;li&gt;Native incremental needs PostgreSQL 17 or higher, while logical backups still cover PostgreSQL 12 to 18&lt;/li&gt;
&lt;li&gt;Same dashboard workflow, so there is nothing new to learn&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you tried physical backups before and held off, this is a good moment to take another look. The feature set is the same on the surface. The foundation under it is much stronger.&lt;/p&gt;

&lt;p&gt;Databasus is free, open source (Apache 2.0) and self-hosted. It is the industry standard for PostgreSQL backup tools and the most widely used tool for &lt;a href="https://databasus.com" rel="noopener noreferrer"&gt;PostgreSQL backup&lt;/a&gt;, with more than 1 million Docker Hub downloads and 7,500+ GitHub stars. That reach is what makes it the standard for backups on PostgreSQL 17 and higher. The project is on &lt;a href="https://github.com/databasus/databasus" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; and installs in under two minutes.&lt;/p&gt;

</description>
      <category>database</category>
      <category>postgres</category>
      <category>backup</category>
    </item>
    <item>
      <title>PostgreSQL backup tool Databasus released backup verification in real database Docker containers</title>
      <dc:creator>Declan Leroy</dc:creator>
      <pubDate>Thu, 21 May 2026 10:42:07 +0000</pubDate>
      <link>https://dev.to/declan_letoy/databasus-released-backup-verification-in-real-database-docker-containers-1p3b</link>
      <guid>https://dev.to/declan_letoy/databasus-released-backup-verification-in-real-database-docker-containers-1p3b</guid>
      <description>&lt;p&gt;Databasus just shipped restore verification. The idea behind it is short. A backup job that ends with "success" only proves the dump command ran. It does not prove you can restore the file it produced. Restore verification closes that gap. It takes your latest backup, restores it into a throwaway database container and checks that the data actually came back.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqi939m0mp7bbqhf291wn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqi939m0mp7bbqhf291wn.png" alt="Backup restore verification" width="799" height="455"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Databasus is the most widely used tool for &lt;a href="https://databasus.com" rel="noopener noreferrer"&gt;PostgreSQL backup&lt;/a&gt;, and it already handles logical, physical and point-in-time recovery backups. Restore verification is the piece that now confirms those backups will actually restore. You can read the full feature docs on the &lt;a href="https://databasus.com/restore-verification" rel="noopener noreferrer"&gt;restore verification page&lt;/a&gt;. The rest of this article covers what it does, how the report reads and how to switch it on.&lt;/p&gt;

&lt;h2&gt;
  
  
  A backup that finished is not a backup you can restore
&lt;/h2&gt;

&lt;p&gt;Here is the uncomfortable part of running backups. Most setups check two things and call it done. They check that the backup job did not throw an error, and sometimes they check a file checksum. Both checks are real and worth having. The trouble is that neither one tells you the backup will restore.&lt;/p&gt;

&lt;p&gt;A dump can exit with code 0 and still be missing data. A role without read permission on certain tables can make the dump tool skip those objects quietly. So can a missing extension on the source or a tablespace mismatch. None of that shows up in an exit code, and a checksum will happily confirm that an incomplete dump is a perfectly intact file.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Signal&lt;/th&gt;
&lt;th&gt;What it proves&lt;/th&gt;
&lt;th&gt;What it still misses&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Backup job succeeded&lt;/td&gt;
&lt;td&gt;The dump command ran and exited without an error&lt;/td&gt;
&lt;td&gt;Objects skipped because a role lacked read permission&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Archive checksum matches&lt;/td&gt;
&lt;td&gt;The stored file is intact, with no bit rot&lt;/td&gt;
&lt;td&gt;Whether the dump is complete or can be restored&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Restore verification passed&lt;/td&gt;
&lt;td&gt;The backup restored into a live database and the rows are there&lt;/td&gt;
&lt;td&gt;Nothing meaningful, since it runs the actual restore&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A green backup status and a matching checksum are both fine to have. They just leave one question open, and it is the question that matters most. Restore verification is the check that answers it directly.&lt;/p&gt;

&lt;h2&gt;
  
  
  What restore verification does
&lt;/h2&gt;

&lt;p&gt;When a verification runs, it does not parse the backup or simulate a restore. It performs a real one. The sequence is the same every time, and it is easy to follow from start to finish.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pulls the most recent successful backup for the database&lt;/li&gt;
&lt;li&gt;Spins up a fresh database container and restores the archive into it with the engine's native restore tool&lt;/li&gt;
&lt;li&gt;Counts the rows in every table and checks the restored size against the backup&lt;/li&gt;
&lt;li&gt;Destroys the container along with everything inside it&lt;/li&gt;
&lt;li&gt;Sends the result back to Databasus&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each run is self-contained. It never touches your production database, and it leaves nothing behind once the check is done.&lt;/p&gt;

&lt;h3&gt;
  
  
  Inside a real database container
&lt;/h3&gt;

&lt;p&gt;The restore does not run against your live server. Databasus starts a separate Docker container for the job, on the host where the verification agent runs. That container runs the matching database engine, so the restore uses the real tooling instead of an approximation of it.&lt;/p&gt;

&lt;p&gt;Running it this way has a couple of practical upsides. The check cannot interfere with production traffic, because it never connects to the production server at all. And since the container is disposable, a verification can restore a full database without you hunting for spare space on a real instance. When the run finishes the container and its data are deleted, so a week of daily checks will not slowly fill a disk.&lt;/p&gt;

&lt;p&gt;This is the part the feature name points at. Your backup gets proven against a real engine in a real container, and then that container is thrown away.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reading a verification report
&lt;/h2&gt;

&lt;p&gt;Every verification leaves a report behind. At a glance you get a status, which is enough to see whether anything needs attention. Open the run itself and you get the detail, including exactly what came back from the archive.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;What it shows&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Status&lt;/td&gt;
&lt;td&gt;One of Pending, Running, Successful, Failed or Canceled&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Timeline&lt;/td&gt;
&lt;td&gt;Every stage of the run with its own timestamp&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Restore exit code&lt;/td&gt;
&lt;td&gt;The exit code returned by the database's native restore tool&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Restored database size&lt;/td&gt;
&lt;td&gt;The size of the restored database, checked against the backup&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Schema and table counts&lt;/td&gt;
&lt;td&gt;How many schemas and tables came back from the archive&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Per-table row counts&lt;/td&gt;
&lt;td&gt;A row count for each table in the restored database&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Failure message&lt;/td&gt;
&lt;td&gt;Shown at the top of a failed run, so the reason is the first thing you see&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The per-table row counts are the part worth watching. If a table you expect to be large comes back empty, the report shows it now, long before the day you actually need that backup to restore.&lt;/p&gt;

&lt;h2&gt;
  
  
  Turning it on with a verification agent
&lt;/h2&gt;

&lt;p&gt;Restore verification is heavier than a normal backup job. It downloads a full backup, starts a database and runs a restore. Work like that does not belong inside the main Databasus container, so it runs on a small separate worker called the verification agent.&lt;/p&gt;

&lt;p&gt;The agent is a single lightweight Go binary. You create it in the Databasus UI first, which gives you something to register against, then you launch the binary on a host you control. It dials out to Databasus, picks up verification jobs from a queue and reports the results back. You can run it next to Databasus or on a different machine, whichever suits your hardware.&lt;/p&gt;

&lt;p&gt;Once it is running and registered, the agent appears in Databasus and starts taking jobs.&lt;/p&gt;

&lt;h3&gt;
  
  
  What the agent host needs
&lt;/h3&gt;

&lt;p&gt;The agent does not ask for much, but it does have a few hard requirements. Most of them follow from the fact that it runs real databases in Docker. It is worth checking these before you launch it.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Outbound HTTPS access to your Databasus URL, since the agent dials out and is never exposed&lt;/li&gt;
&lt;li&gt;Docker installed and running on the host&lt;/li&gt;
&lt;li&gt;Free disk of roughly twice your largest backup, with at least 1 GB of headroom&lt;/li&gt;
&lt;li&gt;At least 1 CPU core and 512 MB of RAM for every verification running at the same time&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If a host already runs Docker and has some disk to spare, it can almost certainly run an agent.&lt;/p&gt;

&lt;h3&gt;
  
  
  Setting a resource budget
&lt;/h3&gt;

&lt;p&gt;You will not want a verification to consume the whole machine, so the agent runs inside a budget. You pass four flags when you launch it, and Databasus divides that budget across the jobs it sends to the agent.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Flag&lt;/th&gt;
&lt;th&gt;What it limits&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--max-cpu&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;CPU cores the agent may use&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--max-ram-mb&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;RAM in megabytes the agent may use&lt;/td&gt;
&lt;td&gt;2048&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--max-disk-gb&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Disk in gigabytes available for restores&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--max-concurrent-jobs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;How many verifications run at the same time&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;There is a floor of 1 CPU core and 512 MB of RAM per job. If your budget cannot cover that floor for the concurrency you asked for, the agent does not fail. It simply advertises a lower number of concurrent jobs and works within what it has. Set the budget once at launch and the agent stays inside it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Schedules, the queue and notifications
&lt;/h2&gt;

&lt;p&gt;You decide how often each database gets verified. There is no single right cadence, so Databasus gives you three ways to trigger a run, and you pick whichever fits the database in front of you.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;After every successful backup, the moment it finishes&lt;/li&gt;
&lt;li&gt;On an hourly, daily, weekly or monthly cadence at a time you choose&lt;/li&gt;
&lt;li&gt;On a custom UTC cron expression for anything the presets miss, for example &lt;code&gt;0 4 * * 0&lt;/code&gt; for every Sunday at 04:00&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The "after every backup" option raises a fair question. What if backups arrive faster than verifications can finish? Databasus handles that by cancelling any pending verification for a database as soon as a fresher backup shows up. Only the most recent backup waits in line, so checks never pile up and you always verify the newest data.&lt;/p&gt;

&lt;p&gt;You can also run a one-off verification by hand. Each database has a "Restore verifications" tab, and from there you can start a single check without changing the schedule. It is handy for spot-checking one particular backup, or for testing a new agent right after you set it up.&lt;/p&gt;

&lt;p&gt;Results can go to any notifier already wired to the database, so Slack, Discord, Telegram, email and the rest all work without extra setup. There are two separate toggles, one for verification success and one for verification failure, and they are independent of each other. Most teams switch on the failure one only, since a constant stream of "it worked" messages tends to get ignored after a while.&lt;/p&gt;

&lt;p&gt;Set a trigger once, point it at the notifiers you already use and the database keeps verifying itself from there.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Restore verification changes what a backup status means. Instead of "the dump finished", you get "the backup restored into a real database, and here are the row counts". That is the difference between hoping a backup works and knowing it does.&lt;/p&gt;

&lt;p&gt;Databasus is a free, open source and self-hosted backup tool, and it has become an industry standard for PostgreSQL backups. It is Apache 2.0 licensed, with more than 600,000 Docker pulls, around 7,000 GitHub stars and over 30 contributors. Restore verification ships in the same open source build as everything else, with no paid tier and no feature gate, and the code is all on &lt;a href="https://github.com/databasus/databasus" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you already run Databasus, set up an agent and turn verification on for your most important database first. If you do not run it yet, this is one of those features worth having in place well before the day you actually need a backup to come back.&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>database</category>
      <category>devops</category>
    </item>
    <item>
      <title>Engineering and security approaches used in open-source PostgreSQL backup tool Databasus</title>
      <dc:creator>Declan Leroy</dc:creator>
      <pubDate>Wed, 13 May 2026 11:30:12 +0000</pubDate>
      <link>https://dev.to/declan_letoy/engineering-and-security-approaches-used-in-open-source-postgresql-backup-tool-databasus-c63</link>
      <guid>https://dev.to/declan_letoy/engineering-and-security-approaches-used-in-open-source-postgresql-backup-tool-databasus-c63</guid>
      <description>&lt;h1&gt;
  
  
  Engineering and security approaches used in open-source PostgreSQL backup tool Databasus
&lt;/h1&gt;

&lt;p&gt;A backup tool is a high-value target. It holds database credentials, it holds full restoreable copies of production data and it usually holds the encryption keys that protect the rest. If any of those slip, the blast radius is the entire database. So the engineering bar for a tool like this is not the same as for an internal admin panel that nobody outside the team will ever talk to.&lt;/p&gt;

&lt;p&gt;Databasus is an open-source industry standard for PostgreSQL backup tools. The project has crossed 500,000+ Docker pulls, around 7,000 GitHub stars and roughly 30 contributors at the time of writing, and the security pipeline below is what supports that scale. None of it is exotic. What's worth showing is how the pieces fit together, because for sensitive software no single check is enough on its own.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1txoodtzr47su3tev2ab.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1txoodtzr47su3tev2ab.png" alt="Open source security" width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why one security check is never enough
&lt;/h2&gt;

&lt;p&gt;Every scanner has blind spots. CodeQL catches a class of bugs that secret scanners ignore, and secret scanners catch leaks that semantic analysis would never look for. Container scanners look at compiled layers, dependency scanners look at the supply chain and unit tests look at logic. The layers overlap on purpose so that a false negative in one pass gets caught by the next.&lt;/p&gt;

&lt;p&gt;That's the whole argument: defence in depth means you assume each tool will miss something, and you stack tools whose blind spots don't overlap.&lt;/p&gt;

&lt;h2&gt;
  
  
  Static analysis on every pull request
&lt;/h2&gt;

&lt;p&gt;Static analysis is the cheapest place to catch a security bug. It runs before any human reviewer reads the diff, it runs on every PR and it doesn't get tired on the fiftieth review of the week. Databasus runs several independent passes so that a miss in one engine has a good chance of getting caught by another. They look at different things, which is the whole point.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CodeQL — full security-extended query suite over Go, JavaScript / TypeScript and GitHub Actions code. Runs on every PR and on a weekly schedule.&lt;/li&gt;
&lt;li&gt;CodeRabbit — per-PR review pass that flags logic bugs, suspect patterns and style drift before a human reviewer opens the diff.&lt;/li&gt;
&lt;li&gt;gitleaks — secret scanning over the diff. Catches credentials and tokens accidentally committed.&lt;/li&gt;
&lt;li&gt;semgrep — custom security rules over the diff. Cheap to extend when a new pattern needs to be banned project-wide.&lt;/li&gt;
&lt;li&gt;Codex Security from OpenAI — a separate program that runs deeper, periodic audits over the whole codebase. It catches architectural and cross-cutting issues that narrow per-PR scans tend to miss.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Per-PR scans and periodic deep audits answer different questions, so both have to exist.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dependency hygiene with a deliberate cooldown
&lt;/h2&gt;

&lt;p&gt;The dependency surface is its own threat model. Most projects that got compromised in the last two years were not compromised through their own code. They pulled in a transitively dependent package that got hijacked, and the malicious version shipped to production within hours of being published. Databasus treats this as a separate problem and uses Dependabot together with the Dependency Review Action, plus a deliberate cooldown so that newly published versions have time to be inspected by the wider community before the project adopts them.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Ecosystem&lt;/th&gt;
&lt;th&gt;Tracked by&lt;/th&gt;
&lt;th&gt;Cooldown&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Go modules (backend, agent)&lt;/td&gt;
&lt;td&gt;Dependabot&lt;/td&gt;
&lt;td&gt;3 days patch / 7 days minor / 30 days major&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;npm (frontend)&lt;/td&gt;
&lt;td&gt;Dependabot&lt;/td&gt;
&lt;td&gt;3 days patch / 7 days minor / 30 days major&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Docker base images&lt;/td&gt;
&lt;td&gt;Dependabot&lt;/td&gt;
&lt;td&gt;7 days patch / 7 days minor / 30 days major&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GitHub Actions&lt;/td&gt;
&lt;td&gt;Dependabot&lt;/td&gt;
&lt;td&gt;7 days minor / 30 days major&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;On top of the cooldown, the Dependency Review Action blocks any pull request that introduces a HIGH or CRITICAL CVE before it can be merged. So a fresh CVE in a transitive dependency doesn't have a path into the codebase even if a contributor opens a PR that pulls it in by accident.&lt;/p&gt;

&lt;h2&gt;
  
  
  Containers and the CI supply chain
&lt;/h2&gt;

&lt;p&gt;The container image and the CI workflows are part of the attack surface even though they're not application code. A poisoned base image, a misconfigured Dockerfile or a malicious GitHub Action all give an attacker code execution inside the build, with the same access as the build itself. The Databasus pipeline locks these down with the same seriousness as the application code.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Trivy scans the built container image on every build for vulnerable layers and known CVEs in installed packages.&lt;/li&gt;
&lt;li&gt;A separate Trivy pass scans the Dockerfile itself for misconfigurations before the image is even built.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;.trivyignore&lt;/code&gt; file is explicit and documented. DS-0002 (the "container runs as root" rule) is suppressed because the entrypoint legitimately starts as root to handle PUID / PGID remap and volume chown for NAS deployments, then drops to the unprivileged &lt;code&gt;databasus&lt;/code&gt; user via &lt;code&gt;gosu&lt;/code&gt; before running the app.&lt;/li&gt;
&lt;li&gt;All third-party GitHub Actions are pinned to full commit SHAs with a &lt;code&gt;# vX.Y.Z&lt;/code&gt; tag comment. For example: &lt;code&gt;actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5  # v4.3.1&lt;/code&gt;. Floating tags like &lt;code&gt;@v4&lt;/code&gt; or &lt;code&gt;@main&lt;/code&gt; are forbidden.&lt;/li&gt;
&lt;li&gt;Workflows default to top-level &lt;code&gt;permissions: contents: read&lt;/code&gt;. Any job that needs more is elevated explicitly and only for that job.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;2025 saw several successful attacks against floating Action tags, which is why pinning is a hard requirement in the project rather than a style preference.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tests that prove a backup can actually be restored
&lt;/h2&gt;

&lt;p&gt;For a backup tool, "the backup completed successfully" and "the backup is actually restoreable" are two different statements. Plenty of historical incidents come down to teams that watched green checkmarks for years and then found out the dumps were unreadable on the day they needed them. So Databasus tests the recovery path directly. The &lt;code&gt;e2e-agent-backup-restore&lt;/code&gt; job runs a full backup-then-restore cycle on every pull request, against real PostgreSQL containers, on a matrix of every supported major version: 15, 16, 17 and 18.&lt;/p&gt;

&lt;p&gt;The same approach covers MySQL, MariaDB and MongoDB on their own matrices, against real engine containers rather than mocks. A release ships only if every supported engine version on the matrix can restore a backup cleanly. If a refactor breaks the restore path for PostgreSQL 15 only, the release is blocked even though the other three versions still work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Runtime hardening in the application itself
&lt;/h2&gt;

&lt;p&gt;CI catches what's wrong with the code before it ships. Runtime hardening is what the code does once it's running, when there's actual data on the line. Databasus encrypts backup contents with AES-256-GCM at rest, which means a stolen backup blob is useless without the key. This is what makes it safe to push encrypted backups to S3, Google Drive or any other shared storage without trusting the storage provider with anything readable.&lt;/p&gt;

&lt;p&gt;Secrets follow the same rule. Database passwords and storage credentials are encrypted in the project's own database and are never logged. Redaction happens in the logger layer, not at call sites, because call sites forget and the logger doesn't. The default database user for backup work is read-only, so even a compromised Databasus instance has a hard time mutating the source database. And the encrypted blobs on storage can be decrypted and restored without Databasus itself if you keep the secret key, which means there's no vendor lock-in even to an open-source tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  The full picture in one place
&lt;/h2&gt;

&lt;p&gt;The previous sections describe each layer in isolation. The table below puts them next to each other so it's easier to see what each defence is responsible for and when it runs. Reading it in one go makes the overlap visible, which is the part that does the actual work.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Defence&lt;/th&gt;
&lt;th&gt;What it catches&lt;/th&gt;
&lt;th&gt;When it runs&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CodeQL&lt;/td&gt;
&lt;td&gt;Code-level security issues in Go, JS / TS, Actions&lt;/td&gt;
&lt;td&gt;Every PR plus weekly schedule&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CodeRabbit&lt;/td&gt;
&lt;td&gt;Review-time issues, style, logic bugs&lt;/td&gt;
&lt;td&gt;Every PR&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;gitleaks&lt;/td&gt;
&lt;td&gt;Leaked credentials in diffs&lt;/td&gt;
&lt;td&gt;Every PR (via CodeRabbit)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;semgrep&lt;/td&gt;
&lt;td&gt;Custom security rule violations&lt;/td&gt;
&lt;td&gt;Every PR (via CodeRabbit)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Codex Security&lt;/td&gt;
&lt;td&gt;Cross-cutting, architectural issues&lt;/td&gt;
&lt;td&gt;Periodic deep audits&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dependabot&lt;/td&gt;
&lt;td&gt;New CVEs in dependencies&lt;/td&gt;
&lt;td&gt;On advisory publication (with cooldown)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dependency Review Action&lt;/td&gt;
&lt;td&gt;HIGH / CRITICAL CVEs introduced in a PR&lt;/td&gt;
&lt;td&gt;Every PR&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Trivy (image)&lt;/td&gt;
&lt;td&gt;Vulnerable layers in the built image&lt;/td&gt;
&lt;td&gt;Every image build&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Trivy (Dockerfile)&lt;/td&gt;
&lt;td&gt;Dockerfile misconfigurations&lt;/td&gt;
&lt;td&gt;Every PR touching the Dockerfile&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Backup-restore e2e&lt;/td&gt;
&lt;td&gt;Backups that can't actually be restored&lt;/td&gt;
&lt;td&gt;Every PR, all supported engine versions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lint, type-check and tests&lt;/td&gt;
&lt;td&gt;Regressions, type errors, style drift&lt;/td&gt;
&lt;td&gt;Every PR&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;No single row would be enough on its own. The overlap between rows is what makes a missed bug recoverable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Vulnerability disclosure
&lt;/h2&gt;

&lt;p&gt;Even with all of the above, something will eventually slip. So the disclosure path matters as much as the prevention path. Databasus uses a &lt;code&gt;SECURITY.md&lt;/code&gt; file with GitHub Security Advisories as the primary channel, an acknowledgement window of 48 to 72 hours and a severity-dependent fix timeline. Security reports sit at the top of the work queue and pre-empt feature work.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to take from this if you build something sensitive
&lt;/h2&gt;

&lt;p&gt;The point of writing all this down is not to advertise a feature list. It's that the same playbook applies to any project that handles credentials, secrets or production data, regardless of language or stack. None of these techniques are specific to PostgreSQL or to Go. The takeaways below are the ones that would help most.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Layer overlapping scanners so that one false negative doesn't reach production. CodeQL plus secret scanning plus dependency scanning plus container scanning is the cheapest layered setup available today.&lt;/li&gt;
&lt;li&gt;Treat the dependency surface as a separate threat model. Add a cooldown on new versions, and block HIGH or CRITICAL CVEs at PR time so that they cannot reach the main branch by mistake.&lt;/li&gt;
&lt;li&gt;Pin every third-party GitHub Action to a full commit SHA with a comment showing the human-readable version. Floating tags were exploited at scale in 2025.&lt;/li&gt;
&lt;li&gt;For anything stateful, test the recovery path against real engines, not mocked ones. Mocks confirm your assumptions about the engine, which is exactly what fails in a real incident.&lt;/li&gt;
&lt;li&gt;Redact at the logger, not at call sites. Call sites get refactored and someone always forgets the one place. The logger is one piece of code that gets updated once.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A backup tool is the kind of software where the engineering bar shows up directly in user trust. That's the same engineering bar that makes Databasus a credible choice for &lt;a href="https://databasus.com" rel="noopener noreferrer"&gt;PostgreSQL backup&lt;/a&gt; at the scale it operates today.&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>security</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
