<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Rustic on Personal blog of Anurag Bhatia</title><link>https://anuragbhatia.com/tags/rustic/</link><description>Recent content in Rustic on Personal blog of Anurag Bhatia</description><generator>Hugo -- gohugo.io</generator><language>en-us</language><lastBuildDate>Thu, 20 Aug 2026 02:34:01 +0530</lastBuildDate><atom:link href="https://anuragbhatia.com/tags/rustic/index.xml" rel="self" type="application/rss+xml"/><item><title>Incremental backup on AWS Glacier with rustic</title><link>https://anuragbhatia.com/post/2026/08/incremental-backup-on-aws-glacier/</link><pubDate>Thu, 20 Aug 2026 02:34:01 +0530</pubDate><guid>https://anuragbhatia.com/post/2026/08/incremental-backup-on-aws-glacier/</guid><description>&lt;p&gt;I have been a heavy &lt;a href="https://restic.net"&gt;restic&lt;/a&gt; user for several years. In Jan of this year, I tried making restic work with &lt;a href="https://aws.amazon.com/s3/storage-classes/glacier/"&gt;AWS Glacier&lt;/a&gt; deep archive but it did not work out. AWS Glacier Deep Archive, for those who may not know, is one of the cheapest storage options, with costs as low as $1/TB, but with very expensive retrieval due to high egress charges from AWS, in addition to retrieval fees, API charges, etc. It can act as a backup of last resort, kind of like insurance if everything else fails, since it costs roughly 1/7th of what &lt;a href="https://www.backblaze.com/cloud-storage/pricing"&gt;Backblaze B2 charges&lt;/a&gt; for the same data ($6.95/TB/month).&lt;/p&gt;
&lt;p&gt;Restic by design is not compatible with Glacier because it stores a mix of data blobs and tree blobs inside /data. Thus, as AWS lifecycle rules move data from S3 Standard to Glacier, basic repo operations start failing. They do have some &lt;a href="https://restic.readthedocs.io/en/stable/faq.html#are-cold-storages-supported"&gt;experimental support as per FAQ&lt;/a&gt;, but it did not work out for me when I tried in Jan.&lt;/p&gt;
&lt;p&gt;A few days back, I came across an open-source project named &lt;a href="https://github.com/rustic-rs/rustic"&gt;rustic&lt;/a&gt; — it&amp;rsquo;s a restic implementation in Rust and supports &lt;a href="https://rustic.cli.rs/docs/commands/init/cold_storage.html"&gt;cold storage&lt;/a&gt;. The rustic client is compatible with a traditional restic repo.&lt;/p&gt;
&lt;br /&gt;
&lt;h2 id="rustic-and-cold-storage"&gt;Rustic and cold storage&lt;/h2&gt;
&lt;p&gt;Rustic supports AWS Glacier as well as OVH cold storage. The way it works is: they keep all data in cold storage and a copy of the metadata in hot storage. This additional copy of metadata in hot storage allows operations like incremental backups, viewing snapshots, and requesting restores where only the specific data blobs needed for the restore are pulled from Glacier. This additional hot storage can sit on S3 standard or even Backblaze B2 (which I prefer due to zero API cost).&lt;/p&gt;
&lt;p&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h2 id="demo"&gt;Demo&lt;/h2&gt;
&lt;h3 id="1-create-repo"&gt;1) Create repo&lt;/h3&gt;
&lt;p&gt;Let&amp;rsquo;s create two buckets - one in S3 (to store data in Glacier) and one on Backblaze B2 to hold an additional copy of metadata.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;&amp;gt; rclone mkdir s3:rustic-demo-cold
&amp;gt; rclone mkdir b2:rustic-demo-hot
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h3 id="2-create-rustic-config-profile"&gt;2) Create rustic config profile&lt;/h3&gt;
&lt;p&gt;This file will define the cold and hot repo locations, along with the password file location, restoration command syntax, etc.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;[repository]
repository = &amp;#34;rclone:s3:rustic-demo-cold&amp;#34;
repo-hot = &amp;#34;rclone:b2:rustic-demo-hot&amp;#34;
password-file = &amp;#34;/home/anurag/.demo-rustic-pass&amp;#34;
warm-up-command = &amp;#39;bash -c &amp;#34;id=%id; rclone backend restore s3:rustic-demo-cold/data/${id:0:2}/ --include \&amp;#34;$id\&amp;#34; -o priority=Standard -o lifetime=7&amp;#34;&amp;#39;
warm-up-wait = &amp;#34;2m&amp;#34;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h3 id="3-generate-password"&gt;3) Generate password&lt;/h3&gt;
&lt;p&gt;This password will encrypt the entire repo, providing encryption at rest.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;&amp;gt; tr -dc &amp;#39;A-Za-z0-9!&amp;#34;#$%&amp;amp;()*+,-./:;&amp;lt;=&amp;gt;?@[]^_{|}~&amp;#39; &amp;lt; /dev/urandom | head -c 16 &amp;gt; /home/anurag/.demo-rustic-pass
&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h3 id="4-initial-rustic-repo"&gt;4) Initial rustic repo&lt;/h3&gt;
&lt;p&gt;Next, initialize the repo. If you have used restic before, it&amp;rsquo;s the same step as init, but instead of creating a repo at only one location, it will create it at both hot &amp;amp; cold storage and ensure data is pushed only to cold storage (while metadata is updated on hot).&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;&amp;gt; rustic -P ~/.config/rustic.toml init
[INFO] using config /home/anurag/.config/rustic.toml
[INFO] using warm-up command bash -c &amp;#39;id=%id; rclone backend restore s3:rustic-demo-cold/data/${id:0:2}/ --include &amp;#34;$id&amp;#34; -o priority=Standard -o lifetime=7&amp;#39; with batch size 1
[INFO] key b4e08cd6 successfully added.
[INFO] repository 2877a54b successfully created.
[INFO] using cache at /home/anurag/.cache/rustic/2877a54b3c9d7d6ba36a7e4cf4b201c4e10b6e7fdf4473db956a48bde8776c7e
&amp;gt; rclone ls s3:rustic-demo-cold
155 config
363 keys/b4e08cd66e06c4cf8f5be2831036beb2f4ad187a2a5985db6bdc563f901a2003
&amp;gt; rclone ls b2:rustic-demo-hot
169 config
363 keys/b4e08cd66e06c4cf8f5be2831036beb2f4ad187a2a5985db6bdc563f901a2003
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h3 id="5-generating-random-files"&gt;5) Generating random files&lt;/h3&gt;
&lt;p&gt;At this step, we are ready to push data. For this demo, I will use AWS Glacier Instant Retrieval because I cannot wait 12 hours for data to be restored. In real commercial use, however, it doesn&amp;rsquo;t make sense to use Instant Retrieval. I will actually use glacier deep archive. I am going to set &lt;strong&gt;storage_class = GLACIER_IR&lt;/strong&gt; in rclone s3 config. The price difference between Glacier Instant Retrieval and B2 is actually not that large, so it doesn&amp;rsquo;t make sense to use Instant Retrieval in production.&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;&amp;gt; dd if=/dev/random of=file.bin bs=1M count=5
5+0 records in
5+0 records out
5242880 bytes (5.2 MB, 5.0 MiB) copied, 0.0140004 s, 374 MB/s
&amp;gt; dd if=/dev/random of=file1.bin bs=1M count=5
5+0 records in
5+0 records out
5242880 bytes (5.2 MB, 5.0 MiB) copied, 0.0148932 s, 374 MB/s
&amp;gt; dd if=/dev/random of=file2.bin bs=1M count=5
5+0 records in
5+0 records out
5242880 bytes (5.2 MB, 5.0 MiB) copied, 0.0134629 s, 403 MB/s
&amp;gt; dd if=/dev/random of=file3.bin bs=1M count=5
5+0 records in
5+0 records out
5242880 bytes (5.2 MB, 5.0 MiB) copied, 0.0139815 s, 403 MB/s
&amp;gt; du -sh file*
5.0M file1.bin
5.0M file2.bin
5.0M file3.bin
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h3 id="6-trigger-a-backup"&gt;6) Trigger a backup&lt;/h3&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;&amp;gt; rustic -P ~/.config/rustic.toml backup file1.bin file2.bin
[INFO] using config /home/anurag/.config/rustic.toml
[INFO] using warm-up command bash -c &amp;#39;id=%id; rclone backend restore s3:rustic-demo-cold/data/${id:0:2}/ --include &amp;#34;$id&amp;#34; -o priority=Standard -o lifetime=7&amp;#39; with batch size 1
[INFO] repository rclone:s3:rustic-demo-cold#rclone:b2:rustic-demo-hot: password is correct.
[INFO] using cache at /home/anurag/.cache/rustic/2877a54b3c9d7d6ba36a7e4cf4b201c4e10b6e7fdf4473db956a48bde8776c7e
[INFO] using no parent
[INFO] starting to backup [&amp;#34;file1.bin&amp;#34;, &amp;#34;file2.bin&amp;#34;] ...
[INFO] Files: 2 new, 0 changed, 0 unchanged
[INFO] Dirs: 1 new, 0 changed, 0 unchanged
[INFO] Added to the repo: 10.0 MiB (raw: 10.0 MiB)
[INFO] processed 2 files, 10.0 MiB
[INFO] snapshot eab96cac successfully saved.
[INFO] backup of file1.bin,file2.bin done.
&amp;gt; rustic -P ~/.config/rustic.toml backup file2.bin
[INFO] using config /home/anurag/.config/rustic.toml
[INFO] using warm-up command bash -c &amp;#39;id=%id; rclone backend restore s3:rustic-demo-cold/data/${id:0:2}/ --include &amp;#34;$id&amp;#34; -o priority=Standard -o lifetime=7&amp;#39; with batch size 1
[INFO] repository rclone:s3:rustic-demo-cold#rclone:b2:rustic-demo-hot: password is correct.
[INFO] using cache at /home/anurag/.cache/rustic/2877a54b3c9d7d6ba36a7e4cf4b201c4e10b6e7fdf4473db956a48bde8776c7e
[INFO] using no parent
[INFO] starting to backup [&amp;#34;file2.bin&amp;#34;] ...
[INFO] Files: 1 new, 0 changed, 0 unchanged
[INFO] Dirs: 1 new, 0 changed, 0 unchanged
[INFO] Added to the repo: 410 B (raw: 640 B)
[INFO] processed 1 files, 5.0 MiB
[INFO] snapshot 44d9e8aa successfully saved.
[INFO] backup of file2.bin done.
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h3 id="7-checking-backed-up-snapshots"&gt;7) Checking backed-up snapshots&lt;/h3&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;&amp;gt; rustic -P ~/.config/rustic.toml snapshots
[INFO] using config /home/anurag/.config/rustic.toml
[INFO] using warm-up command bash -c &amp;#39;id=%id; rclone backend restore s3:rustic-demo-cold/data/${id:0:2}/ --include &amp;#34;$id&amp;#34; -o priority=Standard -o lifetime=7&amp;#39; with batch size 1
[INFO] repository rclone:s3:rustic-demo-cold#rclone:b2:rustic-demo-hot: password is correct.
[INFO] using cache at /home/anurag/.cache/rustic/2877a54b3c9d7d6ba36a7e4cf4b201c4e10b6e7fdf4473db956a48bde8776c7e
[00:00:00] getting snapshots... ████████████████████████████████████████ 2/2
snapshots for (host [desktop], label [], paths [file1.bin,file2.bin])
| ID | Time | Host | Label | Tags | Paths | Files | Dirs | Size |
|----------|---------------------|---------|-------|------|-----------|-------|------|----------|
| eab96cac | 2026-08-19 03:15:35 | desktop | | | file1.bin | 2 | 1 | 10.0 MiB |
| | | | | | file2.bin | | | |
1 snapshot(s)
snapshots for (host [desktop], label [], paths [file2.bin])
| ID | Time | Host | Label | Tags | Paths | Files | Dirs | Size |
|----------|---------------------|---------|-------|------|-----------|-------|------|---------|
| 44d9e8aa | 2026-08-19 03:15:51 | desktop | | | file2.bin | 1 | 1 | 5.0 MiB |
1 snapshot(s)
total: 2 snapshot(s)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h3 id="8-checking-hot--cold-repo-storage-size"&gt;8) Checking hot &amp;amp; cold repo storage size&lt;/h3&gt;
&lt;p&gt;If we check the raw storage via rclone again, we should see the data in cold storage but only a tiny amount of metadata in hot storage:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;&amp;gt; rclone size s3:rustic-demo-cold
Total objects: 9
Total size: 10.004 MiB (10490114 Byte)
&amp;gt; rclone size b2:rustic-demo-hot
Total objects: 8
Total size: 3.444 KiB (3527 Byte)
&amp;gt; rclone lsf s3:rustic-demo-cold/data/cf/cf5ac179f17f6c13f041845665a94b7bc90cd51bfc929ae633b2033e33f69c5a --format &amp;#34;pT&amp;#34;
cf5ac179f17f6c13f041845665a94b7bc90cd51bfc929ae633b2033e33f69c5a;GLACIER_IR
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This confirms that the file is indeed in the Glacier class &lt;em&gt;(Instant Retrieval in this case)&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h3 id="9-data-restore"&gt;9) Data restore&lt;/h3&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;&amp;gt; rustic -P ~/.config/rustic.toml restore 44d9e8aa ~/Downloads/rustic-demo-restore
[INFO] using config /home/anurag/.config/rustic.toml
[INFO] using warm-up command bash -c &amp;#39;id=%id; rclone backend restore s3:rustic-demo-cold/data/${id:0:2}/ --include &amp;#34;$id&amp;#34; -o priority=Standard -o lifetime=7&amp;#39; with batch size 1
[INFO] repository rclone:s3:rustic-demo-cold#rclone:b2:rustic-demo-hot: password is correct.
[INFO] getting snapshot ...
[00:00:00] collecting file information... Files: 1 to restore, 0 unchanged, 0 verified, 0 to modify, 0 additional
Dirs: 0 to restore, 0 to modify, 0 additional
[INFO] total restore size: 5.0 MiB
[00:00:00] warming up Pack(s)... ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 0/1 [
{
&amp;#34;Status&amp;#34;: &amp;#34;Not GLACIER or DEEP_ARCHIVE or INTELLIGENT_TIERING storage class&amp;#34;,
&amp;#34;Remote&amp;#34;: &amp;#34;cf5ac179f17f6c13f041845665a94b7bc90cd51bfc929ae633b2033e33f69c5a&amp;#34;
}
]
[00:00:00] setting metadata... restore done.
&amp;gt; ls ~/Downloads/rustic-demo-restore
file2.bin
&amp;gt; md5sum file2.bin
da0b706cacb5787a8b43ac636ccd397b file2.bin
&amp;gt; md5sum ~/Downloads/rustic-demo-restore/file2.bin
da0b706cacb5787a8b43ac636ccd397b /home/anurag/Downloads/rustic-demo-restore/file2.bin
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h3 id="use-case"&gt;Use case&lt;/h3&gt;
&lt;p&gt;With this, I can migrate a copy of my self-hosted Immich backups from Backblaze B2 to Glacier. I hold multiple backups in different locations/media, and this third backup acts like a backup of last resort.&lt;/p&gt;
&lt;p&gt;Typical cost difference for 600GB data:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Storage Provider&lt;/th&gt;
&lt;th&gt;Monthly per GB cost (USD/INR)&lt;/th&gt;
&lt;th&gt;Total monthly cost for 600GB data (USD/INR)&lt;/th&gt;
&lt;th&gt;Restore cost for full data&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Backblaze B2&lt;/td&gt;
&lt;td&gt;$0.00695 / 0.67 INR&lt;/td&gt;
&lt;td&gt;$4.17 / 402 INR&lt;/td&gt;
&lt;td&gt;Free (as up to 3x i.e. 1.8T B is free egress bandwidth)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AWS Glacier Deep-archive&lt;/td&gt;
&lt;td&gt;$0.00099 / 0.09 INR&lt;/td&gt;
&lt;td&gt;$0.59 / 54 INR&lt;/td&gt;
&lt;td&gt;(Bulk Retrieval: $2.20 + data transfer outside of 100GB quota: $45.00) $47.20 / 4522 INR&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Thus the break-even here is around 396 days. If not restored within this period, it becomes cheaper on Glacier compared to B2. Never use Glacier Deep Archive as the only backup copy for data that you intend to restore regularly.&lt;/p&gt;
&lt;p&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h3 id="limitations"&gt;Limitations&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;One cannot run &lt;code&gt;restic check --read-data&lt;/code&gt; as it would be very expensive in such a setup. I like to do that to validate the consistency of data. However, if this is the 2nd or 3rd copy of the data, I am comfortable relying on AWS&amp;rsquo;s own integrity checks, as they run their own checksums on the data. One cheap DIY way out here can be to retrieve all the data once a year or so using bulk retrieval cost &amp;amp; then have an EC2 machine in the same region to run the check (and avoid massive egress toll).&lt;/li&gt;
&lt;li&gt;AWS Glacier Deep Archive has a minimum storage duration of 180 days. Thus, one cannot delete data before 180 days without incurring an early deletion charge. Depending on what one is backing up, this may or may not be an issue.&lt;/li&gt;
&lt;/ul&gt;</description></item></channel></rss>