retnd mark: a broken ring standing for a transfer cycle in progress

retnd

Tutorial

First run, screen by screen

Every screen a freshly installed instance shows, in the order it shows them, with an example for each field and a sentence on what that entry causes the product to do. Not what the label says. What the entry does.

The worked example throughout is a UGREEN NAS running the canonical Docker Compose deployment, pulling finished backup archives off two VPS over SFTP. The first run configures the first of the two. The second one is an ordinary backup set added afterwards.

Setup is not a separate flow. Once you have an administrator account you land in the ordinary application with a banner saying it has no configuration yet, and you press Add backup set. The wizard behind that button is the same wizard you will use for every set after this one, which is the point: a parallel setup form would be a second place for the same questions to drift.

Where the terminal went

Every signed-in page has a log panel docked to the bottom of the browser window. It is not in the wizard pictures below, because these were taken in a window made tall enough that no screen needed stitching together and the panel is pinned to the bottom of that window, out of frame. It has a page of its own, with moving pictures, because a docked log is something you watch rather than something you look at.

Two defects this page used to warn about are fixed

#274 made an expired enrolment token report “The administrator account could not be created”, which blamed your password for a problem with the link. #275 drew a Cancel control during first run that could not do anything. Both are closed, and the flow below is the one that replaced them.

Before you open a browser

Four things have to be true, and none of them is checked for you in a way that produces a useful message if it is not.

  1. The host directories exist and the container can write them. The runtime image is distroless with no root step, and a bind mount does not chown its source, so create the state, backup and config directories on the NAS and chown them to the container's uid and gid yourself. The config directory is mounted as a writable directory, not a read-only file, which is exactly what lets setup write config.yaml into it.
  2. The config directory is empty. Setup is a one-time door. If anything is already at /etc/retnd/config/config.yaml, however it got there, the first-run write refuses rather than overwriting it, and the instance serves the ordinary application instead of the setup flow.
  3. The remote server has an account to pull from. A dedicated, shell-less, chrooted SFTP account is what the project's own SSH setup guide recommends, and it has a consequence worth knowing before you get to step 6: against that account shape a remote checksum cannot be computed at all, so verification is transfer verification, not a remote-versus-local hash comparison.
  4. You have working SMTP details in front of you. Enrolment asks for a recovery email address and for the mail server to reach it through, and it proves both by sending a confirmation message before it will create the account. Server, port, transport security, the sender address the server will accept, and a username and password if it wants them. A provider's app-password page is the usual place to get them; whatever you use, it has to work from the NAS, because that is where the connection is made from.

The screens

Screen 0

The enrolment link the install prints

There is no screen for this one. On first start, before any browser is involved, the engine prints a one-time enrolment secret to its own stdout. If you installed with install_docker_host.py, you already have it: the installer waits for that line and prints it as the last thing a fresh install says, so scroll up in the install output rather than going looking for it.

If it is not there — the notice landed after the installer stopped waiting, or you stood the stack up by hand — it is in the engine container's log rather than the web one's, and a fresh link is one command away: python3 install_docker_host.py enroll-link restarts the engine, waits for the new notice and prints it. Either way, the line looks like this:

retnd-web: no administrator account exists yet. Open
http://10.0.0.10:8080/enroll?token=<a long random string> to create one
(valid 30 minutes, single use).
ThingValueWhat it means for you
The host in the link this machine's own address install_docker_host.py reads the address this machine's default route leaves by and writes it into PUBLIC_BASE_URL, so the link works from the laptop you are reading it on. Open it as printed, token and all. If you wrote the Compose file yourself instead, the shipped default is http://localhost:${LISTEN_PORT}, which only resolves on the NAS: swap the host for the NAS address, keep the token exactly as printed, and set PUBLIC_BASE_URL in .env to get a correct link next time.
The token 24 random bytes, base64url Reaching the port is deliberately not sufficient to claim the administrator account. The token is what proves you can read the container's own log, which is the thing an attacker on the LAN cannot do.
Valid 30 minutes hard expiry After 30 minutes the token stops working and enrolment fails, currently with a message that does not say so. See the callout on the next screen.
Single use consumed atomically It cannot succeed twice. A restart before enrolment completes mints a fresh token and silently invalidates the old one, so an old link in your scrollback is dead even inside its 30 minutes.
If nothing was printed

The notice is printed only while enrolment is still open. Nothing in the log means an administrator already exists, and the answer is to sign in rather than to hunt for a token. Restarting the engine mints a new token only when no administrator has enrolled.

Screen 1

Create the administrator

Open the link. This account manages retnd and nothing else. It is not your NAS operating-system account, and it is stored on the NAS as an Argon2id hash in /data/state/local-auth.json, in the same volume as the lifecycle journal, so it survives a container restart without a second volume. The recovery address and the mail settings below land in that same record, and the mail password lands there as a reference to a secret rather than as the secret.

The form asks for more than a password, and the reason is the sentence above: this is the only account, and enrolment is the only time it is created. Beside the username and password it takes a recovery email address and the mail server to reach it through, and before it writes the record it sends a confirmation message to that address over exactly those details. If the send fails, the enrolment fails and no account is created. An address nobody has ever delivered to is worth nothing on the day it is needed, and the day it is needed is the day nobody can sign in to fix it.

If the link has expired before you get here

