<?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: Bryn Mailer</title>
    <description>The latest articles on DEV Community by Bryn Mailer (@insidestick).</description>
    <link>https://dev.to/insidestick</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%2F4148242%2F3ae8a084-d3dc-4126-9946-d07cbe074b48.jpg</url>
      <title>DEV Community: Bryn Mailer</title>
      <link>https://dev.to/insidestick</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/insidestick"/>
    <language>en</language>
    <item>
      <title>Blueprints, Not Prompts: Using TLA+ to Guide AI-Written Code</title>
      <dc:creator>Bryn Mailer</dc:creator>
      <pubDate>Wed, 21 Oct 2026 02:29:27 +0000</pubDate>
      <link>https://dev.to/insidestick/blueprints-not-prompts-using-tla-to-guide-ai-written-code-114h</link>
      <guid>https://dev.to/insidestick/blueprints-not-prompts-using-tla-to-guide-ai-written-code-114h</guid>
      <description>&lt;p&gt;&lt;em&gt;Code used to be where a team's understanding of a system lived. Now that LLMs write most of it, that understanding is quietly eroding. Here's why we're exploring formal methods at Mulga, specifically TLA+, to build better systems with AI without losing our grip on what we're building, with a real AWS EBS spec as an example.&lt;/em&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  The real contribution isn't code
&lt;/h4&gt;

&lt;p&gt;It is my sincere belief that shared understanding and curated mental models are the primary artifact produced by a great engineering team.&lt;/p&gt;

&lt;p&gt;For many, the act of sitting down to write the code was the critical step where their understanding of the problem space would SLOWLY be formalized, picked apart, and rearranged. The codebase simply provided a consistent and objective way to distribute the fruits of individual reasoning efforts to the wider team. The design discussions, frantic bug hunts, and PR reviews were core activities that served to deepen a team's understanding of how their particular problem could, and should, be solved.&lt;/p&gt;

&lt;p&gt;At the end of it all, yes, the codebase "did the thing" (ideally). Hooray. The more important outcome was that there was now a formalized solution to the original problem, and likely many adjacent issues. Such a repository of distilled technical intent enabled later generations of engineers within the same company/group/project/community to leverage the hard-won mental models of their forebears, and ultimately drive forward the collective understanding.&lt;/p&gt;

&lt;h4&gt;
  
  
  Where it all went wrong
&lt;/h4&gt;

&lt;p&gt;Since AI-written code became "good enough" to feasibly reach production in early 2026, many of us have been aware of the slow decline of this feedback loop that sustains our industries, livelihoods, and passions. For most of us, days now consist of prompting an LLM to produce code that satisfies a particular business need. Usually via written English. I can't speak for the entire development community, but I can personally attest to this having degraded my ability to write code by hand.&lt;/p&gt;

&lt;p&gt;One could play devil's advocate and say something along the lines of: if LLMs can write all the code now, then why does it matter if we lose the ability to do so ourselves? To which I would say: it shouldn't really matter, provided we are able to maintain the cycle of capturing individual understanding in a manner that may be easily consumed by peers.&lt;/p&gt;

&lt;p&gt;At last, we arrive at the core of the problem. By degrading/losing the ability to write code, we become disconnected from the act of translating ideas and intent into verifiable formalized logic that is useful to others.&lt;/p&gt;

&lt;h4&gt;
  
  
  The problem with natural language
&lt;/h4&gt;

&lt;p&gt;How many of us regularly chat to our LLMs using the most horrendous, broken English we can possibly conceive out of sheer laziness? I'm definitely guilty! Even if you are one of the few who maintain a high quality of communication in your prompts, written/spoken language is a famously imprecise method of communication (&lt;em&gt;cough&lt;/em&gt; synonyms &lt;em&gt;cough cough&lt;/em&gt;).&lt;/p&gt;

&lt;p&gt;I challenge you to try to write a system specification in your language of choice that makes explicit all assumptions and ignored details, yet leaves no room for alternate interpretations of the details that are captured. It is unlikely that you will succeed to a degree that allows you to hand that specification to an LLM and get a result that satisfies your intent.&lt;/p&gt;

&lt;p&gt;If your system is simple enough that you do achieve an initially satisfactory result, you will inevitably discover some edge case that needs addressing. At that point, you could either adjust the written specification and have AI regenerate the implementation from scratch, which would likely resolve the issue, but also leave you with an entirely different system (unless your written spec is absurdly long and detailed).&lt;/p&gt;

&lt;p&gt;Or you could simply ask the LLM to patch the change into the existing implementation via a quick prompt. A prompt whose core ideas are never properly understood, refined, and formalized into the collective human understanding of the project. Even if you do take the time to update the specification, fixing the edge case in isolation from the rest of the system introduces drift that severely degrades (even destroys) the spec's trustworthiness.&lt;/p&gt;

&lt;p&gt;To illustrate the hilarity of the current prevailing approach to software specification, imagine an architect giving a construction team nothing more than a written description of the building! If natural language were precise enough to facilitate proper specifications, we wouldn't need lawyers to disambiguate the legal system for us average folk.&lt;/p&gt;

&lt;h4&gt;
  
  
  Formal methods and TLA+
&lt;/h4&gt;

&lt;p&gt;So what do we do? How do we plug the AI-shaped hole in the bottom of the bucket that used to hold the collective knowledge of our field? Honestly, I don't know. However, my desire for a solution to this conundrum has led me deep into the formal methods rabbit hole over the past year. On my journey, I've picked up a few techniques/tools that have begun to help me address my concerns with LLM-assisted development.&lt;/p&gt;

&lt;p&gt;One such tool that has been especially beneficial is &lt;a href="https://github.com/tlaplus/tlaplus" rel="noopener noreferrer"&gt;TLA+&lt;/a&gt;. It was originally created by Leslie Lamport, and proves once again just how far ahead of his time he was. I'm not going to dive into an explanation of TLA+ here as it's already been done far better than I ever could, by the man himself. I highly recommend taking a moment to read Lamport's high-level view of TLA+ over on his &lt;a href="https://lamport.azurewebsites.net/tla/high-level-view.html" rel="noopener noreferrer"&gt;website&lt;/a&gt; before proceeding. For those of you who will not heed my advice (shame), TLA+ is a language based on formal mathematical notation that allows one to create precise models of anything that can be represented as a state machine (everything, with a bit of creativity). It ships alongside TLC, which can be used to exhaustively explore logical paths within your models, and find design issues.&lt;/p&gt;

