The short version: you bring a storage bucket, or several. A bucket is a folder in an online storage service such as Cloudflare R2 or AWS S3. Your code and your backups go in there, and nothing of yours is ever stored by moin. One bucket serves every app; more than one keeps a client’s data in the client’s bucket. moin only signs short-lived links into it.
Why a bucket
Your laptop and your server never talk to each other directly: the server has no open port, your laptop sits behind a router. They need a place in the middle to hand things over. That place is your bucket, not our servers.
moin deploy laptop ──▶ your bucket ──▶ your server (the server deletes it after)
backups server ──▶ your bucket (every change, plus snapshots)
GitHub GitHub ──────────────────▶ your server (no bucket needed)
moin keeps the bucket’s secret key encrypted and uses it for one thing: to sign links that work for a few minutes. Your code and your data never pass through moin.
What needs it
| Needs the bucket | |
|---|---|
moin deploy from your laptop, the TUI or the moin GitHub Action |
yes |
| Backups and restores | yes |
| Deploys from GitHub | no: the server downloads the commit from GitHub itself |
| Image deploys from CI | no: the server pulls the image from your registry |
Add it
Tells moin which bucket to use. Do it once per workspace: one bucket serves all your servers and apps. The first bucket is the workspace’s default.
Buckets in the sidebar, then Add bucket
Any S3-compatible storage works: Cloudflare R2, AWS S3, Backblaze B2, Hetzner Object Storage, Exoscale, MinIO. Pick one in the country your data should stay in.
Before you add it:
- Create a bucket in your storage provider’s dashboard.
- Create an access key that may read, write and delete in that bucket. You get two parts: the access key (its ID) and the secret key.
- Look up the endpoint, the web address of the storage. Your provider shows it next to the bucket. For Cloudflare R2 it is
https://<account-id>.r2.cloudflarestorage.com.
What moin needs:
| Field | What to type |
|---|---|
| Name | Your name for it in moin, like default or client-a: lowercase letters, digits and dashes. The CLI takes it as the first argument. |
| Endpoint | The endpoint. It must start with https://. Only http://localhost and http://127.0.0.1 may use plain http://, on a self-hosted control plane. For AWS S3, leave it empty. |
| Bucket | The name of the bucket. |
| Folder | Usually empty. See The folder field. |
| Access key, Secret key | The two parts of the key. The CLI asks for the secret key on stdin, so it never ends up in your shell history. |
| Region | auto for Cloudflare R2. For AWS S3, the region of the bucket, like eu-central-1. Otherwise what your provider says. |
moin writes and deletes a test file first, so a wrong key fails right away. To make a server back up only to this bucket, see the backups-to rule. The hosted control plane only reaches buckets on the public internet, not on a private or local address.
What goes in the bucket
Everything of one workspace goes into one folder, named after the workspace’s ID. Inside it each app gets its own folder. So two workspaces can share one bucket, even with apps of the same name, and never touch each other’s files.
With only a bucket name:
<your-bucket>/
└── <workspace>/ ← your workspace's ID, it never changes
└── <app-name>/ ← the name of your app
└── <app-id>/ ← this app's ID, a new app with the same name gets a new one
├── uploads/ ← your code on its way to the server
└── <database-file>/ ← one per database (e.g. data-app.db for /data/app.db)
├── litestream/ ← every change, any second of the last 7 days
└── snapshots/ ← full copies, the newest 50
With a bucket name and a folder, everything moves one level down, into the folder:
<your-bucket>/
└── <folder>/ ← the folder you typed into the Folder field
└── <workspace>/ ← your workspace's ID, it never changes
└── <app-name>/ ← the name of your app
└── <app-id>/ ← this app's ID, a new app with the same name gets a new one
├── uploads/ ← your code on its way to the server
└── <database-file>/ ← one per database (e.g. data-app.db for /data/app.db)
├── litestream/ ← every change, any second of the last 7 days
└── snapshots/ ← full copies, the newest 50
What each folder holds:
- uploads: your code on its way to the server. Temporary: deleted as soon as the server has it, and anything left over is deleted after 24 hours.
- litestream: every change as it happens, so you can go back to any second of the last 7 days.
- snapshots: full copies before every deploy and change, the last 50. Each one is a plain gzipped SQLite file, or for Postgres a
pg_dumpyou open withpg_restore. - walg: for Postgres, every change and the base backups, kept by WAL-G for 7 days.
The Buckets page shows your workspace’s folder in each bucket. The Backups tab of an app shows the app’s folder and its bucket.
How long things stay
| What | How long it stays | Who deletes it |
|---|---|---|
uploads/ |
until the server has it, 24 hours at most | the moin agent on your server: right after the download, and once an hour anything older than 24 hours |
litestream/ |
every change of the last 7 days | the moin agent on your server, with Litestream’s own cleanup: older changes go as new ones come in |
snapshots/ |
the newest 50 per database | the moin agent on your server: after each new snapshot it deletes the oldest beyond 50 |
| an app you removed | until you delete it | you: delete its <app-id>/ folder in your storage provider’s dashboard or with any S3 tool |
| a bucket or folder you changed | until you delete it | you: moin starts in the new place with a full copy and leaves the old place as it is |
| a bucket you removed from moin | until you delete it | you: moin deletes nothing |
| an app you moved to another bucket | until you delete it | you: its older backups stay in the old bucket, and moin still lists and restores them from there |
Remove an app and create a new one with the same name, and the new app starts with an empty folder: old backups never mix with new ones. The old app’s backups stay in its own <app-id>/ folder. To get its data back, download a snapshot from there, gunzip it and open it with sqlite3.
More than one bucket
A workspace can keep several buckets, each with its own name. Reasons to add another one:
- a client’s data in the client’s own bucket,
- a bucket in another region, near the server,
- separate costs per project,
- production backups apart from test apps.
One bucket is the default: every app without a bucket of its own uploads and backs up there, and so does every new app. The first bucket you add is the default; a bucket from before moin had more than one is the default, named default.
See your buckets
Lists every bucket, the default first, with the apps that use it and the apps that keep older backups in it. Secret keys never show.
Buckets in the sidebar
Choose the bucket of an app
Moves the uploads and the new backups of one app to another bucket. Without a choice an app follows the default.
the app page, Backups tab, Use another bucket, then Move
What happens to the backups of the app:
- New backups go to the new bucket. It starts with a full copy of every database, then every change, so a point in time restore reaches back to the move and no further.
- Older backups stay in the old bucket. moin moves and deletes nothing. The backups list of the app shows the older snapshots too, with the bucket they are in.
- Restores read from where a backup lives. Restoring an older snapshot downloads it from the old bucket: on the web Restore on the snapshot, in the CLI
moin restore <snapshot> --bucket <old bucket>. A point in time restore always reads from the app’s bucket now. - Uploads of
moin deploygo to the new bucket right away.
Making another bucket the default moves every app that follows the default the same way. The rule backups-to of a server allows one bucket: an app on that server in another bucket is refused there.
A server’s agent needs this release or newer to back up an app to its own bucket. Older agents back up every app to the default bucket; agents update themselves unless you turned that off.
Change a bucket
Renames a bucket, points it at another place, gives it new keys or makes it the default. Leave the secret key empty to keep the stored one. A new place starts with a full copy, and the old place keeps what it has.
Buckets in the sidebar, Change on the bucket, then Save bucket; Make default
Remove a bucket
Removes a bucket from moin. What is in it stays there.
Buckets in the sidebar, Remove on the bucket, then type its name
moin refuses to remove a bucket that is still in use, and names the apps:
- Apps use it: their uploads and backups go there, the default’s also for every app without a bucket of its own. Move them to another bucket first.
- It keeps older backups of an app that moved away. Restores of those read from there, so the bucket stays while the app exists.
A workspace without apps can remove its last bucket. moin asks you to type the bucket’s name first; --yes skips it in scripts.
The folder field
The Folder field on the Buckets page is --path in the CLI and path in the API and MCP. Most people leave it empty. Fill it in when the bucket holds other files too, such as a bucket you already use for something else. Everything of moin then stays inside that one folder, like moin/, and the rest of the bucket is left alone.
You do not need it to share one bucket between workspaces: each workspace gets its own folder anyway.
Next
- Backups: what moin keeps in the bucket and how to restore it.
- Deploy: how
moin deployputs your code into it. - Remove a bucket: what moin refuses, and why.