The token lasts 30 minutes and works once, and nothing re-issues it while the engine keeps running. One command mints a fresh one, from wherever you ran the installer:

python3 install_docker_host.py enroll-link

Output

==> Restarting retnd, which is what mints a token
==> Waiting up to 90s for the new notice

retnd-web: no administrator account exists yet. Open http://10.0.0.10:8080/enroll?token=jdurSlionp6jyoHTzP_5fHYfLy3Kk6tq to create one (valid 30 minutes, single use).

    Valid 30 minutes, and it works once. Every link printed before this one is now dead,
    including any still in your scrollback.

It restarts the engine, because that is what mints a token, and reads back the newest notice rather than the first: the container keeps its log across the restart, so the earlier link is still in it and is the one the restart killed. That is the same reason an old link in your scrollback is dead even inside its 30 minutes.

The Create retnd administrator form, empty, with the username and password fields, the Account recovery block of mail settings, the ATTENTION banner and a disabled Create administrator button
The enrolment form as the link lands on it. The Create administrator button stays disabled until every field is valid, so there is no way to submit a half-filled form.
The banner on the form is not decoration

Above the mail fields the page carries a warning headed ATTENTION, with links to SMTP2go and Gmail SMTP setup. It is there because this is the one part of setup that needs a credential from somewhere else, and the two links go to the two ways most deployments get one: a transactional-mail provider's free tier, or an app password on an existing mailbox. Neither is required and neither is endorsed — any server that will accept a submission works — but an operator who has never configured SMTP should not have to guess where to start while holding a 30-minute token.

FieldExampleWhat the entry does
Username nas-admin Becomes the single administrator record for this instance. Enrolment is one-shot and irreversible: once this record exists, the enrolment route is closed for good and there is no second account. What there is instead is a way back into this one, which is what the recovery address below buys. Pick something you will recognise in a log line.
Password a passphrase of 12 characters or more Hashed with Argon2id and written to the state volume. The plaintext is never stored and never leaves the NAS. Twelve characters is a floor the form enforces, not a recommendation.
Confirm password the same again Client-side only. You can change this password later from Settings, which asks for the current one, and a password nobody knows is now recoverable through the recovery address below rather than through a reinstall. This field is still the cheapest place to catch a typo, because the alternative costs you a working mail path.
Recovery email nas-admin@example.com Stored on the administrator record beside the username, and the only address a password reset will ever be sent to. It is not a second login: the username stays what you sign in with. Anything that is not an address is refused as INVALID_EMAIL before a connection is attempted, and the account is not created until a confirmation message has actually been accepted for this address.
SMTP host and Port smtp.example.com, 587 Where that confirmation, and every later reset, is submitted. The connection is made from the engine container, not from your browser, so a server only the laptop you are sitting at can reach fails here, and so does one behind a firewall the NAS is on the wrong side of.
Security STARTTLS One of STARTTLS, TLS or none, and it defaults to STARTTLS. Port 587 is usually the first and 465 the second. None sends the credential below in the clear, so it is for a relay on the same host and for nothing else.
SMTP username and SMTP password an app password, not the password to your mailbox Authenticates the submission. The password is the one field here that is neither hashed nor stored: the record keeps a reference to it in the same form the product uses for every other secret, and no API response and no log line ever carries it back. Leave both empty for a relay that asks for no credential.
From address retnd@example.com The sender the messages are submitted as. It has to be one this server accepts from this account, which is the most common reason a first attempt comes back SMTP_SEND_FAILED: providers reject a sender they do not own long before they reject a password.
The enrolment form showing the message Minimum 12 characters below a short password
Below-minimum passwords are refused inline before submission, and the submit button stays disabled.
The enrolment form with every field filled, the SMTP username and password left empty, and the Create administrator button enabled
Every field valid. Pressing Create administrator sends the username, the password, the recovery address, the mail settings and the token from the URL; the server sends the confirmation message, and only then writes the record and signs you in.
Two of these fields are photographed empty on purpose

SMTP username and SMTP password are blank in the pictures above, and that is a real configuration rather than an unfinished screenshot: the fixture these are shot against submits without a credential, and a placeholder in a password field is the one kind of placeholder somebody copies into production. Fill both in if your own server asks for them. Security is likewise left on its default of STARTTLS.

What arrives in the mailbox

A message titled “retnd: recovery email confirmed”, sent the moment before the account is written. It is the receipt for the whole arrangement: it proves the address is real, that the server accepts mail from this deployment, and that a reset link would arrive if you ever needed one. Read it as the only part of setup that is verified rather than merely entered, and if it does not appear, treat the enrolment as unfinished even if the browser says otherwise.

If the mail cannot be sent

The API answers SMTP_SEND_FAILED, the enrolment is refused, and no administrator is created — which is the point: an account whose recovery path has never worked is an account that cannot be recovered. The message carries the server's own refusal, sanitised, and it is usually one of three things: a From address the provider will not let this account send as, a password that is your mailbox password where the provider wanted an app password, or a port and security setting that disagree (587 with implicit TLS, 465 with STARTTLS).

The link is not spent by a failure. Only a successful enrolment consumes the token, so fix the field and press the button again on the same page. That covers a rejected password, an address refused as INVALID_EMAIL, and every SMTP failure. It is also a change: before #830 a too-short password burned the token and left you restarting the engine for a fresh link.