&lt;p&gt;TLA+ is NOT code; it is mathematics. Before you recoil in disgust, here's an excerpt from Lamport's website that explains why this is so:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;TLA+ teaches you that math is inherently more expressive than programming languages because it can describe a value without having to describe how the value is computed.&amp;nbsp; For example, it can describe the greatest common divisor (GCD) of two numbers as the largest positive integer that divides both numbers. This makes it possible to write a model for a specific purpose, abstracting away irrelevant details such as how to calculate the GCD.&amp;nbsp;(Putting code in a procedure doesn't abstract it away; it just makes you go elsewhere to read the code, requiring that you understand the semantics of procedure call as well as the code.)&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In the context of a world in which AI writes the code, this is enormously important. It gives us a language in which we can elegantly capture our intent, without getting bogged down in implementation details that ultimately don't matter to the use case. A well-written TLA+ spec is a human-readable artifact that forces the author to reason through the system they are designing in a constrained and logical format, is easy for peers to digest and iterate upon, and is objectively machine checkable (via TLC) so that LLMs are given a concrete benchmark that implementations may be evaluated against.&lt;/p&gt;

&lt;p&gt;The TLA+ model is the architectural blueprint that the design team argues over, iterates upon, and hands to the next generation as learning material, just as the codebase used to be. The code itself becomes a concern of the construction team (coding agents). The details of the implementation can be tied to the spec, preventing drift, with the use of strategies such as &lt;a href="https://arxiv.org/abs/2404.16075" rel="noopener noreferrer"&gt;trace-checking&lt;/a&gt; and refinement mapping. I liken these processes to the surveyor who measures a new build against the numbers in the plans to ensure conformance.&lt;/p&gt;

&lt;p&gt;Many readers might be thinking something along the lines of "if TLA+ is so great, why haven't I heard of it before?". TLA+, and the discipline of formal methods as a whole, has largely flown under the radar of the wider software engineering community, with the exception of niche teams within resource-rich industry titans such as Google and &lt;a href="https://www.amazon.science/publications/how-amazon-web-services-uses-formal-methods" rel="noopener noreferrer"&gt;AWS&lt;/a&gt;. As far as I can tell, this is mostly due to the barrier to entry.&lt;/p&gt;

&lt;p&gt;Engineers are trained to write code. Most gave their discrete mathematics classes only the bare minimum attention needed to pass their exams, then promptly forgot the skills once they entered the workforce. From the perspective of an engineering executive: why would I have my team spend time writing a model in TLA+ (or another specification language), when I could have them build the real solution in [insert programming language of choice]? Someone would have to write the code eventually anyway...&lt;/p&gt;

&lt;p&gt;At the time, such arguments were perfectly reasonable. No matter how precise your formal spec, someone still has to write the executable code. Or rather, someTHING! Enter LLMs. On February 5th of this year (2026), Anthropic released Opus 4.6. It is widely considered the turning point for AI-assisted development; models could finally write code that was at least somewhat reasonable. We live in a world where, if we can produce testable specifications that sufficiently constrain the details of a system that matter to the use case, we can give AI an "abstract" model and receive a working implementation with very little human babysitting.&lt;/p&gt;

&lt;p&gt;This is MUCH easier said than done, but TLA+ is an incredible step in the right direction. And given that so many engineers are no longer writing code themselves, one could argue that improving the quality and accuracy of our specifications is a core part of the job, in a way that it never was before. I have a hunch that this mentality shift will go a long way towards breaking down the perceived "barrier to entry" that has so limited the usefulness of formal methods to the wider software engineering community.&lt;/p&gt;

&lt;h4&gt;
  
  
  A TLA+ specification of the AWS Elastic Block Store API
&lt;/h4&gt;

&lt;p&gt;My attention at Mulga is largely focused on the storage components of our stack, &lt;a href="https://github.com/mulgadc/predastore" rel="noopener noreferrer"&gt;Predastore&lt;/a&gt; (S3-compatible object storage) and &lt;a href="https://github.com/mulgadc/viperblock" rel="noopener noreferrer"&gt;Viperblock&lt;/a&gt; (EBS-like virtual disks). In such projects, details are extraordinarily important, and ambiguity is to be avoided at all costs. When a user engages with something claiming to be a storage service, they assume that their data is in good hands and won't be carelessly lost. On top of this responsibility, the systems are complex and easy to lose track of under the tidal wave of AI-generated contributions.&lt;/p&gt;

&lt;p&gt;I recently used TLA+ to write a specification of the observable behavior of a subset of AWS's EBS API. The intent was to solidify my understanding of the EBS volume lifecycle state machine, so that I can ensure Spinifex (our AWS-compatible control plane) is properly adhering to the behaviors expected by the AWS CLI for the EBS operations it exposes.&lt;/p&gt;

&lt;p&gt;For the curious reader, the specification is open source and is available to read in the Viperblock GitHub &lt;a href="https://github.com/mulgadc/viperblock/tree/dev/spec/EBS" rel="noopener noreferrer"&gt;repository&lt;/a&gt;. It's only ~400 lines (as of now), across 4 files! This is rather notable, considering the relevant (English) technical documentation published by AWS for these operations extends across 13 separate web pages, and still manages to leave a vast amount of detail up to the reader to discern. The specification's brevity makes sense as, naturally, a written description of a building would take up far more space than a blueprint capturing the relevant details! Some request/response fields, possible error codes, and features have been abstracted out of this initial version, of course, but the core volume/attachment status state machines are captured very clearly. You do not need to be a career mathematician to make fantastic use of TLA+!&lt;/p&gt;

&lt;p&gt;A common question I receive is "can't AI just write the TLA+ too?". Well, yes... technically. However, I'd argue there's not really any value in doing things that way. If an AI writes it, you're right back to relying on prompts that nobody has rigorously thought through, and that are invisible to colleagues and mentees.&lt;/p&gt;

&lt;p&gt;TLA+ is ultimately a tool for the communication of ideas, in the same way that the legal system uses a constrained version of natural language to avoid confusion and mathematicians use formal notation. The power in a specification language built upon said mathematical notation is its innate ability to be applied effectively at any level of abstraction. If all you have is a vague idea of how the system you wish to build should appear to a user, that is enough to write a solid specification. There is plenty of room later to refine, and add detail that was missed (or deemed unimportant) on the first pass. This can extend as far down as the electrons themselves, if such detail is relevant to your problem space.&lt;/p&gt;

&lt;p&gt;To illustrate this idea, here is the part of my EBS specification that captures the subset of the observable behavior of the CreateVolume operation that I was particularly interested in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;\* A client requested a new volume.
CreateVolume(req, res) ==
  /\ req \in CreateVolumeRequests
  /\ LET PossibleErrors(db) == Errors({})
         PossibleRecords    == [status             : {"creating"},
                                \* After enabling encryption by default, CreateVolume may still
                                \* see a stale value from another replica for a short window.
                                encrypted          : {db.encryptedByDefault : db \in VisibleDBs},
                                multiAttachEnabled : {FALSE}]
        \* The request was accepted.
     IN \/ /\ PossibleErrors(CurrentDB) = {}
           /\ \E vId \in VolumeIds \ usedIds, record \in PossibleRecords :
                /\ Commit([CurrentDB EXCEPT !.volumes = InsertKey(@, vId, record)])
                /\ res = [vId |-&amp;gt; vId] @@ record
        \* The request was rejected, possibly by a stale replica.
        \/ /\ \E db \in VisibleDBs : res \in PossibleErrors(db)
           /\ UNCHANGED vars
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Some information to aid in reading this excerpt: &lt;code&gt;/\&lt;/code&gt; and &lt;code&gt;\/&lt;/code&gt; can be read as AND and OR respectively; curly braces represent a &lt;a href="https://en.wikipedia.org/wiki/Set_(mathematics)" rel="noopener noreferrer"&gt;set&lt;/a&gt;; &lt;code&gt;==&lt;/code&gt; is a definition; &lt;code&gt;=&lt;/code&gt; is a comparison; the lines that begin with &lt;code&gt;\E&lt;/code&gt; are checking whether an element &lt;a href="https://en.wikipedia.org/wiki/Existential_quantification" rel="noopener noreferrer"&gt;exists&lt;/a&gt; in a target set that satisfies the condition after the &lt;code&gt;:&lt;/code&gt;. The comments give enough of the gist that the rest of the syntax can be glossed over. Specifics can be found at &lt;a href="https://learntla.com/" rel="noopener noreferrer"&gt;learntla.com&lt;/a&gt;. It helps to think of a TLA+ spec as one big condition in an if statement that you would find in a traditional programming language.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This operator (an &lt;em&gt;action&lt;/em&gt; in TLA+ terms) contains very little detail about how exactly a volume is created. In fact, throughout my entire spec, the concept of a "volume" being a virtual disk that bytes can be read from and written to is not captured anywhere! This is because it was not relevant to the control plane operations that I was concerned with at the time. I simply abstracted it away.&lt;/p&gt;

&lt;p&gt;A final anecdote: while writing the EBS control plane model, I found myself unable to glean all of the information required to get a good picture of the possible volume status transitions from the AWS docs. This drove me to run some tests myself, and led to the discovery that a volume whose status is "creating" can immediately transition to "deleting" without first passing through "error" in certain cases. This is undocumented behavior that is extremely valuable to me in the design of Viperblock.&lt;/p&gt;

&lt;h4&gt;
  
  
  What's next?
&lt;/h4&gt;

&lt;p&gt;At the time of writing, the spec only captures a few particularly important EBS API endpoints. I'm currently working on using TLC to trace-check my model against the real EBS API to verify that it is correct. The plan is to then trace-check Spinifex's EBS API implementation against the verified model, and fix the divergences that will inevitably be surfaced! From there I will extend the model to capture all EBS API operations that Spinifex supports, and repeat the trace-checking process.&lt;/p&gt;

&lt;p&gt;This was not the article I initially set out to write. I'd intended to cover far more technical detail and dive deeper into the structure of the spec itself, the trace-checking methodology, and my ideas around how formal specification may be feasibly brought into the industry as a standard practice. However, in explaining my experiences to friends and colleagues, I realized that such content would not be very useful to the engineering community if I didn't first attempt to give the "why" behind the methods. Alas, the rest will have to wait for future articles.&lt;/p&gt;

&lt;p&gt;If I have sufficiently piqued your interest, rest assured that you will not be waiting long for the next installment in this saga. So stay tuned. Until next time!&lt;/p&gt;

&lt;p&gt;Bryn&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>testing</category>
      <category>tools</category>
    </item>
    <item>
      <title>Migrating from MinIO to Predastore</title>
      <dc:creator>Bryn Mailer</dc:creator>
      <pubDate>Tue, 29 Sep 2026 04:58:47 +0000</pubDate>
      <link>https://dev.to/insidestick/migrating-from-minio-to-predastore-21jj</link>
      <guid>https://dev.to/insidestick/migrating-from-minio-to-predastore-21jj</guid>
      <description>&lt;p&gt;If you're reading this, you're probably already aware of MinIO's fall from grace. The open source community's favorite object storage system was officially archived back in April (2026, for future readers). The impending deficit of security patches and bug fixes has since sent many sysadmins looking for a maintained alternative.&lt;/p&gt;

&lt;p&gt;If you're one of the affected individuals, I'd like to take the opportunity to introduce you to &lt;a href="https://github.com/mulgadc/predastore" rel="noopener noreferrer"&gt;Predastore&lt;/a&gt;!&lt;/p&gt;

&lt;p&gt;My team and I have been working on this project for around a year and a half now. Long before the crescendo of the MinIO saga. The original motivation was to build an object storage system capable of operating in resource constrained edge environments with an unreliable network fabric.&lt;/p&gt;

&lt;p&gt;Although there is still plenty of work to be done to soundly meet that goal, the project in its current state happens to fit the more standard use cases (that MinIO excelled in) rather well!&lt;/p&gt;

&lt;p&gt;By the end of this article, you will hopefully have a clear understanding of the process of migrating from a MinIO deployment to a brand new Predastore cluster.&lt;/p&gt;

&lt;h2&gt;
  
  
  An architectural overview
&lt;/h2&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%2Fpxl7lzm9mjpr6pwf4o3z.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%2Fpxl7lzm9mjpr6pwf4o3z.png" alt="Diagram of a simple Predastore cluster: S3 clients connect over HTTPS to the gate node on each of three hosts. Each host has its own IP address and runs a gate, meta and blob node on separate ports, and each blob node writes to its own disk." width="800" height="448"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Note: Everything in this section is subject to change. Predastore is a fairly young project, and certain design decisions have not been set in stone yet. For example, I'm not particularly sold on the "blob" terminology yet. If anyone has any better ideas, please raise a GitHub issue! It will make my day.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In order to start building a mental model of how MinIO concepts map to Predastore, a rudimentary of understanding of Predastore itself is required. This will be rather brief. If you want the meatier details, check out the design doc located &lt;a href="https://github.com/mulgadc/predastore/blob/main/docs/DESIGN.md" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;A Predastore cluster is organized into two levels; hosts, and nodes. A host is just a single Predastore process, invoked via &lt;code&gt;s3d&lt;/code&gt;. Each host can contain any number of nodes, where each node has a role that is one of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;gate&lt;/strong&gt; - Exposes the S3 HTTP API. There may be at most one gate node per host process.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;meta&lt;/strong&gt; - A Raft replica holding bucket and object records, including where every object's shards live.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;blob&lt;/strong&gt; - A key-value store that writes encrypted, erasure-coded shards to append-only segment files. The directory to which a blob node reads/writes is set using the &lt;code&gt;data_dir&lt;/code&gt; field in the config file.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each host gets its own IP address, and each node within a single host binds to a separate port under the host IP address. This ensures that every node is individually addressable.&lt;/p&gt;

&lt;p&gt;A cluster must contain at least one of each type of node. Additionally, it is highly recommended that each blob node is pointed at a separate disk to ensure proper isolation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before you begin: what Predastore doesn't currently do
&lt;/h2&gt;

&lt;p&gt;MinIO was a mature project, with many years spent adding support for all manner of features provided by S3. Predastore is rather young as of yet, and while we support the main body of features required by most deployments, there are a number of things still on the TODO list.&lt;/p&gt;

&lt;p&gt;Before proceeding with the following steps, take a moment to skim the S3 API coverage page &lt;a href="https://docs.mulgadc.com/coverage/s3" rel="noopener noreferrer"&gt;here&lt;/a&gt; to ensure Predastore isn't missing an operation your use case depends on.&lt;/p&gt;

&lt;p&gt;If something you're after is absent, please notify my team and me by raising a GitHub issue. We're pretty responsive. Depending on the request, there's a good chance we'll be able to slip it into a minor release.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Pick a mode: standalone or under Spinifex
&lt;/h2&gt;

&lt;p&gt;The first step in the process is to decide whether your new Predastore cluster is going to be deployed standalone, or via &lt;a href="https://mulgadc.com/spinifex" rel="noopener noreferrer"&gt;Spinifex&lt;/a&gt;. This is usually a fairly simple choice, depending on your desired authentication structure. The general capabilities of each method:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Standalone&lt;/strong&gt; - A fixed set of service accounts, one tenant, no external dependencies. Manual distribution of keys and certificates to nodes during setup.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Via Spinifex&lt;/strong&gt; - IAM users/groups/roles, STS, multiple tenants. Automatic distribution of keys and certificates to member nodes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I highly recommend taking a moment to peruse the "&lt;a href="https://github.com/mulgadc/predastore#standalone-or-spinifex" rel="noopener noreferrer"&gt;Standalone or Spinifex?&lt;/a&gt;" section of Predastore's README for guidance on which path to take.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Stand up Predastore
&lt;/h2&gt;

&lt;p&gt;The exact installation steps you should follow depend on the structure of your pre-existing MinIO cluster. Expand the section below that matches your deployment topology and chosen mode of installation.&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;
  Single-Node Single/Multi-Drive - Standalone
  &lt;p&gt;This is the simplest path, and the closest equivalent to a single MinIO server. You'll need a Linux machine running systemd, along with Go (1.27 or newer), &lt;code&gt;git&lt;/code&gt;, &lt;code&gt;make&lt;/code&gt;, and &lt;code&gt;openssl&lt;/code&gt; in order to build Predastore from source. Most distro packages of Go are a few versions behind, so you may need to grab it from &lt;a href="https://go.dev/dl/" rel="noopener noreferrer"&gt;go.dev&lt;/a&gt;. You'll also want the AWS CLI for checking Predastore once it's up.&lt;/p&gt;
&lt;h4&gt;
  
  
  Build and install
&lt;/h4&gt;

&lt;p&gt;First up, clone the repository and build the &lt;code&gt;s3d&lt;/code&gt; binary:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone &lt;span class="nt"&gt;--branch&lt;/span&gt; v1.21.0 https://github.com/mulgadc/predastore.git
&lt;span class="nb"&gt;cd &lt;/span&gt;predastore
make build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Note: This guide was written against v1.21.0. The install tooling used below (&lt;code&gt;make install&lt;/code&gt;, &lt;code&gt;predastore-keygen&lt;/code&gt;, and the example config) first shipped in v1.20.0, so don't go any older than that.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Install it, along with its systemd service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;make &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then create the dedicated &lt;code&gt;predastore&lt;/code&gt; user, and the config and data directories (&lt;code&gt;/etc/predastore&lt;/code&gt; and &lt;code&gt;/var/lib/predastore&lt;/code&gt; respectively):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemd-sysusers &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;systemd-tmpfiles &lt;span class="nt"&gt;--create&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Share MinIO's drives
&lt;/h4&gt;

&lt;p&gt;There's no need for new drives. Predastore can live on the ones MinIO is already using, in a directory of its own alongside MinIO's data, until you're done with MinIO in step 5. Each drive needs roughly as much free space again as MinIO is using on it, plus some headroom for re-syncs, which &lt;code&gt;df -h&lt;/code&gt; will tell you.&lt;/p&gt;

&lt;p&gt;The steps below assume MinIO's drives are mounted at &lt;code&gt;/mnt/disk1&lt;/code&gt;, &lt;code&gt;/mnt/disk2&lt;/code&gt;, and so on. If they're pooled with RAID or ZFS, treat the pool as a single drive.&lt;/p&gt;

&lt;p&gt;For each drive, create a directory for Predastore. The leading &lt;code&gt;.&lt;/code&gt; matters, as MinIO treats any other top-level directory on its drives as a bucket, and will delete it along with that bucket.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo install&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; predastore &lt;span class="nt"&gt;-g&lt;/span&gt; predastore &lt;span class="nt"&gt;-m&lt;/span&gt; 0700 /mnt/disk1/.predastore
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The service isn't permitted to write anywhere outside &lt;code&gt;/var/lib/predastore&lt;/code&gt;, so bind-mount each directory into it. The extra options make sure the service waits for the mount, and refuses to start without it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo install&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt; 0755 /var/lib/predastore/disk1
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'/mnt/disk1/.predastore /var/lib/predastore/disk1 none bind,nofail,x-systemd.requires-mounts-for=/mnt/disk1,x-systemd.required-by=predastore.service,x-systemd.before=predastore.service 0 0'&lt;/span&gt; | &lt;span class="nb"&gt;sudo tee&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt; /etc/fstab
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once every drive has its line in &lt;code&gt;/etc/fstab&lt;/code&gt;, mount them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl daemon-reload
&lt;span class="nb"&gt;sudo &lt;/span&gt;mount &lt;span class="nt"&gt;-a&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Note: Leave &lt;code&gt;/var/lib/predastore/disk1&lt;/code&gt; and friends owned by root. If a mount ever goes missing, that's what stops Predastore from quietly writing to the system disk in its place.&lt;/em&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Match your MinIO setup
&lt;/h4&gt;

&lt;p&gt;Predastore ships with an example config that already describes a working single-drive host, so there isn't a great deal to change. Copy it into place, and open it up in your editor of choice:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo cp&lt;/span&gt; /etc/predastore/predastore.toml.example /etc/predastore/predastore.toml
&lt;span class="nb"&gt;sudo&lt;/span&gt; &lt;span class="nv"&gt;$EDITOR&lt;/span&gt; /etc/predastore/predastore.toml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the purposes of the migration, there are only two fields you need to set. &lt;code&gt;region&lt;/code&gt; needs to match whatever your MinIO clients are configured with, as S3 clients sign every request for a specific region. &lt;code&gt;[[auth]]&lt;/code&gt; holds the access key and secret your applications will use.&lt;/p&gt;

&lt;p&gt;Keep in mind that these are root credentials, with full access to every bucket in the cluster (as mentioned in the standalone vs Spinifex discussion).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="py"&gt;region&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"us-east-1"&lt;/span&gt;

&lt;span class="nn"&gt;[[auth]]&lt;/span&gt;
&lt;span class="py"&gt;access_key_id&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"&amp;lt;access-key&amp;gt;"&lt;/span&gt;
&lt;span class="py"&gt;secret_access_key&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"&amp;lt;secret-key&amp;gt;"&lt;/span&gt;
&lt;span class="py"&gt;account_id&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"100000000001"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The example config also includes an &lt;code&gt;example-bucket&lt;/code&gt; entry, which you can go ahead and delete.&lt;/p&gt;

&lt;p&gt;Next, point the blob nodes at the drives. If MinIO only had one drive, set &lt;code&gt;data_dir = "/var/lib/predastore/disk1"&lt;/code&gt; on the example's blob node, and the config is done.&lt;/p&gt;

&lt;p&gt;Otherwise, replace the example's single blob node with one blob node per drive. Give each one its own id and port, and point its own &lt;code&gt;data_dir&lt;/code&gt; at the drive's bind mount.&lt;/p&gt;

&lt;p&gt;Leave the host's &lt;code&gt;data_dir&lt;/code&gt; as it is, since the meta node doesn't name a directory of its own and keeps its state under &lt;code&gt;/var/lib/predastore&lt;/code&gt; on the system disk. With three drives, the &lt;code&gt;[[host]]&lt;/code&gt; block ends up looking like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[[host]]&lt;/span&gt;
&lt;span class="py"&gt;id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="py"&gt;addr&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"127.0.0.1"&lt;/span&gt;
&lt;span class="py"&gt;admin_port&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;9099&lt;/span&gt;
&lt;span class="py"&gt;data_dir&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"/var/lib/predastore"&lt;/span&gt;
&lt;span class="py"&gt;tls_cert&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"/etc/predastore/server.pem"&lt;/span&gt;
&lt;span class="py"&gt;tls_key&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"/etc/predastore/server.key"&lt;/span&gt;
&lt;span class="py"&gt;encryption_key&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"/etc/predastore/master.key"&lt;/span&gt;

  &lt;span class="nn"&gt;[[host.node]]&lt;/span&gt;
  &lt;span class="py"&gt;id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
  &lt;span class="py"&gt;role&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"gate"&lt;/span&gt;
  &lt;span class="py"&gt;port&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8443&lt;/span&gt;
  &lt;span class="py"&gt;bind_addr&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"0.0.0.0"&lt;/span&gt;

  &lt;span class="nn"&gt;[[host.node]]&lt;/span&gt;
  &lt;span class="py"&gt;id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
  &lt;span class="py"&gt;role&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"meta"&lt;/span&gt;
  &lt;span class="py"&gt;port&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;6660&lt;/span&gt;

  &lt;span class="nn"&gt;[[host.node]]&lt;/span&gt;
  &lt;span class="py"&gt;id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
  &lt;span class="py"&gt;role&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"blob"&lt;/span&gt;
  &lt;span class="py"&gt;port&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;9991&lt;/span&gt;
  &lt;span class="py"&gt;data_dir&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"/var/lib/predastore/disk1"&lt;/span&gt;

  &lt;span class="nn"&gt;[[host.node]]&lt;/span&gt;
  &lt;span class="py"&gt;id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;
  &lt;span class="py"&gt;role&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"blob"&lt;/span&gt;
  &lt;span class="py"&gt;port&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;9992&lt;/span&gt;
  &lt;span class="py"&gt;data_dir&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"/var/lib/predastore/disk2"&lt;/span&gt;

  &lt;span class="nn"&gt;[[host.node]]&lt;/span&gt;
  &lt;span class="py"&gt;id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;
  &lt;span class="py"&gt;role&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"blob"&lt;/span&gt;
  &lt;span class="py"&gt;port&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;9993&lt;/span&gt;
  &lt;span class="py"&gt;data_dir&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"/var/lib/predastore/disk3"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, set &lt;code&gt;[rs]&lt;/code&gt; to match. Each object is split into &lt;code&gt;data&lt;/code&gt; shards plus &lt;code&gt;parity&lt;/code&gt; shards, each on a different drive, so &lt;code&gt;parity&lt;/code&gt; is the number of drives you can lose, and &lt;code&gt;data + parity&lt;/code&gt; can't exceed the number of drives.&lt;/p&gt;

&lt;p&gt;RS(2,1) across three drives will survive losing any one of them (for two drives, use RS(1,1)). Your data survives, but the service won't start while a drive's bind mount is missing, so mount the replacement drive at the same path, recreate its &lt;code&gt;.predastore&lt;/code&gt; directory, and run &lt;code&gt;sudo mount -a&lt;/code&gt; before restarting. &lt;code&gt;[rs]&lt;/code&gt; can't be changed once objects have been written, so make sure you're happy with it before moving any data across.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/mulgadc/predastore#configuration" rel="noopener noreferrer"&gt;configuration reference&lt;/a&gt; covers the details.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[rs]&lt;/span&gt;
&lt;span class="py"&gt;data&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
&lt;span class="py"&gt;parity&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Generate keys and a certificate
&lt;/h4&gt;

&lt;p&gt;Generate the key used to encrypt data at rest, along with a self-signed TLS certificate. &lt;code&gt;PREDA_SAN&lt;/code&gt; should list every hostname and IP address your clients will use to reach Predastore, otherwise they'll refuse to trust the certificate when you come to copy data across. It replaces the default list rather than adding to it, so include &lt;code&gt;DNS:localhost&lt;/code&gt; and &lt;code&gt;IP:127.0.0.1&lt;/code&gt; if you'll also be connecting from the machine itself.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;&lt;span class="nv"&gt;PREDA_SAN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"DNS:s3.example.com,IP:192.0.2.10"&lt;/span&gt; predastore-keygen /etc/predastore
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Note: Make sure to back up &lt;code&gt;/etc/predastore/master.key&lt;/code&gt; somewhere separate from your data backups. Without it your data is unreadable, but storing it alongside the data backups defeats the purpose of encrypting at all.&lt;/em&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Start the service
&lt;/h4&gt;

&lt;p&gt;Enable and start the service, then follow its logs to make sure it comes up without any errors:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl daemon-reload
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable&lt;/span&gt; &lt;span class="nt"&gt;--now&lt;/span&gt; predastore
&lt;span class="nb"&gt;sudo &lt;/span&gt;journalctl &lt;span class="nt"&gt;-u&lt;/span&gt; predastore &lt;span class="nt"&gt;-f&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Note: A few warnings while the nodes start up are normal, such as the gate reporting it isn't ready yet. It's errors, and the service restarting, that you're looking out for.&lt;/em&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Check it's up
&lt;/h4&gt;

&lt;p&gt;With the service running, it's worth confirming that Predastore is reachable, and that your client and cluster agree on the region.&lt;/p&gt;

&lt;p&gt;The certificate is only readable by the &lt;code&gt;predastore&lt;/code&gt; user, so copy it somewhere your own user can read (and over to any other machine you'll be running clients from). The rest of this guide uses this copy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo install&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt; 0644 &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$USER&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; /etc/predastore/server.pem ~/predastore-ca.pem
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Point the AWS CLI at the credentials from your &lt;code&gt;[[auth]]&lt;/code&gt; entry, and at the certificate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;AWS_ACCESS_KEY_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;access-key&amp;gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;AWS_SECRET_ACCESS_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;secret-key&amp;gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;AWS_CA_BUNDLE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;~/predastore-ca.pem
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then create, list, and remove a test bucket. Listing buckets alone won't catch a region mismatch, which is why the check creates one.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws s3 mb s3://predastore-test &lt;span class="nt"&gt;--endpoint-url&lt;/span&gt; https://s3.example.com:8443 &lt;span class="nt"&gt;--region&lt;/span&gt; us-east-1
aws s3 &lt;span class="nb"&gt;ls &lt;/span&gt;s3://predastore-test &lt;span class="nt"&gt;--endpoint-url&lt;/span&gt; https://s3.example.com:8443 &lt;span class="nt"&gt;--region&lt;/span&gt; us-east-1
aws s3 rb s3://predastore-test &lt;span class="nt"&gt;--endpoint-url&lt;/span&gt; https://s3.example.com:8443 &lt;span class="nt"&gt;--region&lt;/span&gt; us-east-1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If all three commands complete without any errors, your Predastore cluster is ready for data. If you see &lt;code&gt;AuthorizationHeaderMalformed ... incorrect region&lt;/code&gt;, the region your client is signing for doesn't match the one Predastore was set up with.&lt;/p&gt;



&lt;br&gt;
&lt;p&gt;&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;
  Multi-Node Multi-Drive - Standalone
  &lt;p&gt;This path is the equivalent of a distributed MinIO deployment. Every machine runs the same config file, and shares a single TLS certificate and encryption key.&lt;/p&gt;

&lt;p&gt;The steps below assume three machines at &lt;code&gt;10.0.0.1&lt;/code&gt;, &lt;code&gt;10.0.0.2&lt;/code&gt;, and &lt;code&gt;10.0.0.3&lt;/code&gt;, but the process is the same for any number of them. You'll need Go (1.27 or newer), &lt;code&gt;git&lt;/code&gt;, &lt;code&gt;make&lt;/code&gt;, and &lt;code&gt;openssl&lt;/code&gt; on each machine, as well as the AWS CLI on at least one of them for checking the cluster once it's up.&lt;/p&gt;
&lt;h4&gt;
  
  
  Build and install
&lt;/h4&gt;

&lt;p&gt;On every machine, clone the repository and build the &lt;code&gt;s3d&lt;/code&gt; binary:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone &lt;span class="nt"&gt;--branch&lt;/span&gt; v1.21.0 https://github.com/mulgadc/predastore.git
&lt;span class="nb"&gt;cd &lt;/span&gt;predastore
make build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Note: This guide was written against v1.21.0. The install tooling used below (&lt;code&gt;make install&lt;/code&gt;, &lt;code&gt;predastore-keygen&lt;/code&gt;, and the example config) first shipped in v1.20.0, so don't go any older than that.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Install it, along with its systemd service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;make &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create the dedicated &lt;code&gt;predastore&lt;/code&gt; user, and the config and data directories:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemd-sysusers &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;systemd-tmpfiles &lt;span class="nt"&gt;--create&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;make install&lt;/code&gt; also ships some kernel network settings. Hosts on separate machines talk to each other over QUIC, and need larger socket buffers than most distros provide by default. Without them, writes will start failing under load, with no obvious indication as to why. Apply them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;sysctl &lt;span class="nt"&gt;--system&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Share MinIO's drives
&lt;/h4&gt;

&lt;p&gt;There's no need for new drives. Predastore can live on the ones MinIO is already using, in a directory of its own alongside MinIO's data, until you're done with MinIO in step 5. Each drive needs roughly as much free space again as MinIO is using on it, plus some headroom for re-syncs, which &lt;code&gt;df -h&lt;/code&gt; will tell you.&lt;/p&gt;

&lt;p&gt;The steps below assume each machine's MinIO drives are mounted at &lt;code&gt;/mnt/disk1&lt;/code&gt;, &lt;code&gt;/mnt/disk2&lt;/code&gt;, and so on. If they're pooled with RAID or ZFS, treat the pool as a single drive. The pool covers a failed drive, and Predastore covers a failed machine.&lt;/p&gt;

&lt;p&gt;On every machine, create a directory for Predastore on each drive. The leading &lt;code&gt;.&lt;/code&gt; matters, as MinIO treats any other top-level directory on its drives as a bucket, and will delete it along with that bucket.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo install&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; predastore &lt;span class="nt"&gt;-g&lt;/span&gt; predastore &lt;span class="nt"&gt;-m&lt;/span&gt; 0700 /mnt/disk1/.predastore
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The service isn't permitted to write anywhere outside &lt;code&gt;/var/lib/predastore&lt;/code&gt;, so bind-mount each directory into it. The extra options make sure the service waits for the mount, and refuses to start without it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo install&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt; 0755 /var/lib/predastore/disk1
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'/mnt/disk1/.predastore /var/lib/predastore/disk1 none bind,nofail,x-systemd.requires-mounts-for=/mnt/disk1,x-systemd.required-by=predastore.service,x-systemd.before=predastore.service 0 0'&lt;/span&gt; | &lt;span class="nb"&gt;sudo tee&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt; /etc/fstab
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once every drive has its line in &lt;code&gt;/etc/fstab&lt;/code&gt;, mount them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl daemon-reload
&lt;span class="nb"&gt;sudo &lt;/span&gt;mount &lt;span class="nt"&gt;-a&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Note: Leave &lt;code&gt;/var/lib/predastore/disk1&lt;/code&gt; and friends owned by root. If a mount ever goes missing, that's what stops Predastore from quietly writing to the system disk in its place.&lt;/em&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Describe the cluster
&lt;/h4&gt;

&lt;p&gt;A single config file describes the entire cluster, with one &lt;code&gt;[[host]]&lt;/code&gt; block per machine. Each host gets its own address, its data directory, key, and certificate paths, and its own gate, meta, and blob nodes. Node ids must be unique across the whole file, not just within a host.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;region&lt;/code&gt; needs to match whatever your MinIO clients are configured with, as S3 clients sign every request for a specific region. &lt;code&gt;[[auth]]&lt;/code&gt; holds the access key and secret your applications will use.&lt;/p&gt;

&lt;p&gt;Keep in mind that these are root credentials, with full access to every bucket in the cluster (as mentioned in the standalone vs Spinifex discussion).&lt;/p&gt;

&lt;p&gt;&lt;code&gt;[rs]&lt;/code&gt; determines how objects are spread across the machines. Each object is split into &lt;code&gt;data&lt;/code&gt; shards plus &lt;code&gt;parity&lt;/code&gt; shards, each on a different blob node. RS(2,1) across three machines will survive losing any one of them.&lt;/p&gt;

&lt;p&gt;Check out the &lt;a href="https://github.com/mulgadc/predastore#configuration" rel="noopener noreferrer"&gt;configuration reference&lt;/a&gt; for the full format.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="py"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="py"&gt;region&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"us-east-1"&lt;/span&gt;

&lt;span class="nn"&gt;[rs]&lt;/span&gt;
&lt;span class="py"&gt;data&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
&lt;span class="py"&gt;parity&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;

&lt;span class="nn"&gt;[[host]]&lt;/span&gt;
&lt;span class="py"&gt;id&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="py"&gt;addr&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"10.0.0.1"&lt;/span&gt;
&lt;span class="py"&gt;admin_port&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;9099&lt;/span&gt;
&lt;span class="py"&gt;data_dir&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"/var/lib/predastore"&lt;/span&gt;
&lt;span class="py"&gt;tls_cert&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"/etc/predastore/server.pem"&lt;/span&gt;
&lt;span class="py"&gt;tls_key&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"/etc/predastore/server.key"&lt;/span&gt;
&lt;span class="py"&gt;encryption_key&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"/etc/predastore/master.key"&lt;/span&gt;

  &lt;span class="nn"&gt;[[host.node]]&lt;/span&gt;
  &lt;span class="py"&gt;id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
  &lt;span class="py"&gt;role&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"gate"&lt;/span&gt;
  &lt;span class="py"&gt;port&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8443&lt;/span&gt;
  &lt;span class="py"&gt;bind_addr&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"0.0.0.0"&lt;/span&gt;

  &lt;span class="nn"&gt;[[host.node]]&lt;/span&gt;
  &lt;span class="py"&gt;id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
  &lt;span class="py"&gt;role&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"meta"&lt;/span&gt;
  &lt;span class="py"&gt;port&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;6660&lt;/span&gt;

  &lt;span class="nn"&gt;[[host.node]]&lt;/span&gt;
  &lt;span class="py"&gt;id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
  &lt;span class="py"&gt;role&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"blob"&lt;/span&gt;
  &lt;span class="py"&gt;port&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;9991&lt;/span&gt;

&lt;span class="nn"&gt;[[host]]&lt;/span&gt;
&lt;span class="py"&gt;id&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
&lt;span class="py"&gt;addr&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"10.0.0.2"&lt;/span&gt;
&lt;span class="py"&gt;admin_port&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;9099&lt;/span&gt;
&lt;span class="py"&gt;data_dir&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"/var/lib/predastore"&lt;/span&gt;
&lt;span class="py"&gt;tls_cert&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"/etc/predastore/server.pem"&lt;/span&gt;
&lt;span class="py"&gt;tls_key&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"/etc/predastore/server.key"&lt;/span&gt;
&lt;span class="py"&gt;encryption_key&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"/etc/predastore/master.key"&lt;/span&gt;

  &lt;span class="nn"&gt;[[host.node]]&lt;/span&gt;
  &lt;span class="py"&gt;id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;
  &lt;span class="py"&gt;role&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"gate"&lt;/span&gt;
  &lt;span class="py"&gt;port&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8443&lt;/span&gt;
  &lt;span class="py"&gt;bind_addr&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"0.0.0.0"&lt;/span&gt;

  &lt;span class="nn"&gt;[[host.node]]&lt;/span&gt;
  &lt;span class="py"&gt;id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;
  &lt;span class="py"&gt;role&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"meta"&lt;/span&gt;
  &lt;span class="py"&gt;port&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;6660&lt;/span&gt;

  &lt;span class="nn"&gt;[[host.node]]&lt;/span&gt;
  &lt;span class="py"&gt;id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;
  &lt;span class="py"&gt;role&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"blob"&lt;/span&gt;
  &lt;span class="py"&gt;port&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;9991&lt;/span&gt;

&lt;span class="nn"&gt;[[host]]&lt;/span&gt;
&lt;span class="py"&gt;id&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
&lt;span class="py"&gt;addr&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"10.0.0.3"&lt;/span&gt;
&lt;span class="py"&gt;admin_port&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;9099&lt;/span&gt;
&lt;span class="py"&gt;data_dir&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"/var/lib/predastore"&lt;/span&gt;
&lt;span class="py"&gt;tls_cert&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"/etc/predastore/server.pem"&lt;/span&gt;
&lt;span class="py"&gt;tls_key&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"/etc/predastore/server.key"&lt;/span&gt;
&lt;span class="py"&gt;encryption_key&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"/etc/predastore/master.key"&lt;/span&gt;

  &lt;span class="nn"&gt;[[host.node]]&lt;/span&gt;
  &lt;span class="py"&gt;id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;
  &lt;span class="py"&gt;role&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"gate"&lt;/span&gt;
  &lt;span class="py"&gt;port&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8443&lt;/span&gt;
  &lt;span class="py"&gt;bind_addr&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"0.0.0.0"&lt;/span&gt;

  &lt;span class="nn"&gt;[[host.node]]&lt;/span&gt;
  &lt;span class="py"&gt;id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;
  &lt;span class="py"&gt;role&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"meta"&lt;/span&gt;
  &lt;span class="py"&gt;port&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;6660&lt;/span&gt;

  &lt;span class="nn"&gt;[[host.node]]&lt;/span&gt;
  &lt;span class="py"&gt;id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;9&lt;/span&gt;
  &lt;span class="py"&gt;role&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"blob"&lt;/span&gt;
  &lt;span class="py"&gt;port&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;9991&lt;/span&gt;

&lt;span class="nn"&gt;[[auth]]&lt;/span&gt;
&lt;span class="py"&gt;access_key_id&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"&amp;lt;access-key&amp;gt;"&lt;/span&gt;
&lt;span class="py"&gt;secret_access_key&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"&amp;lt;secret-key&amp;gt;"&lt;/span&gt;
&lt;span class="py"&gt;account_id&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"100000000001"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Save it as &lt;code&gt;/etc/predastore/predastore.toml&lt;/code&gt; on every machine.&lt;/p&gt;

&lt;p&gt;Since every machine shares the same config file, each one needs telling which &lt;code&gt;[[host]]&lt;/code&gt; block describes it. That lives in &lt;code&gt;/etc/predastore/predastore.env&lt;/code&gt;. On each machine, copy the example into place, and open it up:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo cp&lt;/span&gt; /etc/predastore/predastore.env.example /etc/predastore/predastore.env
&lt;span class="nb"&gt;sudo&lt;/span&gt; &lt;span class="nv"&gt;$EDITOR&lt;/span&gt; /etc/predastore/predastore.env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Set &lt;code&gt;PREDA_HOST_ID&lt;/code&gt; to the id of the &lt;code&gt;[[host]]&lt;/code&gt; block that describes this machine, so &lt;code&gt;1&lt;/code&gt; on &lt;code&gt;10.0.0.1&lt;/code&gt;, &lt;code&gt;2&lt;/code&gt; on &lt;code&gt;10.0.0.2&lt;/code&gt;, and so on. The rest of the file can be left as it is.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PREDA_HOST_ID=1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, point the blob nodes at the drives. If each machine only has one drive, add &lt;code&gt;data_dir = "/var/lib/predastore/disk1"&lt;/code&gt; to every host's blob node, and the config is done.&lt;/p&gt;

&lt;p&gt;Otherwise, give each host one blob node per drive, each with its own id, port, and &lt;code&gt;data_dir&lt;/code&gt; pointing at the drive's bind mount. Leave each host's &lt;code&gt;data_dir&lt;/code&gt; as it is, since the meta node keeps its state there.&lt;/p&gt;

&lt;p&gt;With two drives per machine, host 1 looks like this, and hosts 2 and 3 follow the same pattern with node ids 5 to 8 and 9 to 12:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[[host]]&lt;/span&gt;
&lt;span class="py"&gt;id&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="py"&gt;addr&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"10.0.0.1"&lt;/span&gt;
&lt;span class="py"&gt;admin_port&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;9099&lt;/span&gt;
&lt;span class="py"&gt;data_dir&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"/var/lib/predastore"&lt;/span&gt;
&lt;span class="py"&gt;tls_cert&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"/etc/predastore/server.pem"&lt;/span&gt;
&lt;span class="py"&gt;tls_key&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"/etc/predastore/server.key"&lt;/span&gt;
&lt;span class="py"&gt;encryption_key&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"/etc/predastore/master.key"&lt;/span&gt;

  &lt;span class="nn"&gt;[[host.node]]&lt;/span&gt;
  &lt;span class="py"&gt;id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
  &lt;span class="py"&gt;role&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"gate"&lt;/span&gt;
  &lt;span class="py"&gt;port&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8443&lt;/span&gt;
  &lt;span class="py"&gt;bind_addr&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"0.0.0.0"&lt;/span&gt;

  &lt;span class="nn"&gt;[[host.node]]&lt;/span&gt;
  &lt;span class="py"&gt;id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
  &lt;span class="py"&gt;role&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"meta"&lt;/span&gt;
  &lt;span class="py"&gt;port&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;6660&lt;/span&gt;

  &lt;span class="nn"&gt;[[host.node]]&lt;/span&gt;
  &lt;span class="py"&gt;id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
  &lt;span class="py"&gt;role&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"blob"&lt;/span&gt;
  &lt;span class="py"&gt;port&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;9991&lt;/span&gt;
  &lt;span class="py"&gt;data_dir&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"/var/lib/predastore/disk1"&lt;/span&gt;

  &lt;span class="nn"&gt;[[host.node]]&lt;/span&gt;
  &lt;span class="py"&gt;id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;
  &lt;span class="py"&gt;role&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"blob"&lt;/span&gt;
  &lt;span class="py"&gt;port&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;9992&lt;/span&gt;
  &lt;span class="py"&gt;data_dir&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"/var/lib/predastore/disk2"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, set &lt;code&gt;[rs]&lt;/code&gt; to match, which needs some care. Predastore spreads an object's shards across distinct blob nodes, but doesn't take into account which machine each blob node lives on, so two shards of the same object can land on two drives in the same machine.&lt;/p&gt;

&lt;p&gt;To survive losing a whole machine, &lt;code&gt;parity&lt;/code&gt; needs to be at least the number of drives per machine, and &lt;code&gt;data + parity&lt;/code&gt; can't exceed the total number of drives. For three machines with two drives each, RS(2,2) survives losing any one machine. &lt;code&gt;[rs]&lt;/code&gt; can't be changed once objects have been written, so make sure you're happy with it before moving any data across.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[rs]&lt;/span&gt;
&lt;span class="py"&gt;data&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
&lt;span class="py"&gt;parity&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Generate keys and a certificate
&lt;/h4&gt;

&lt;p&gt;Every machine in the cluster needs to share the same TLS certificate and the same encryption key. This one is a bit of a trap; machines with their own certificates never manage to form a cluster.&lt;/p&gt;

&lt;p&gt;So rather than running &lt;code&gt;predastore-keygen&lt;/code&gt; on each machine straight away, generate the files once on a single machine and distribute them yourself.&lt;/p&gt;

&lt;p&gt;On one machine, generate a self-signed certificate. Make sure &lt;code&gt;subjectAltName&lt;/code&gt; lists every machine's address, as well as the hostname your clients will use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;openssl req &lt;span class="nt"&gt;-x509&lt;/span&gt; &lt;span class="nt"&gt;-newkey&lt;/span&gt; rsa:2048 &lt;span class="nt"&gt;-nodes&lt;/span&gt; &lt;span class="nt"&gt;-days&lt;/span&gt; 825 &lt;span class="nt"&gt;-subj&lt;/span&gt; &lt;span class="s1"&gt;'/CN=predastore'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-addext&lt;/span&gt; &lt;span class="s2"&gt;"subjectAltName=DNS:s3.example.com,IP:10.0.0.1,IP:10.0.0.2,IP:10.0.0.3"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-keyout&lt;/span&gt; /etc/predastore/server.key &lt;span class="nt"&gt;-out&lt;/span&gt; /etc/predastore/server.pem
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the same machine, generate the encryption key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;sh &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'umask 0177 &amp;amp;&amp;amp; openssl rand -out /etc/predastore/master.key 32'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Copy &lt;code&gt;server.pem&lt;/code&gt;, &lt;code&gt;server.key&lt;/code&gt; and &lt;code&gt;master.key&lt;/code&gt; into &lt;code&gt;/etc/predastore&lt;/code&gt; on every other machine, using something that preserves file permissions, such as &lt;code&gt;scp -p&lt;/code&gt;. Predastore refuses to start if either key is readable by anyone other than its owner, so if in doubt, tighten them up on each machine once they've landed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo chmod &lt;/span&gt;0600 /etc/predastore/master.key /etc/predastore/server.key
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, on every machine, run &lt;code&gt;predastore-keygen&lt;/code&gt;. It keeps the copied files, and sets their ownership:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;predastore-keygen /etc/predastore
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Still on every machine, add the certificate to the system trust store, so each host trusts the others:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo cp&lt;/span&gt; /etc/predastore/server.pem /usr/local/share/ca-certificates/predastore.crt
&lt;span class="nb"&gt;sudo &lt;/span&gt;update-ca-certificates
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Note: The trust store commands above are for Debian and Ubuntu. For RHEL and Fedora, see the &lt;a href="https://github.com/mulgadc/predastore#standalone-tls-trust" rel="noopener noreferrer"&gt;Standalone TLS Trust&lt;/a&gt; section of the README. As with the single node setup, make sure to back up &lt;code&gt;master.key&lt;/code&gt; somewhere separate from your data backups.&lt;/em&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Start the service
&lt;/h4&gt;

&lt;p&gt;On every machine, enable and start the service, then follow its logs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl daemon-reload
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable&lt;/span&gt; &lt;span class="nt"&gt;--now&lt;/span&gt; predastore
&lt;span class="nb"&gt;sudo &lt;/span&gt;journalctl &lt;span class="nt"&gt;-u&lt;/span&gt; predastore &lt;span class="nt"&gt;-f&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Until every machine is up, the ones that started first will log errors about not being able to reach the others, which is expected. Once the service is running on every machine, the hosts will find each other and elect a leader, and those errors should stop. From then on, it's errors and the service restarting that you're looking out for.&lt;/p&gt;

&lt;h4&gt;
  
  
  Check it's up
&lt;/h4&gt;

&lt;p&gt;With the cluster running, it's worth confirming that Predastore is reachable, and that your client and cluster agree on the region. Run the following from any one of the machines.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;s3.example.com&lt;/code&gt; should point at every machine's gate, not just one, either with a DNS record per machine or a load balancer in front of them. Clients only talk to the gates that name resolves to, so if it names a single machine, losing that machine takes Predastore offline for your applications, even though the rest of the cluster carries on.&lt;/p&gt;

&lt;p&gt;The certificate is only readable by the &lt;code&gt;predastore&lt;/code&gt; user, so copy it somewhere your own user can read (and over to any other machine you'll be running clients from). The rest of this guide uses this copy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo install&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt; 0644 &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$USER&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; /etc/predastore/server.pem ~/predastore-ca.pem
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Point the AWS CLI at the credentials from your &lt;code&gt;[[auth]]&lt;/code&gt; entry, and at the certificate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;AWS_ACCESS_KEY_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;access-key&amp;gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;AWS_SECRET_ACCESS_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;secret-key&amp;gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;AWS_CA_BUNDLE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;~/predastore-ca.pem
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then create, list, and remove a test bucket. Listing buckets alone won't catch a region mismatch, which is why the check creates one.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws s3 mb s3://predastore-test &lt;span class="nt"&gt;--endpoint-url&lt;/span&gt; https://s3.example.com:8443 &lt;span class="nt"&gt;--region&lt;/span&gt; us-east-1
aws s3 &lt;span class="nb"&gt;ls &lt;/span&gt;s3://predastore-test &lt;span class="nt"&gt;--endpoint-url&lt;/span&gt; https://s3.example.com:8443 &lt;span class="nt"&gt;--region&lt;/span&gt; us-east-1
aws s3 rb s3://predastore-test &lt;span class="nt"&gt;--endpoint-url&lt;/span&gt; https://s3.example.com:8443 &lt;span class="nt"&gt;--region&lt;/span&gt; us-east-1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If all three commands complete without any errors, your Predastore cluster is ready for data. If you see &lt;code&gt;AuthorizationHeaderMalformed ... incorrect region&lt;/code&gt;, the region your client is signing for doesn't match the one Predastore was set up with.&lt;/p&gt;



&lt;br&gt;
&lt;p&gt;&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;
  Single-Node Single/Multi-Drive - Via Spinifex
  &lt;p&gt;&lt;em&gt;A caveat before you start: Spinifex currently keeps all of Predastore's data on a single drive. While MinIO is still running, that means picking one of MinIO's drives to hold everything, which limits Predastore to that drive's capacity, with no protection against losing it. If MinIO's drives are already pooled with RAID or ZFS, the pool counts as one drive and keeps its redundancy. Support for spreading Predastore across multiple drives under Spinifex is on its way. Until then, if you need to survive a failed drive and can live without IAM, the standalone path is the better fit.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If you've gone with Spinifex, Predastore comes along for the ride. It's installed and configured as part of Spinifex, so there's no need to set it up separately.&lt;/p&gt;

&lt;p&gt;On a single node, Spinifex runs Predastore with one blob node and RS(1,0), which means a single copy of each object with no parity.&lt;/p&gt;
&lt;h4&gt;
  
  
  Run the installer
&lt;/h4&gt;

&lt;p&gt;Before installing, make sure the machine has a Linux bridge configured for VM networking. The &lt;a href="https://docs.mulgadc.com/docs/vpc-networking#bridge-setup-physical-network-wiring" rel="noopener noreferrer"&gt;bridge setup guide&lt;/a&gt; walks through setting one up.&lt;/p&gt;

&lt;p&gt;Run the installer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://install.mulgadc.com | bash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then set up OVN networking. &lt;code&gt;--management&lt;/code&gt; also starts OVN's central services, which a single node needs to run itself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo&lt;/span&gt; /usr/local/share/spinifex/setup-ovn.sh &lt;span class="nt"&gt;--management&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Initialize the node
&lt;/h4&gt;

&lt;p&gt;Initializing the node generates Predastore's config, keys, and certificates for you. Just like the standalone setup, &lt;code&gt;--region&lt;/code&gt; needs to match your MinIO clients, as Spinifex defaults to &lt;code&gt;ap-southeast-2&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;spx admin init &lt;span class="nt"&gt;--node&lt;/span&gt; node1 &lt;span class="nt"&gt;--nodes&lt;/span&gt; 1 &lt;span class="nt"&gt;--region&lt;/span&gt; us-east-1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Share one of MinIO's drives
&lt;/h4&gt;

&lt;p&gt;Predastore can live on one of the drives MinIO is already using, in a directory of its own alongside MinIO's data, until you're done with MinIO in step 5. Pick the drive with the most free space, as it needs enough room for a full copy of your data, plus some headroom for re-syncs.&lt;/p&gt;

&lt;p&gt;The steps below assume that drive is mounted at &lt;code&gt;/mnt/disk1&lt;/code&gt;. Create a directory for Predastore on it. The leading &lt;code&gt;.&lt;/code&gt; matters, as MinIO treats any other top-level directory on its drives as a bucket, and will delete it along with that bucket.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo install&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; spinifex-storage &lt;span class="nt"&gt;-g&lt;/span&gt; spinifex &lt;span class="nt"&gt;-m&lt;/span&gt; 0700 /mnt/disk1/.spinifex-predastore
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Predastore's service isn't permitted to write anywhere outside &lt;code&gt;/var/lib/spinifex/predastore&lt;/code&gt;, so bind-mount the directory over it. The extra options make sure the service waits for the mount, and won't write to the system disk without it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo chown &lt;/span&gt;root:root /var/lib/spinifex/predastore
&lt;span class="nb"&gt;sudo chmod &lt;/span&gt;0755 /var/lib/spinifex/predastore
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'/mnt/disk1/.spinifex-predastore /var/lib/spinifex/predastore none bind,nofail,x-systemd.requires-mounts-for=/mnt/disk1,x-systemd.required-by=spinifex-predastore.service,x-systemd.before=spinifex-predastore.service 0 0'&lt;/span&gt; | &lt;span class="nb"&gt;sudo tee&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt; /etc/fstab
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl daemon-reload
&lt;span class="nb"&gt;sudo &lt;/span&gt;mount &lt;span class="nt"&gt;-a&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Start the services
&lt;/h4&gt;

&lt;p&gt;Start Spinifex's services, Predastore included:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl start spinifex.target
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Spinifex's certificate only covers the machine's own hostname and addresses. If your clients will reach Predastore by any other name, such as &lt;code&gt;s3.example.com&lt;/code&gt;, add it to the certificate, and restart the services to pick it up:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;spx admin cert renew &lt;span class="nt"&gt;--extra-dns&lt;/span&gt; s3.example.com
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl restart spinifex.target
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Check it's up
&lt;/h4&gt;

&lt;p&gt;With Spinifex running, it's worth confirming that Predastore is reachable, and that your client and cluster agree on the region.&lt;/p&gt;

&lt;p&gt;Copy Spinifex's CA certificate somewhere your own user can read (and over to any other machine you'll be running clients from). The rest of this guide uses this copy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo install&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt; 0644 &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$USER&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; /etc/spinifex/ca.pem ~/predastore-ca.pem
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Point the AWS CLI at the &lt;code&gt;spinifex&lt;/code&gt; profile created during setup, which takes care of the credentials, and at your copy of the certificate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;AWS_PROFILE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;spinifex
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;AWS_CA_BUNDLE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;~/predastore-ca.pem
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then create, list, and remove a test bucket. Listing buckets alone won't catch a region mismatch, which is why the check creates one.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws s3 mb s3://predastore-test &lt;span class="nt"&gt;--endpoint-url&lt;/span&gt; https://s3.example.com:8443 &lt;span class="nt"&gt;--region&lt;/span&gt; us-east-1
aws s3 &lt;span class="nb"&gt;ls &lt;/span&gt;s3://predastore-test &lt;span class="nt"&gt;--endpoint-url&lt;/span&gt; https://s3.example.com:8443 &lt;span class="nt"&gt;--region&lt;/span&gt; us-east-1
aws s3 rb s3://predastore-test &lt;span class="nt"&gt;--endpoint-url&lt;/span&gt; https://s3.example.com:8443 &lt;span class="nt"&gt;--region&lt;/span&gt; us-east-1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If all three commands complete without any errors, your Predastore cluster is ready for data. If you see &lt;code&gt;AuthorizationHeaderMalformed ... incorrect region&lt;/code&gt;, the region your client is signing for doesn't match the one you passed to &lt;code&gt;spx admin init&lt;/code&gt;.&lt;/p&gt;



&lt;br&gt;
&lt;p&gt;&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;
  Multi-Node Multi-Drive - Via Spinifex
  &lt;p&gt;&lt;em&gt;A caveat before you start: Spinifex currently keeps all of each machine's Predastore data on a single drive. While MinIO is still running, that means picking one of MinIO's drives on each machine, which limits each machine's share of Predastore to that drive's capacity. Predastore's erasure coding still covers a failed machine, and it treats a failed drive as a failed machine, but there's no protection against losing a drive beyond that. If MinIO's drives are already pooled with RAID or ZFS, the pool counts as one drive and keeps its redundancy. Support for spreading Predastore across multiple drives under Spinifex is on its way. Until then, if you need to survive more failed drives than failed machines and can live without IAM, the standalone path is the better fit.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Setting up a multi-node Spinifex cluster involves a fair bit more than Predastore alone, including configuring OVN networking on each server.&lt;/p&gt;

&lt;p&gt;Rather than repeating all of it here, I'll point you to Spinifex's &lt;a href="https://docs.mulgadc.com/docs/install-multi-node" rel="noopener noreferrer"&gt;multi-node install guide&lt;/a&gt; for the full procedure, and only call out the parts that matter for the migration.&lt;/p&gt;

&lt;p&gt;Spinifex runs one Predastore blob node per machine, so its erasure coding protects you against losing a machine.&lt;/p&gt;
&lt;h4&gt;
  
  
  Follow the install guide
&lt;/h4&gt;

&lt;p&gt;Work through steps 1 to 3 of the multi-node guide, which cover installing Spinifex, setting the node IP variables, and configuring OVN networking on each server.&lt;/p&gt;
&lt;h4&gt;
  
  
  Form the cluster
&lt;/h4&gt;

&lt;p&gt;Step 4 of the guide forms the cluster, with one change; &lt;code&gt;--region&lt;/code&gt; needs to match your MinIO clients, on both the init and every join. Spinifex picks the erasure coding for you, using RS(1,1) for two machines and RS(2,1) for three or more.&lt;/p&gt;

&lt;p&gt;On server 1, initialize the cluster:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;spx admin init &lt;span class="nt"&gt;--force&lt;/span&gt; &lt;span class="nt"&gt;--node&lt;/span&gt; node1 &lt;span class="nt"&gt;--nodes&lt;/span&gt; 3 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--bind&lt;/span&gt; &lt;span class="nv"&gt;$SPINIFEX_NODE1&lt;/span&gt; &lt;span class="nt"&gt;--cluster-bind&lt;/span&gt; &lt;span class="nv"&gt;$SPINIFEX_NODE1&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--port&lt;/span&gt; 4432 &lt;span class="nt"&gt;--region&lt;/span&gt; us-east-1 &lt;span class="nt"&gt;--az&lt;/span&gt; us-east-1a
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;While init is still running, join each other server, using the token from init's output. Change &lt;code&gt;--node&lt;/code&gt; and the bind addresses to match each server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;spx admin &lt;span class="nb"&gt;join&lt;/span&gt; &lt;span class="nt"&gt;--force&lt;/span&gt; &lt;span class="nt"&gt;--node&lt;/span&gt; node2 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--bind&lt;/span&gt; &lt;span class="nv"&gt;$SPINIFEX_NODE2&lt;/span&gt; &lt;span class="nt"&gt;--cluster-bind&lt;/span&gt; &lt;span class="nv"&gt;$SPINIFEX_NODE2&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--host&lt;/span&gt; &lt;span class="nv"&gt;$SPINIFEX_NODE1&lt;/span&gt;:4432 &lt;span class="nt"&gt;--token&lt;/span&gt; &amp;lt;token-from-init-output&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; us-east-1 &lt;span class="nt"&gt;--az&lt;/span&gt; us-east-1a
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Share one of MinIO's drives
&lt;/h4&gt;

&lt;p&gt;On each machine, Predastore can live on one of the drives MinIO is already using, in a directory of its own alongside MinIO's data, until you're done with MinIO in step 5. Pick the drive with the most free space, as it needs roughly as much room as MinIO is using across all of that machine's drives combined, plus some headroom for re-syncs.&lt;/p&gt;

&lt;p&gt;The steps below assume that drive is mounted at &lt;code&gt;/mnt/disk1&lt;/code&gt;. On every server, create a directory for Predastore on it. The leading &lt;code&gt;.&lt;/code&gt; matters, as MinIO treats any other top-level directory on its drives as a bucket, and will delete it along with that bucket.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo install&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; spinifex-storage &lt;span class="nt"&gt;-g&lt;/span&gt; spinifex &lt;span class="nt"&gt;-m&lt;/span&gt; 0700 /mnt/disk1/.spinifex-predastore
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Predastore's service isn't permitted to write anywhere outside &lt;code&gt;/var/lib/spinifex/predastore&lt;/code&gt;, so on every server, bind-mount the directory over it. The extra options make sure the service waits for the mount, and won't write to the system disk without it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo chown &lt;/span&gt;root:root /var/lib/spinifex/predastore
&lt;span class="nb"&gt;sudo chmod &lt;/span&gt;0755 /var/lib/spinifex/predastore
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'/mnt/disk1/.spinifex-predastore /var/lib/spinifex/predastore none bind,nofail,x-systemd.requires-mounts-for=/mnt/disk1,x-systemd.required-by=spinifex-predastore.service,x-systemd.before=spinifex-predastore.service 0 0'&lt;/span&gt; | &lt;span class="nb"&gt;sudo tee&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt; /etc/fstab
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl daemon-reload
&lt;span class="nb"&gt;sudo &lt;/span&gt;mount &lt;span class="nt"&gt;-a&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Start the services
&lt;/h4&gt;

&lt;p&gt;Finish off with steps 5 and 6 of the multi-node guide to start the services and verify the cluster.&lt;/p&gt;

&lt;p&gt;As with a single node, Spinifex's certificates only cover each server's own hostname and addresses. If your clients will reach Predastore by any other name, such as &lt;code&gt;s3.example.com&lt;/code&gt;, add it on every server, and restart the services to pick it up:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;spx admin cert renew &lt;span class="nt"&gt;--extra-dns&lt;/span&gt; s3.example.com
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl restart spinifex.target
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Check it's up
&lt;/h4&gt;

&lt;p&gt;With the cluster running, it's worth confirming that Predastore is reachable, and that your client and cluster agree on the region. Run the following on server 1.&lt;/p&gt;

&lt;p&gt;Copy Spinifex's CA certificate somewhere your own user can read (and over to any other machine you'll be running clients from). The rest of this guide uses this copy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo install&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt; 0644 &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$USER&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; /etc/spinifex/ca.pem ~/predastore-ca.pem
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Point the AWS CLI at the &lt;code&gt;spinifex&lt;/code&gt; profile created during setup, which takes care of the credentials, and at your copy of the certificate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;AWS_PROFILE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;spinifex
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;AWS_CA_BUNDLE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;~/predastore-ca.pem
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then create, list, and remove a test bucket. Listing buckets alone won't catch a region mismatch, which is why the check creates one.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws s3 mb s3://predastore-test &lt;span class="nt"&gt;--endpoint-url&lt;/span&gt; https://s3.example.com:8443 &lt;span class="nt"&gt;--region&lt;/span&gt; us-east-1
aws s3 &lt;span class="nb"&gt;ls &lt;/span&gt;s3://predastore-test &lt;span class="nt"&gt;--endpoint-url&lt;/span&gt; https://s3.example.com:8443 &lt;span class="nt"&gt;--region&lt;/span&gt; us-east-1
aws s3 rb s3://predastore-test &lt;span class="nt"&gt;--endpoint-url&lt;/span&gt; https://s3.example.com:8443 &lt;span class="nt"&gt;--region&lt;/span&gt; us-east-1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If all three commands complete without any errors, your Predastore cluster is ready for data. If you see &lt;code&gt;AuthorizationHeaderMalformed ... incorrect region&lt;/code&gt;, the region your client is signing for doesn't match the one you passed to &lt;code&gt;spx admin init&lt;/code&gt;.&lt;/p&gt;



&lt;br&gt;
&lt;p&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Recreate identities and access
&lt;/h2&gt;

&lt;p&gt;With Predastore up and running, the next step is to give your applications a way in. How you go about this depends on the mode you picked back in step 1.&lt;/p&gt;

&lt;h3&gt;
  
  
  Standalone
&lt;/h3&gt;

&lt;p&gt;Standalone Predastore has no concept of users or policies. Access is controlled entirely by the &lt;code&gt;[[auth]]&lt;/code&gt; entries in &lt;code&gt;predastore.toml&lt;/code&gt;, and every one of them is a root credential. So the job here is fairly simple.&lt;/p&gt;

&lt;p&gt;Add an &lt;code&gt;[[auth]]&lt;/code&gt; entry for each access key your applications currently use against MinIO. You're free to reuse the same key and secret values, which means your applications won't need new credentials when you switch them over.&lt;/p&gt;

&lt;p&gt;On a multi-node cluster, make the same change to the config on every machine.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[[auth]]&lt;/span&gt;
&lt;span class="py"&gt;access_key_id&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"&amp;lt;existing-minio-access-key&amp;gt;"&lt;/span&gt;
&lt;span class="py"&gt;secret_access_key&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"&amp;lt;existing-minio-secret-key&amp;gt;"&lt;/span&gt;
&lt;span class="py"&gt;account_id&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"100000000001"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then restart the service (on each machine, for a multi-node cluster) to pick it up:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl restart predastore
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Note: If you relied on MinIO policies to restrict what each application could access, there's no standalone equivalent. That's what Spinifex is for.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Via Spinifex
&lt;/h3&gt;

&lt;p&gt;Spinifex implements AWS-compatible IAM, so MinIO's users, groups, and policies map across fairly directly. All of the commands below use the standard AWS CLI, pointed at Spinifex via the profile created during setup.&lt;/p&gt;

&lt;p&gt;For more detail on any of them, check out Spinifex's &lt;a href="https://docs.mulgadc.com/docs/iam-users-and-policies" rel="noopener noreferrer"&gt;IAM guide&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;AWS_PROFILE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;spinifex
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Export your MinIO policies
&lt;/h4&gt;

&lt;p&gt;MinIO policies are written in the same JSON format as AWS IAM policies, so in most cases they can be carried over as-is.&lt;/p&gt;

&lt;p&gt;List the policies on your MinIO deployment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mc admin policy list myminio
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then export each of your custom policies to a file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mc admin policy info myminio &amp;lt;policy-name&amp;gt; &lt;span class="nt"&gt;--policy-file&lt;/span&gt; &amp;lt;policy-name&amp;gt;.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Before importing them, there are a few things worth checking for, as Spinifex only supports a subset of the IAM policy language:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;MinIO-specific actions such as &lt;code&gt;admin:*&lt;/code&gt; and &lt;code&gt;kms:*&lt;/code&gt; mean nothing to Predastore, and should be removed. Spinifex doesn't check action names, so it will accept them without complaint. MinIO's &lt;code&gt;admin:*&lt;/code&gt; statements usually have no &lt;code&gt;Resource&lt;/code&gt;, though, which Spinifex does reject, with &lt;code&gt;Resource is required&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;NotAction&lt;/code&gt; and &lt;code&gt;NotResource&lt;/code&gt; aren't supported yet. Use &lt;code&gt;Action&lt;/code&gt; and &lt;code&gt;Resource&lt;/code&gt; with an explicit list instead. A statement with only a &lt;code&gt;NotAction&lt;/code&gt; or &lt;code&gt;NotResource&lt;/code&gt; is rejected with &lt;code&gt;Action is required&lt;/code&gt; or &lt;code&gt;Resource is required&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Condition keys are limited to &lt;code&gt;aws:SourceIp&lt;/code&gt;, &lt;code&gt;aws:SecureTransport&lt;/code&gt;, &lt;code&gt;aws:username&lt;/code&gt;, &lt;code&gt;aws:userid&lt;/code&gt;, &lt;code&gt;aws:PrincipalAccount&lt;/code&gt;, &lt;code&gt;aws:PrincipalType&lt;/code&gt;, and &lt;code&gt;s3:prefix&lt;/code&gt;. The only supported operators are &lt;code&gt;StringEquals&lt;/code&gt;, &lt;code&gt;StringLike&lt;/code&gt;, &lt;code&gt;IpAddress&lt;/code&gt;, and &lt;code&gt;Bool&lt;/code&gt;, so conditions using the likes of &lt;code&gt;StringNotEquals&lt;/code&gt; will need rewriting.&lt;/li&gt;
&lt;li&gt;Policy variables are limited to &lt;code&gt;${aws:username}&lt;/code&gt;, &lt;code&gt;${aws:userid}&lt;/code&gt;, and &lt;code&gt;${aws:PrincipalAccount}&lt;/code&gt;. MinIO's &lt;code&gt;${jwt:*}&lt;/code&gt; and &lt;code&gt;${ldap:*}&lt;/code&gt; variables won't carry over.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Create the policies
&lt;/h4&gt;

&lt;p&gt;Create each policy from its exported file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws iam create-policy &lt;span class="nt"&gt;--policy-name&lt;/span&gt; &amp;lt;policy-name&amp;gt; &lt;span class="nt"&gt;--policy-document&lt;/span&gt; file://&amp;lt;policy-name&amp;gt;.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The good news is that Spinifex loudly refuses a policy that uses an unsupported element, condition, or variable. So if one of those slips through, &lt;code&gt;create-policy&lt;/code&gt; will fail with a &lt;code&gt;MalformedPolicyDocument&lt;/code&gt; error telling you what needs changing. Unrecognised actions are the exception, as mentioned above.&lt;/p&gt;

&lt;h4&gt;
  
  
  Recreate your users and groups
&lt;/h4&gt;

&lt;p&gt;Each MinIO user and group gets an IAM equivalent, with the same policies attached. You'll need your account ID for the policy ARNs, which this will tell you:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws sts get-caller-identity
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create each user, and attach their policies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws iam create-user &lt;span class="nt"&gt;--user-name&lt;/span&gt; alice
aws iam attach-user-policy &lt;span class="nt"&gt;--user-name&lt;/span&gt; alice &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--policy-arn&lt;/span&gt; arn:aws:iam::&amp;lt;account-id&amp;gt;:policy/&amp;lt;policy-name&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create each group, attach its policies, and add its members:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws iam create-group &lt;span class="nt"&gt;--group-name&lt;/span&gt; developers
aws iam attach-group-policy &lt;span class="nt"&gt;--group-name&lt;/span&gt; developers &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--policy-arn&lt;/span&gt; arn:aws:iam::&amp;lt;account-id&amp;gt;:policy/&amp;lt;policy-name&amp;gt;
aws iam add-user-to-group &lt;span class="nt"&gt;--group-name&lt;/span&gt; developers &lt;span class="nt"&gt;--user-name&lt;/span&gt; alice
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Issue new access keys
&lt;/h4&gt;

&lt;p&gt;MinIO won't hand over existing secret keys, and Spinifex generates its own, so each user will need a fresh key pair. The secret is only shown once, so make sure to save it somewhere safe.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws iam create-access-key &lt;span class="nt"&gt;--user-name&lt;/span&gt; alice
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Note: Buckets belong to the account that creates them. When you come to copy data across in the next step, use a key belonging to the account you want to own the buckets.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Move the data
&lt;/h2&gt;

&lt;p&gt;Now for the main event. To copy your data from MinIO to Predastore, I recommend using &lt;a href="https://rclone.org" rel="noopener noreferrer"&gt;rclone&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;You might be tempted to reach for &lt;code&gt;mc mirror&lt;/code&gt;, and it does work, but with MinIO archived, &lt;code&gt;mc&lt;/code&gt; is no longer maintained and its official downloads have been taken down. rclone on the other hand is actively maintained, packaged for pretty much every distro, and lets you set the region explicitly on both ends.&lt;/p&gt;

&lt;h3&gt;
  
  
  Install rclone
&lt;/h3&gt;

&lt;p&gt;Install it on a machine that can reach both MinIO and Predastore:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="p"&gt;;&lt;/span&gt; curl https://rclone.org/install.sh | &lt;span class="nb"&gt;sudo &lt;/span&gt;bash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Configure both ends
&lt;/h3&gt;

&lt;p&gt;rclone needs a remote for each side. Add the following to &lt;code&gt;~/.config/rclone/rclone.conf&lt;/code&gt;, filling in your own endpoints and keys. &lt;code&gt;force_path_style&lt;/code&gt; is required, as Predastore only supports path-style requests, and &lt;code&gt;region&lt;/code&gt; needs to match the one you set up Predastore with.&lt;/p&gt;

&lt;p&gt;If you went with Spinifex, use a key belonging to the account you want to own the buckets. The &lt;code&gt;spinifex&lt;/code&gt; profile's own key, in &lt;code&gt;~/.aws/credentials&lt;/code&gt;, belongs to the account created during setup.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[minio]&lt;/span&gt;
&lt;span class="py"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;s3&lt;/span&gt;
&lt;span class="py"&gt;provider&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;Minio&lt;/span&gt;
&lt;span class="py"&gt;endpoint&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;https://minio.example.com:9000&lt;/span&gt;
&lt;span class="py"&gt;access_key_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;minio-access-key&amp;gt;&lt;/span&gt;
&lt;span class="py"&gt;secret_access_key&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;minio-secret-key&amp;gt;&lt;/span&gt;
&lt;span class="py"&gt;region&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;us-east-1&lt;/span&gt;

&lt;span class="nn"&gt;[predastore]&lt;/span&gt;
&lt;span class="py"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;s3&lt;/span&gt;
&lt;span class="py"&gt;provider&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;Other&lt;/span&gt;
&lt;span class="py"&gt;endpoint&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;https://s3.example.com:8443&lt;/span&gt;
&lt;span class="py"&gt;access_key_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;predastore-access-key&amp;gt;&lt;/span&gt;
&lt;span class="py"&gt;secret_access_key&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;predastore-secret-key&amp;gt;&lt;/span&gt;
&lt;span class="py"&gt;region&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;us-east-1&lt;/span&gt;
&lt;span class="py"&gt;force_path_style&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Confirm both remotes work by listing their buckets:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rclone lsd minio:
rclone lsd predastore: &lt;span class="nt"&gt;--ca-cert&lt;/span&gt; ~/predastore-ca.pem
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Copy each bucket
&lt;/h3&gt;

&lt;p&gt;Copy a bucket by creating it on the Predastore side, then filling it with &lt;code&gt;rclone sync&lt;/code&gt;. rclone only creates a bucket when it has something to put in it, so without the &lt;code&gt;mkdir&lt;/code&gt; an empty bucket would quietly get left behind.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;--checksum&lt;/code&gt; compares objects by their hash rather than their modification time, and &lt;code&gt;--metadata&lt;/code&gt; carries across user metadata (&lt;code&gt;x-amz-meta-*&lt;/code&gt;) and &lt;code&gt;Content-Type&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;rclone's &lt;code&gt;--ca-cert&lt;/code&gt; flag applies to both remotes at once, not just Predastore. If your MinIO is served over HTTPS, passing Predastore's certificate on its own will stop rclone from trusting MinIO, so pass the system's CA bundle alongside it (on RHEL and Fedora, that's &lt;code&gt;/etc/pki/tls/certs/ca-bundle.crt&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rclone &lt;span class="nb"&gt;mkdir &lt;/span&gt;predastore:&amp;lt;bucket&amp;gt; &lt;span class="nt"&gt;--ca-cert&lt;/span&gt; ~/predastore-ca.pem
rclone &lt;span class="nb"&gt;sync &lt;/span&gt;minio:&amp;lt;bucket&amp;gt; predastore:&amp;lt;bucket&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--checksum&lt;/span&gt; &lt;span class="nt"&gt;--metadata&lt;/span&gt; &lt;span class="nt"&gt;--progress&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--ca-cert&lt;/span&gt; /etc/ssl/certs/ca-certificates.crt &lt;span class="nt"&gt;--ca-cert&lt;/span&gt; ~/predastore-ca.pem
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Note: &lt;code&gt;rclone sync&lt;/code&gt; makes the destination match the source, which means deleting anything in the destination that isn't in the source. Always name the bucket on both sides, and never run it against the root of the remotes, as that will clear out anything in Predastore that MinIO doesn't know about. If in doubt, add &lt;code&gt;--dry-run&lt;/code&gt; first to see what it would do.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;To copy every bucket in one go:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;for &lt;/span&gt;bucket &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;rclone lsf &lt;span class="nt"&gt;--dirs-only&lt;/span&gt; minio: | &lt;span class="nb"&gt;tr&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; /&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;rclone &lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="s2"&gt;"predastore:&lt;/span&gt;&lt;span class="nv"&gt;$bucket&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--ca-cert&lt;/span&gt; ~/predastore-ca.pem
  rclone &lt;span class="nb"&gt;sync&lt;/span&gt; &lt;span class="s2"&gt;"minio:&lt;/span&gt;&lt;span class="nv"&gt;$bucket&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"predastore:&lt;/span&gt;&lt;span class="nv"&gt;$bucket&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--checksum&lt;/span&gt; &lt;span class="nt"&gt;--metadata&lt;/span&gt; &lt;span class="nt"&gt;--progress&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--ca-cert&lt;/span&gt; /etc/ssl/certs/ca-certificates.crt &lt;span class="nt"&gt;--ca-cert&lt;/span&gt; ~/predastore-ca.pem
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Check the copy
&lt;/h3&gt;

&lt;p&gt;Check each bucket with &lt;code&gt;rclone check&lt;/code&gt;. &lt;code&gt;--download&lt;/code&gt; reads every object from both sides and compares the actual contents. It's slow for large buckets, but it's the most reliable way to be sure.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rclone check minio:&amp;lt;bucket&amp;gt; predastore:&amp;lt;bucket&amp;gt; &lt;span class="nt"&gt;--download&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--ca-cert&lt;/span&gt; /etc/ssl/certs/ca-certificates.crt &lt;span class="nt"&gt;--ca-cert&lt;/span&gt; ~/predastore-ca.pem
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A successful check reports &lt;code&gt;0 differences found&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Switch clients over and check the result
&lt;/h2&gt;

&lt;p&gt;With the data across, all that's left is to point your applications at Predastore. It's worth doing this in a sensible order, so that nothing written to MinIO in the meantime gets left behind.&lt;/p&gt;

&lt;h3&gt;
  
  
  Stop writes to MinIO, and run a final sync
&lt;/h3&gt;

&lt;p&gt;Stop your applications from writing to MinIO (or put them into maintenance mode), then run the copy from step 4 one last time to pick up anything that changed since.&lt;/p&gt;

&lt;h3&gt;
  
  
  Compare the two sides
&lt;/h3&gt;

&lt;p&gt;Before switching anything over, check that both sides agree on the number of objects and the total size of each bucket:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rclone size minio:&amp;lt;bucket&amp;gt;
rclone size predastore:&amp;lt;bucket&amp;gt; &lt;span class="nt"&gt;--ca-cert&lt;/span&gt; ~/predastore-ca.pem
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Update your clients
&lt;/h3&gt;

&lt;p&gt;For most clients, switching over comes down to a handful of settings:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Endpoint&lt;/strong&gt; - &lt;code&gt;https://&amp;lt;predastore-host&amp;gt;:8443&lt;/code&gt;. Predastore only speaks HTTPS, so any &lt;code&gt;http://&lt;/code&gt; endpoints will need updating along with the host and port.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Region&lt;/strong&gt; - Whatever you set during setup in step 2. A mismatch shows up as an &lt;code&gt;AuthorizationHeaderMalformed&lt;/code&gt; error on every bucket and object request.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Credentials&lt;/strong&gt; - Unchanged if you reused your MinIO keys on a standalone deployment. Otherwise, the new keys from step 3.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Addressing style&lt;/strong&gt; - Predastore only supports path-style requests (&lt;code&gt;https://host:8443/bucket/key&lt;/code&gt;), not virtual-hosted ones (&lt;code&gt;https://bucket.host:8443/key&lt;/code&gt;). Most SDKs have an option for this, such as &lt;code&gt;addressing_style&lt;/code&gt; in boto3, &lt;code&gt;forcePathStyle&lt;/code&gt; in the JavaScript SDK, and &lt;code&gt;UsePathStyle&lt;/code&gt; in the Go SDK.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Certificate trust&lt;/strong&gt; - If you're using a self-signed certificate, clients need to trust it. That's the &lt;code&gt;~/predastore-ca.pem&lt;/code&gt; copy made when you checked the cluster was up in step 2.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For example, an AWS CLI profile for Predastore in &lt;code&gt;~/.aws/config&lt;/code&gt; looks something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[profile predastore]&lt;/span&gt;
&lt;span class="py"&gt;region&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;us-east-1&lt;/span&gt;
&lt;span class="py"&gt;endpoint_url&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;https://s3.example.com:8443&lt;/span&gt;
&lt;span class="py"&gt;ca_bundle&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;/home/&amp;lt;you&amp;gt;/predastore-ca.pem&lt;/span&gt;
&lt;span class="py"&gt;aws_access_key_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;access-key&amp;gt;&lt;/span&gt;
&lt;span class="py"&gt;aws_secret_access_key&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;secret-key&amp;gt;&lt;/span&gt;
&lt;span class="py"&gt;s3&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
  &lt;span class="py"&gt;addressing_style&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;path&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Note: Any presigned URLs generated before the switch point at MinIO, and won't carry over. Your applications will need to generate new ones against Predastore.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Keep MinIO around for a little while
&lt;/h3&gt;

&lt;p&gt;Once your applications are happily talking to Predastore, I'd recommend leaving MinIO running in a read-only state for a while before decommissioning it. That way, if anything turns out to be missing, the original is still there to fall back on.&lt;/p&gt;

&lt;h3&gt;
  
  
  Retire MinIO
&lt;/h3&gt;

&lt;p&gt;Once you're confident nothing is missing, stop MinIO for good, and delete its data to hand the space over to Predastore. Predastore carries on running throughout, and there's nothing to restart. Under Spinifex, only the drive holding Predastore's data gains anything for now, and the rest are free for other uses.&lt;/p&gt;

&lt;p&gt;If MinIO was pointed at a directory on each drive, such as &lt;code&gt;/mnt/disk1/minio&lt;/code&gt;, delete that directory on every drive:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo rm&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; /mnt/disk1/minio
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If MinIO was pointed at the drive itself, delete its &lt;code&gt;.minio.sys&lt;/code&gt; directory, along with a directory for each of your buckets, naming them explicitly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; /mnt/disk1 &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sudo rm&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; .minio.sys &amp;lt;bucket&amp;gt; &amp;lt;bucket&amp;gt; ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Note: Resist the temptation to reach for &lt;code&gt;rm -r /mnt/disk1/*&lt;/code&gt;. It misses &lt;code&gt;.minio.sys&lt;/code&gt;, and if Predastore's directory is ever missing its leading &lt;code&gt;.&lt;/code&gt;, it'll take your freshly migrated data with it.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing thoughts
&lt;/h2&gt;

&lt;p&gt;Hurray! Hopefully that went smoothly for you. If it didn't, raise a GitHub issue so we can help you out.&lt;/p&gt;

&lt;p&gt;We've got plenty of features and improvements planned for Predastore, so be sure to keep your eyes on the release page.&lt;/p&gt;

&lt;p&gt;I've got other articles in the works as well, focusing more on the design rationale behind the system. Stay tuned for those. That's all for this one!&lt;/p&gt;

</description>
      <category>minio</category>
      <category>s3</category>
      <category>selfhosted</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