If it says the account could not be created

Read that message as the token, not the password. When the enrolment secret is missing, expired or already used, the API answers BOOTSTRAP_TOKEN_INVALID with “missing, expired or already-used bootstrap token”, and the page currently discards that and shows “The administrator account could not be created” instead. Trying a different password cannot help.

The fix is a fresh token, and this is the one failure on this screen that needs one: restart the engine container, read the new link out of the log, and open that one. If the token was already used, an administrator exists and you should sign in instead.

The correlation id shown on that error, cid_enroll, is a hardcoded literal and does not match the one in the response, so it will not find anything in a log. This is #274 and it is being fixed.

Something this page used to warn about

Error banners here once rendered their leading glyph as the literal six characters \u2715 rather than as a cross, which was #257. Every glyph in the interface is now a compiled Font Awesome path rather than a Unicode character, so that class of defect is gone along with the one where the navigation looked different on every operating system.

Screen 2

The application with nothing in it

Enrolment signs you in and drops you on the dashboard, not on a setup page. Every section in the navigation is there and every one of them has nothing behind it, and a banner says why.

The dashboard of an unconfigured instance, with a first-run banner saying retnd has no configuration yet and an empty state offering Add backup set
The dashboard one second after enrolment. The banner deliberately carries no button of its own: the two pages that can act on it already offer Add backup set, and a banner repeating it would put the same primary action on one page twice.

That banner is one of the two on this product that cannot be dismissed, and the reason is worth knowing rather than reading as an oversight. It is mounted once for the session, above the routed content, so no navigation unmounts it and a dismissal would last until a hard reload. Its body says “until that is done nothing is backed up”. An operator who clears that and forgets believes they have backups they do not have.

Press Add backup set. Everything from here is the ordinary wizard.

Screen 3 · wizard step 1 of 8

Step 1: Source

Eight steps, and the tracker across the top ticks each one off as it becomes valid. You can go back to any step you have reached; you cannot jump forward past one you have not answered, because the tracker refuses it and so does Continue. The footer says which step you are on out of how many, so the count on screen is the count this page walks.

Two names for the same step

The tracker names each step in a word or two, and the step itself opens with a fuller heading. Where the two differ this page uses the tracker's label to tell you where to click and the step's own heading to describe what you are looking at: step 4 is Repository on the tracker and “Repository domain” at the top of the card, step 5 is Consistency and “Source consistency”, step 7 is Retention and “Storage, retention and holds”. One of them changes with the engine rather than being merely shorter: step 6 always reads Verification on the tracker, while the card reads “Completion and validation” for the artifact engine this tutorial uses and “Verification” for the incremental one. That is the product asking two engines the same question, not a mistake on this page.

The Add backup set wizard on step 1 Source, with an eight-step tracker and six fields carrying the fixture's example values
Step 1 with the product's own example values in place. Everything visible here is a text field you are expected to overwrite.
Step 1 Source filled in with a backup set name, a hostname, port 22, a username, a directory and a filename pattern
Step 1 filled in for the worked example, and the tracker already showing ticks on steps 3 to 6: those steps have defaults, and a tick means “this step's own answers are in” rather than “you have been here”. Step 2 has no default and no tick, which is why nothing past it is reachable yet.
FieldExampleWhat the entry does
Backup set name api-server-nightly Becomes the backup set's id in config.yaml, and half of its permanent identity: a backup set is identified by source plus set, never by the set name alone. It also becomes part of a filename on disk, because the trust anchor from step 2 is written to known_hosts.d/<source>_<name>_known_hosts, so keep it to something filename-shaped.
Server hostname api-server.example.net The VPS this NAS pulls from. Editing it after you have trusted a host key on step 2 revokes that trust, deliberately, so that trusting server A cannot leave server B showing as trusted. It also un-proves the connection test, which locks every step after step 2 again until you run it against the machine now named here.
SSH port 22 Written to remote.port. The step will not let you leave it until this is a whole number between 1 and 65535. The port shown in the pictures is the SSH default, which is also what the field's own help offers as its example; put your own number there. The save path still falls back to 22 for anything that is not a port, and the gate is what stops you ever handing it one — see below.
Username backup-agent The SFTP account on the remote server. This account only ever needs read access to the directory below plus the right to delete the artifacts it produced, because the manager pulls and then removes the source. It is never given write access to your data.
Directory to back up /var/backups/ Becomes remote_path, and it has to be absolute. This is the directory on the VPS that gets emptied. Every file directly under it that matches a pattern and passes the completion check is pulled and then deleted from the VPS, so point it at a directory whose only job is holding finished backups, never at a working directory. It is asked for here rather than three steps later because step 2's connection test lists this very directory: asking afterwards would mean testing before anything had said what to test.
Filename patterns to back up *.tar.zst Becomes include, a list. Comma-separated here, split and trimmed on save, and each pattern is matched against a remote file's own name only and never its path, so a pattern cannot contain a slash. A file that matches nothing is invisible to the manager: it is never pulled and never deleted, which is the safe direction but also means a producer that changes its output extension quietly stops being backed up.
The port is the one field to double-check

A non-numeric port used to become 22 silently, and because 22 will usually be reachable on a VPS, a typo produced a wizard that appeared to work while pointing somewhere you did not intend. That coercion is still in the save path and is now unreachable through the wizard: the step is unfinished until the field holds a real port, so Continue and the tracker both refuse to move. What the gate cannot catch is a port that is a number and the wrong one, so check remote.port in the written config.yaml once setup finishes.

There is no exclusion control, on this step or anywhere else

The patterns above say what to back up. Nothing on any screen says what to leave out. The configuration file does have a field for it — exclude_paths, a list of directory paths under the remote folder — and it is deliberately not the same kind of thing as the patterns above: a pattern is a filename and may not contain a slash, a path is a path and prunes the walk itself rather than filtering what the walk found. Set it by editing config.yaml. The backup-set detail page draws an Exclude row and it reads as empty on every real deployment whatever the file says, because the API response carries no field for it and the client fills that row with an empty list.

Screen 4 · wizard step 2 of 8

Step 2: Connection test

One step, and three questions that used to be two steps and a panel on the last one: which key this set authenticates with, which machine it is allowed to talk to, and what those two can actually do right now. It is second in the flow on purpose. Its answer constrains the steps after it — the write probe in its own report is what decides whether step 7 may offer to delete from the remote server at all — and nothing past it is reachable until the test has come back clean.

Step 2 Connection test as it opens: three key-source choices with Generate selected, an information banner, the fetched host-key fingerprint marked not yet trusted, and a connection panel saying nothing has been proven
The step as it opens. The host key has already been fetched — opening the step asks the server for it — and nothing has been trusted, tested or saved. The footer reads Step 2 of 8 and Continue is refused.

Credentials

Three options are offered, and the default is the one that cannot finish.

ChoiceState todayWhat it does
Generate dedicated SSH key not implemented Marked “Recommended” and selected by default, and saving with it selected is refused. Picking it draws an information banner on this step saying so and pointing at the two options beside it, which is a change worth knowing: the banner used to send you to an “Authentication” step that no longer exists, for controls that are directly below it. It no longer shows a public key to install, because the key it used to show was a fixed example string for a path that cannot be saved.
Use managed key works, once you pick one Lists the keys this deployment already holds, each with its fingerprint and the backup sets currently using it — a real read of the key store, not a sample list, so on a default install the key the installer generated is already sitting here. A key that needs a passphrase to be resolvable, or whose public half could not be read, is listed and disabled with the reason on it. Saving is refused until one of them is actually selected; with one selected it saves like an imported key, because from the save's point of view it is one.
Import key works Paste the private key of an account you have already installed the matching public key for. This is the path the rest of this page walks.
Step 2 with Import key selected, showing an empty private key textarea and a disabled Import key button
Import key selected. The field is shown empty on purpose: a screenshot of a private key, even a fake one, is not a picture worth publishing.

Paste an unencrypted OpenSSH or PEM private key and press Import key. What happens then is worth knowing precisely, because it is the only moment key material exists anywhere near a browser:

Step 2 showing a green Key imported banner with an algorithm and fingerprint, and a Replace button
After import: the algorithm and fingerprint of what was stored, and a note that the pasted material has already been discarded. Check this fingerprint against ssh-keygen -lf on your own copy of the key before continuing.

Host key

The same step then fetches the remote server's host key and asks you to trust it. This is the half people click through, and it is the one that decides whether a future man-in-the-middle is caught or ignored. The fingerprint in the pictures is the development fixture's; yours will be your server's.

The instruction on the screen is the whole point of it: confirm this fingerprint through a channel other than this connection. Run ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub on the VPS itself, over a session you already trust, and compare. A fingerprint that only ever came down the connection you are trying to authenticate proves nothing.

Step 2 after pressing Trust host, showing a trusted timestamp, the button now reading Host trusted, and the connection panel ready to test
After Trust host. The button is now inert, the record shows when trust was granted, and Test connection at the bottom of the step has become available: it needs a key and a trusted host before it can check anything.
ControlExampleWhat it does
Trust host press once, after comparing Captures the probed known_hosts line. On save it is written to <config dir>/known_hosts.d/<source>_<set>_known_hosts and named by remote.known_hosts, and every later connection is checked against that file. This is the trust anchor, not display text.
Re-fetch fingerprint after a legitimate server rebuild Re-runs the probe against the same host. It does not grant trust; you still have to press Trust host, and if the key has changed the button becomes a destructive-confirm variant reading “Trust new fingerprint”. Changing the hostname or port on step 1 re-probes on its own and drops any trust already granted.
What a changed host key costs later

If this fingerprint ever changes, the backup set halts. Backup operations for it stop, and remote artifact deletion is blocked, until an administrator verifies the new fingerprint independently. That is the intended behaviour and it is why this half of the step exists: the alternative is a manager that keeps deleting remote originals while talking to a server that is no longer yours.

Host-key verification cannot be switched off. A configuration that names a known_hosts value which would disable it is refused outright at load time, so there is no accepted spelling of “skip this”.

The connection has to be proven before anything is written

This is #624, and it closed a gap that had been open since the wizard existed. Saving used to be gated on a pinned known_hosts line and an imported key, and both of those settle host identity: which machine answered. Neither says whether the key authenticates or whether the account can read the folder. So a backup set could be created, enabled and put on a schedule against a connection nobody had ever made.

The check used to sit on the review step, because that was the first point at which everything it needs had been answered. It is here now, and the reordering is the point: its answer is available to every step that depends on it, rather than arriving after the engine, the domain and the retention chain had already been chosen.

The connection panel after Test connection, listing seven named checks all passed, the sentence This source has been proven, and a green write-permission verdict
Seven named checks, in a fixed order, and never a single verdict. The key resolves and its permissions are unchanged, the hostname resolves, the socket opens, the host key matches what this set trusts, the server accepts the key, the folder lists with a count of what is in it, and a probe file is created under it and removed again. Why it is a result per check and not one verdict. The detail text beside each check is the fixture's own, so it names the fixture's host rather than the values on this form.

The last of those seven is #852's write probe, and it is stated here rather than only where it takes effect. A source that proves writable arms the source-deletion behaviour on step 7; one that does not disables it there and says why. Read-only is a perfectly good posture for a backup account, and the recommended one unless you want retnd to free space on the server for you: backups still run, because reading is all a backup needs. An operator who reads this line is never surprised by a disabled control five steps later.

Editing the host, the port or anything else the test was run against takes the result back to unproven and re-locks every step after this one. A green tick standing for a connection nobody made to the machine now named on step 1 is the defect the whole gate exists for.

Screen 5 · wizard step 3 of 8

Step 3: Engine

The one answer that cannot be changed later, asked once there is enough context to answer it. It has a default, so the step is “answered” from the moment it exists; what the step is for is making sure the default was read.

Step 3 Engine with two choices, Artifact selected, and a banner headed Why it is permanent
Step 3, with the default in place. The wire value is printed under each choice, because it is what ends up in a configuration file and in an API call.
ChoiceWritten asWhat it does
Artifact engine=artifact The default, and what this tutorial configures. Pulls finished files a producer leaves for you and keeps each one whole, verified on arrival. One file in, one backup kept. Steps 4 and 5 do not apply to it and step 6 asks the artifact form of its question.
Incremental engine=kopia Snapshots a whole directory tree every run and stores only content the repository does not already hold. Every run keeps a full restore point and only what changed is stored. This is the engine that makes steps 4, 5 and 6 into real questions, and it is out of scope for this page.
A set's history belongs to its engine

Snapshots and whole-file backups are different objects in different places. Switching a set that has run would leave everything it has collected behind and start again from nothing, so retnd asks you to create a new set instead — and the edit form for a saved set has no field for this at all. This is the step to slow down on.

Screen 6 · wizard step 4 of 8

Step 4: Repository domain

The encrypted store an incremental set's snapshots live in. For the artifact engine this tutorial uses there is nothing here to choose, and the step says so rather than disappearing.

Step 4 Repository domain showing a panel reading Not asked for an Artifact set
Step 4 for an artifact set: “A repository domain is where snapshots live. An artifact set keeps whole files instead, so there is nothing here to choose.”

A rail that changed length under you as you filled the form in would teach nothing, and a step that is simply skipped teaches nothing either. A step that explains itself teaches the difference between the two engines at the moment it matters, which is why both this step and the next one are still there, still numbered, and complete rather than blocking: the tracker ticks them because an artifact set has no answer to give, not because one was given.

Screen 7 · wizard step 5 of 8

Step 5: Source consistency

What you have arranged on the server for the duration of a run — a live tree, a quiesced one, or a snapshot taken outside retnd. It is recorded rather than detected, and a run that contradicts it is reported. Like step 4 it is an incremental-engine question, and like step 4 it says so.

Step 5 Source consistency showing a panel reading Not asked for an Artifact set
Step 5 for an artifact set: “Consistency describes a tree being walked. An artifact set keeps whole files instead, so there is nothing here to choose.” A finished file a producer has closed is not a tree being walked, so the question does not arise.
Screen 8 · wizard step 6 of 8

Step 6: Completion and validation

This is the step whose heading changes with the engine: Verification on the tracker, “Completion and validation” on the card for an artifact set and “Verification” for an incremental one. They are the same question — how hard is a backup checked before it counts — and a whole-file backup answers it with how completion is signalled and what validates the file on arrival.

Completion method, and why it is the consequential choice on this step

A backup artifact that is still being written looks exactly like one that is finished. Getting this wrong means pulling a half-written archive, verifying the half, and then deleting the remote original. The three answers are not equivalent.

Step 6 Completion and validation with Atomic rename selected under the Recommended heading, transfer verification marked always on, and an application validation dropdown
Step 6 for the worked example, with Atomic rename chosen; the default is the completion marker. The two options under “Recommended” both require the producer to signal completion; the one under “Advanced” infers it.
ChoiceWritten asWhat it does
Atomic rename completion.strategy: rename Your producer writes to a temporary name and renames into place when it is done. The rename is the completion signal, and on a POSIX filesystem it is atomic, so a partially written file is never visible under a name the manager will match. Pick this if you control the producer.
Completion marker / manifest completion.strategy: marker Your producer writes a sidecar file once the artifact is complete. The manager waits for the marker, so an artifact with no marker is never touched, however old it is. Equally safe, and the right answer when your producer cannot rename.
Stable file size / timestamp completion.strategy: stable The manager infers completion from a size and mtime that have stopped moving. This is a heuristic, and it is the only strategy that can be wrong. A producer that stalls mid-write for long enough looks finished.
If you choose stable size

Two different timers apply, and conflating them is the mistake to avoid. stable_for answers “has this looked done long enough to start processing it”, and the wizard sends one hour for it. A separate delete_safety_delay, defaulting to one hour and applied only to this strategy, answers a different and more dangerous question: “has it looked done long enough to destroy the only other copy”. The remote delete waits for that second timer on top of everything else.

The other two strategies carry a producer-provided completion signal, so they do not need it, and the wizard sends a zero for both timers when you choose one of them.

Validation

Two controls, one of which is not a control.

ControlExampleWhat the entry does
Transfer verification none to give Shown, correctly labelled “always on”, and not a control. It cannot be switched off, and the checkbox is disabled rather than absent so the step says what verification you are getting.
Application validation None, or an id from the list Really sent, as validation.validator_id. The list is the backend's own registered catalogue, and the option labels are the ids themselves because an id is what ends up in config.yaml. A validator runs against every artifact after transfer and checksum, and rejecting one quarantines the artifact and leaves the remote copy in place, which is the whole reason to use one.
Why checksum verification is written off

Comparing a local hash against a remote one requires the remote to compute a hash, and against the chrooted, shell-less, forced-internal-sftp account the project's own setup guide recommends, that is not possible at all. Turning hashing on against that account shape does not make verification stronger, it fails every artifact at the verification stage, every time. So a wizard-created backup set is written with validation.hash: "", meaning transfer verification alone, which is the honest posture when hash capability is absent.

If your SFTP account is not hardened that way and does support hashing, set validation.hash: sha256 by hand in config.yaml.

One field this step has no control for at all: stale_after, which decides when a backup set with no fresh artifact starts reporting as stale, is set to 48 hours for every set the wizard creates. Change it by editing config.yaml if your producer runs less often than that.

Screen 9 · wizard step 7 of 8

Step 7: Storage, retention and holds

Where the NAS copy lives, how long backups are kept, and whether retnd may free space on the remote server. The retention half used to be a row of controls that were drawn and never sent, and they are gone: retention is a deployment-wide policy with an editor of its own, and this step reads it back rather than offering a second place to set it.

Step 7 showing a NAS destination field with a Validate path button, the retention chain unavailable, and the remote source handling box with its acknowledgement unticked
Step 7 for the worked example, before the acknowledgement. Continue is refused until that box is ticked, which is why this is the last step that can block you.
ControlExampleWhat the entry does
NAS destination /data/backups/api-server/ Becomes local_path, and it has to be absolute. This is a path inside the container, not on the NAS filesystem. The Compose file mounts your NAS backup share at /data/backups, so this example lands at <your BACKUP_DIR>/api-server/ on the NAS. Give each backup set its own subdirectory: this path also becomes the containment root that the local prune refuses to delete outside of.
Validate path none to give On a platform with no native storage picker the button is labelled this way rather than pretending to offer a browser the integration does not have. The hint below the field names the mounted path for you.
This source is read-only leave it unticked for the worked example Declares that retnd may pull from the remote server but must never delete the original. Ticking it replaces the acknowledgement below with a sentence saying every remote copy is kept for good. If the write probe on step 2 proved these credentials cannot write to the source, this is ticked and disabled for you, with the reason beside it: retnd will not offer to remove a file it has not proved it can remove.
The acknowledgement tick it “I understand the remote backup will be removed only after the NAS copy has been safely committed.” Unticked, this step is unfinished: the review step is unreachable and every save is refused. It is asked here, where the page already explains what deleting the remote source means, rather than as an unexplained toggle earlier in the flow.

The box those last two controls live in is the product stating its own most dangerous behaviour before you agree to it, and the chain it draws is the real order of operations: discovered, transferred, verified, committed, safe state persisted, remote artifact deleted. The delete is last and the journal write is before it, so a crash in between leaves two copies rather than none.

Step 7 with the acknowledgement ticked and Continue now available
Acknowledged. This is the step that unlocks the review step: with it ticked, every step behind you is answered and the tracker is finally green all the way across.
During first run this step cannot read the retention chain

Where the picture above says the deployment's retention chain could not be read, that is the real first-run state rather than a fault: an instance with no configuration does not serve the retention route at all, and the wizard is the one surface that runs before there is a configuration. It changes nothing about the set being created — it will still be retained under whatever the policy says once there is one — and the same step on an ordinary Add backup set reads the chain back and shows it. What the chain actually is, on this deployment and after setup, is below.

What actually decides deletion

Retention is not asked for on this step and it is not per backup set by default. It is a deployment-wide policy, editable under Settings, which any individual set can override with one of its own. It governs deletion of the NAS copies and is unrelated to the remote delete, which is gated by the lifecycle rule and happens as soon as a verified local copy is durable. A hold placed on a backup later overrides all of it: a held snapshot is never expired, by any tier, until the hold is released.

Since 0.3.3 each tier in that policy also says where its copies live, and the drive on this machine is one of the choices rather than an unnamed default. The reference page covers the destinations, the picker and the connection test behind it.

A wizard-created configuration is written with the default chain, three tiers:

TierBuckets byWindowWhat it keeps
dailyday7 daysThe newest good artifact in each of today and the six days before it.
weeklyweek3 monthsThe newest good artifact in each week, looking back over three calendar months. The window is counted in months, not weeks, which is why this tier needs its own window_unit.
monthlymonth12 monthsThe newest good artifact in each of the last twelve calendar months.

Each bucket contributes at most one artifact, and what is kept is the union of every tier's selections plus the protected last-known-good. Anything the union does not claim is a delete candidate, including artifacts that fell in a gap between two tiers' windows rather than outside all of them. Tier order never changes which artifacts survive, only the order tier names appear in a verdict.

Before you change it

There is no spelling of “keep nothing” in the schema. Emptying the tier list does not disable retention, it reinstates the default daily/weekly/monthly policy, which is the fail-safe direction. Retention is turned off by not running a retention pass. And retnd retention previews the decisions without acting on them, which is the right way to check a chain edit before it deletes anything. The web interface has the same preview, with the engine's reason beside every artifact.

Screen 10 · wizard step 8 of 8

Step 8: Review, and Finish setup

Nothing is typed here. What makes this step finished is that the flow can actually be committed, which is the same question the save buttons answer.

Step 8 Review showing a nine-cell summary grid and two save buttons, Finish setup and Save disabled
The review summary, with the connection already proven back on step 2 and the save buttons available. During first run there are two of them, not three.

The summary grid reads back what will be written. Check three things in it specifically, because they are the three the earlier steps make easy to get wrong:

The two steps this tutorial's engine does not use are in the grid too, as not applicable rather than as a blank: a summary that quietly omitted them would read the same as one where they had been answered.

ButtonWhen to use itWhat it does
Finish setup the normal path Writes the configuration and enables the backup set. The write is an exclusive create, not a rename over whatever is there, so if something else produced a config file in the meantime this refuses rather than destroying it.
Save disabled when the remote side is not ready Writes exactly the same configuration with disabled: true. The set is excluded from every processing cycle until you enable it, so nothing is pulled and nothing is deleted. Useful if you want the configuration in place before the producer starts writing.

If a button is disabled, the sentence beside it names the reason: an un-imported key, an untrusted host, a host key that has changed since it was trusted, a connection nobody has tested and the unticked acknowledgement each get their own message rather than a single generic one, and each names the step to fix it on.

What lands on disk

Everything that can fail happens before anything is persisted: the request is validated, the key is resolved, the whole assembled configuration goes through the same validation a hand-edited file goes through at boot, the state directory is checked, and any chosen validator is resolved. Only then is the file created. A configuration the process refuses to finish is never the configuration left behind.

For the worked example, this is the file:

poll_interval: 1h0m0s
state:
    database: /data/state/state.db
sources:
    - id: api
      backup_sets:
        - id: api-server-nightly
          remote:
            type: sftp
            host: api-server.example.net
            port: 22            # the port you entered on step 1
            user: backup-agent
            key_file: /etc/retnd/config/ssh_keys/6f2a1c92-7d40-4b18-9a6e-1e0c5f3b77aa
            key:
                file: /etc/retnd/config/ssh_keys/6f2a1c92-7d40-4b18-9a6e-1e0c5f3b77aa
                env: ""
                command: []
            known_hosts: /etc/retnd/config/known_hosts.d/api_api-server-nightly_known_hosts
          remote_path: /var/backups/
          local_path: /data/backups/api-server/
          include:
            - '*.tar.zst'
          completion:
            strategy: rename
            stable_for: 0s
            delete_safety_delay: 0s
          stale_after: 48h0m0s
          disabled: false
          validation:
            hash: ""
            validator_id: ""
            command: null
          revalidation:
            interval: 0s
            max_per_cycle: 0
            hash: false
            command: null
retention:
    timezone: UTC
    week_starts_on: monday
    daily_days: 7
    weekly_months: 3
    monthly_months: 12
    protect_last_known_good: true
alerts:
    enabled: false
    repeated_failure_threshold: 3

Five things in that file are worth noticing, none of which any screen mentioned:

Screen 11

What you get next

The configuration is durably on disk the moment Finish setup returns. The instance then tries to bring a real service up against it in the same process, so you do not have to restart a container you just installed, and on success it takes you to the backup sets list, which is the page that now has something on it.

This picture is fixture data

The screenshot below is the backup sets list as the development mock renders it, which means four sets in four different states. A real instance one second after first run has one backup set and no artifacts at all. Nothing in the numbers below is a claim about your deployment. It is here for the shape: the section navigation, the per-set controls, and where things live.

The backup sets list with four sets, each with its state, its last run and per-set controls
The application after setup completes, rendering the development fixture's data. Structurally accurate, factually fictional. Each card carries the controls for that set, and the run buttons are covered in the interface guide.

What to actually do first on a real instance:

  1. Check the port. Open Settings or read config.yaml and confirm remote.port is the number you meant, given that a non-numeric entry became 22.
  2. Run one cycle by hand before trusting the schedule, and watch it reach the end. The default poll interval a wizard-written configuration gets is one hour.
  3. Watch the first artifact all the way through to remote deletion. That is the first moment the whole rule is exercised, and it is the cheapest time to find out that the include pattern matched nothing.
  4. Send yourself a test email from Settings' Account recovery card, and check it arrives. The enrolment confirmation already proved the mail path once, and Send test email is the control that proves it still works — app passwords get rotated and providers change their submission rules, and the failure is silent until the day you need a reset link.
Screen 12

Coming back later

Once an administrator exists, the enrolment page is closed and this is what the address answers with.

The retnd sign-in page with username and password fields and a link reading First time here? Create the administrator account
The sign-in page. It is deliberately not styled like a NAS system login, and it says so, because an operator must never believe they are handing NAS operating-system credentials to this application.

The “First time here? Create the administrator account” link at the bottom goes to the enrolment page without a token. That is the right destination on a genuinely fresh install reached without the printed link, and on an instance that already has an administrator it will fail, correctly, with the message discussed above.

This page also carries Forgot password?, which the picture above predates; the next screen is what it does.

Screen 13

Forgetting the password

Enrolment is still a one-time door, and it is still not reopened by losing the password. The way back in is the recovery address, and it starts with Forgot password? on the sign-in page. Two screens, both reached without being signed in: Reset your password at /forgot-password, which asks for the Username and has one button, Email a reset link; and Choose a new password at /reset-password, reached only from the link in the mail, with New password, Confirm new password and Set password.

The first of them asks for the username and nothing else, and it answers the same way whatever you type — a confirmation that a message has been sent, if one was due — because an endpoint that answered differently would tell anyone who can reach the port what the account is called. The page is not where you find out whether you got the name right. The mailbox is.

  1. A message titled “retnd: password reset” arrives at the recovery address, carrying a link of the form http://10.0.0.10:8080/reset-password?token=<a long random string>. It is built from the same PUBLIC_BASE_URL as the enrolment notice, so a deployment still on the shipped default of localhost mails a link that only resolves on the NAS itself. Unlike the enrolment notice, nobody is watching a log to notice that, which makes it worth setting properly at install time.
  2. The link works once, and for 30 minutes. Same shape as the enrolment token, and for the same reason. It lives in the engine process, so restarting the container invalidates any outstanding link, and asking again mints a new one.
  3. Set password signs everything out. Every live session is revoked, including the browser that asked for the reset and any other still open elsewhere, so what a successful reset hands you is the sign-in page rather than a dashboard. That is deliberate: a reset is also what you do when you think somebody else has a session, and one that left existing sessions running would not be a recovery at all.

Nothing arrives? Then the address or the mail path is the problem, not the password, and the browser cannot tell you which. If you are still signed in somewhere, fix it in Settings, in the Account recovery card: the recovery address and the SMTP details are both editable there, Send test email proves the path without changing anything, Save recovery settings writes it, and changing the address sends a fresh confirmation to the new one rather than trusting it. If you are not signed in anywhere and no mail arrives, you are down to a factory reset, which archives the administrator record, the catalog and the configuration, reopens enrolment, and leaves every retained backup on disk — install --mode factory-reset, and it is the last resort rather than the procedure.

While we are here: the password can also simply be changed from Settings, which asks for the current one. That is the ordinary rotation and it has nothing to do with the mail path.

Screen 14

Verifying the recovery address

The message enrolment sends is not a receipt. It is a verification link, and the account it belongs to is provisional until somebody opens it: /verify-email?token=<a long random string>, one page, no fields, reached from whatever device holds the mailbox — a phone that has never signed into this deployment opens it just as well as the browser you enrolled from, because the token in the link is the credential and nothing else is asked for.

Three facts about it, in the order they matter:

  1. The link works once, and for 30 minutes. Opening it a second time says the link has expired or has already been used, which is also what an unknown token, a link for an account that no longer exists and an address that is already verified all say — one answer, so that nothing about the account can be probed with it. If the window closed, sign in and use Resend verification email in the banner or in Settings; a resend mints a fresh link and invalidates the previous one.
  2. Nobody opening it deletes the administrator. That is the point of it, rather than a harsh edge: an address that was mistyped is an account that is already lost, and the loss is only discovered months later at the one moment it cannot be fixed. So an unverified account lapses at a deadline fixed when it was created — at least 30 minutes — and when it does, the record, its sessions and its SMTP settings go, enrolment reopens, and a fresh enrolment notice is printed to the engine's log exactly like the one on screen 0. Re-running a 30-second enrolment with the address spelled right is the whole of the recovery.
  3. Verifying is permanent. The deadline is cleared for good, so an established administrator who later edits the address in Settings gets an unverified address and a banner asking them to confirm it — never a deleted account.

While the address is unverified, every signed-in screen carries an undismissable banner naming the address, the deadline and a Resend action. It is deliberately not dismissable: the thing it is warning about is the account disappearing.

Adding the second VPS

First run configures exactly one backup set, because the point of it is to turn an unconfigured instance into a serviceable one, not to finish the whole deployment. The second VPS is an ordinary addition afterwards.

Go to Backup sets, press Add backup set, and you get the same eight steps you have just walked, with three differences:

Give the second set its own local_path, for example /data/backups/cicd/. Two backup sets sharing a destination directory would put two prune containment roots on the same tree, which is not a configuration worth having.

Both sets will land under the same api source, so their identities become api/api-server-nightly and api/<your second set>. Retention stays a single global policy applied to both.

Screens not pictured here

Five states on this path exist in the product and are not on this page, because the development mock cannot produce them. They are described rather than quietly omitted.

This page moved under itself once already

An earlier version of it walked a dedicated setup page whose heading was “Set up retnd”. That page no longer exists: #275 replaced it with the ordinary wizard reached from the banner, and the capture script kept describing the old flow until it stopped running at all.