SeaweedFS
SeaweedFS is a Go, Apache-2.0
S3-compatible object store whose defining trait is volume-granularity disk
management: data is split into fixed-size volumes (.vol files) that the
master places across volumeservers, giving O(1) reads and cheap small-file
storage. pgcli runs it as a standalone, top-level addon with the same CLI
surface as garage, minio, silo and
rustfs (install, list, logs, autostart, password, pg mc), but
its runtime model diverges in four ways that shape this page:
- One process, all roles. A single node runs
weed mini— master, volume server, filer and S3 gateway in one process (the officially endorsed single-binary deployment). A cluster member runsweed server -filer -s3, one per host, all roles together — never four containers. - Three rendered files, argv for everything else. pgcli renders
s3.json(the S3 identity) and, for a cluster,filer.toml(the PostgreSQL metadata store) under the instance directory and bind-mounts them read-only; the rest of the configuration isweedcommand-line flags. - Cluster metadata lives in a shared PostgreSQL. Every cluster node fronts
filer + S3, so the filer namespace must be one database, not per-node
leveldb2 — pgcli points all members at one
--dsnPostgreSQL. This is the natural home ground of pgcli: the store can sit next to (or in) a pgcli-managed instance. - Native S3 TLS —
--tls(self-signed) or--tls-cert/--tls-key(BYO, e.g.pg certoutput) — the MinIO posture, not Garage’s nginx-front one. With TLS on, the pooled S3 port serves HTTPS only; the cleartext listener is not bound at all.
Platform support: tested on Linux (host networking). The macOS bridge path is code-complete and follows the same shape as the other object stores, but has not been exercised on a Mac yet.
How It Works
One container per instance, from the upstream
docker.io/chrislusf/seaweedfs:4.48_large_disk image (pull-only). pgcli
overrides the image’s ENTRYPOINT and runs /usr/bin/weed -logtostderr=true <subcommand> … directly: the stock entrypoint would chown -R /data and drop
to uid 1000 on every start — on a large data directory that is exactly the
silent slowdown shape pgcli avoids, and without -logtostderr weed logs to
files inside the container where pg logs cannot see them.
Host state lives under <base-dir>/addon/seaweedfs/<name>/:
data/→/data(volumes and, single-node, the filer’s leveldb2 metadata) — replaced by one bind mount per drive (/data1…/dataN) in multi-disk mode, see Multi-disk,s3.json→/etc/seaweedfs/s3.json:ro(the S3 identity; 0600 on the host),filer.toml→/etc/seaweedfs/filer.toml:ro(cluster /--dsnonly; the[postgres2]section rendering the metadata DSN),cert.pem/key.pem→ mounted under/etc/seaweedfs/when TLS is on.
Credentials are an access key / secret key pair rendered into s3.json
(SeaweedFS’s static-identity mechanism — AWS_* env is lower priority and gets
overridden by the local store, so pgcli never uses it):
root_useris the access key id — on a single-node install it is generated on first install (SW+ 28 hex chars) or supplied via--root-user.root_passwordis the secret key (64 hex chars), generated or supplied via--root-password.- Under
--clusterboth are required and must be identical on every node — see Multi-node cluster.
The identity is rendered with the bare Admin action, which in SeaweedFS’s
permission model short-circuits to full access. All values land in pg.yaml
(addons.seaweedfs.<name>.*) so a recreate is stable. The install also renders
one bucket — named after the instance (--bucket overrides): single-node weed mini -bucket creates it at boot; a cluster seeds it through the S3 API with
mc mb --ignore-existing once the node’s health gate is green.
Install
The single-node output reports endpoints and the minted credentials:
The install ends with a health gate: pgcli polls the master’s JSON
/cluster/status API for up to 30 s before declaring success — a detached
container with --restart unless-stopped would otherwise swallow a bad argv
into a crash loop behind a green-looking install. Only after the gate is green
does the bucket seeding run.
Re-running install against a live instance is a no-op: the container is not
recreated (a stopped one is simply started, with a notice), flags are merged
into the stored config, and existing credentials are kept. Pass --force to
recreate the container so changed ports, listen address, or credentials take
effect — without losing the data directory or the metadata database.
Bind address: the default
127.0.0.1keeps the store local;--listen 0.0.0.0(or thelistenkey inpg.yaml) exposes it on the network. A cluster member always binds0.0.0.0regardless of--listen— peers must reach its gRPC twin (port + 10000) for Raft — while-ipstill registers the single address peers dial.
-ipis never left to auto-detection. weed registers its volumes under whatever address it guesses; the filer/S3 data path then dials that guess and uploads 500 on an otherwise healthy-looking container. pgcli passes-ipexplicitly (--listenvalue single-node,--advertise-hostin a cluster) — verified the hard way on a test host.
TLS
SeaweedFS serves HTTPS natively on the S3 port; there is no nginx front and no web-console leg to proxy. Two shapes, same as minio/rustfs:
What TLS changes under the hood (verified): the cleartext listener is off
(-s3.port=0 — the pooled port simply refuses TCP connections), and the same
pooled S3 port becomes -s3.port.https. So the endpoint printed at install is
https://…:8403, not an extra port. Point pg mc at it as usual — the loopback
alias gets --insecure automatically; for a real or generated CA, pass its
ca.crt to your S3 client.
Multi-disk
A single node can use several disks: weed’s own -dir is a comma list
(dir[,dir]…) that one volume server stores its volumes across, and pgcli
exposes it as a repeatable --drive — one host directory per disk, each
bind-mounted at its own slot:
The summary and pg addon list then show the mapping instead of a single
Data: line:
and the container runs with -dir=/data1,/data2,/data3 (verified: the volume
server’s /status reports each host directory as its own DiskStatuses entry
with independent disk accounting, and remove --clean-data walks the drives
one by one, refusing any that is still a live mount point).
Two semantics worth knowing:
- Placement, not RAID. This is one volume server spreading
.volfiles across its directories — no striping or mirroring at the OS level, and no erasure coding between the drives either (the cluster replication story is orthogonal — see Multi-node cluster). A drive that disappears takes the volumes on it; whether that loses data depends on the replication code, not on the local layout. - Advisory device check.
installwarns about any drive sitting on the same device as the host root filesystem — multi-disk only pays off on separately mounted disks. Unlike MinIO (which refuses such a drive at startup), weed happily uses them, so pgcli warns and continues. Format and mount each disk first; pgcli never partitions or formats anything.
Drives are a per-node knob, and they compose with the cluster: pass
--drive on each member’s install command and every node runs its own
multi-disk volume server behind the shared cluster (verified on a 3-host
weed server cluster where each member mounted two drives — every member’s
process showed -dir=/data1,/data2 with two independent DiskStatuses
entries, and the 001 replica of one 8 MB volume landed on the real disk of one
node and on the other drive of the replica node, i.e. each node’s volume
server picked among its own dirs while placement chose across nodes). The
nodes do not need the same drive count; each account only sees its own -dir
list. Stop/start and --force recreate handle the drive set like any other
config change — the mounts and -dir are rebuilt from pg.yaml.
Multi-node cluster
The cluster is MinIO-MNMD shaped, not Garage’s bootstrap/join: there is no
first node, no join command with a carried cluster secret, no layout to
assign. You run the same install command on every host — Raft membership
is the full --peer list every node passes, and a node that starts before
quorum simply waits (its health gate warns instead of failing; the install that
completes quorum finishes the bucket seeding).
Two things differ from a bigger single node, and both are shared by definition — every member’s command carries them identically:
- One credential pair for the whole cluster. Each gateway authenticates
against only its own rendered
s3.json; SeaweedFS has no shared-credential backend without a standalone IAM server (out of pgcli’s v1 scope), so per-node auto-generation would leave the three gateways accepting three different identities. pgcli therefore refuses to auto-generate under--cluster: pass the same--root-userand--root-passwordon every node. Run the first node’s install with them, copy the pair from its printed summary, and carry it verbatim to the rest (pgcli never executes anything remotely). A half pair (only one of the two) is an error; so is re-running a single-node config’s install with--cluster. - The shared filer metadata database.
--dsn postgres://user:pass@host:port/dbis required, must be dialable from every host — so use the host-published port / LAN address, never a container-local127.0.0.1:5432— and the database itself must already exist: pgcli only probes reachability, it never creates (or drops) databases it does not own. The metadata tables need no setup: weed’spostgres2store creates.system,filemetaand one table per bucket inside it on first use (verified).sslmode=disableis rendered intofiler.toml, so reachability +pg_hbais all that must line up.
Three-host walkthrough
Prepare the metadata database once, anywhere all nodes can reach (here a pgcli-managed instance on host 1, host port 35432):
Then, on each host, the same command with that host’s own
--advertise-host (keys below are placeholders — use one pair everywhere):
Add --drive to that same command to give each member its own multi-disk
volume server (--drive /mnt/sw-disk1 --drive /mnt/sw-disk2 — see
Multi-disk); it is a per-node flag, so hosts may carry
different drive sets. Verified on a 3-host cluster.
The summary ends with a copy-paste block for the remaining hosts with exactly that shape (credentials and DSN filled in from this node’s config).
What the first installs say while the cluster is still assembling is normal:
- Host 1 comes up with a leader already visible (a lone weed master with an
odd peer count can still win its own election here — the observed behaviour
with a 3-node list is host 1 leading immediately), but the bucket seed may
fail with a connection refused: the cluster’s S3 side is not serving yet.
pgcli prints
[!] bucket seeding incomplete … re-run this install command once the cluster has quorumand exits 0. - Host 2 (or 3) completes quorum; the same idempotent seed succeeds there
(
Bucket created successfully). Re-running host 1’s command afterwards re-checks quietly andmb --ignore-existingstays a no-op.
After all three, any member shows the whole cluster:
Reading and writing through any node
Because credentials are shared and the filer namespace is one database, any
gateway serves any object: write through host 2’s S3 port and read back
through host 3’s with the same key — verified end-to-end on a 3-host test
cluster, including a wrong-secret attempt coming back as
The request signature we calculated does not match (it is not a
per-node local trust store). --replication 001 keeps one remote copy of every
volume (volume.list shows ReplicaPlacement:001 and each volume on two
data nodes).
Stop one member and the cluster keeps serving (Raft quorum is a majority; with 3 masters, 2 keep electing and writing):
pg addon start rejoins the node; its volumes re-register under the same
-ip. Changing cluster membership is not orchestrated: the --peer list
is static Raft configuration — add or remove a member by rebuilding the
cluster (v1 limitation, see Limitations).
Getting the cluster’s AK/SK
The pair is by design present in every node’s pg.yaml (you carried it
there), so unlike garage’s CRDT key — whose secret lives on exactly one host —
pg addon password seaweedfs --name store works on any member. If the printed
secret is lost everywhere, pick a new pair and re-install each node with
--force (the rendered s3.json is rewritten; data and metadata are
untouched).
Using the mc Client
pg mc runs MinIO’s mc from a throwaway container — no local install — and
speaks SeaweedFS’s S3 API like any other store. Use the access key in place
of a user and the secret key in place of a password:
Aliases persist in ~/.mc/config.json; flags pg’s parser would claim go after
-- (pg mc rm store/scratch -- --force). On macOS point the alias at
host.containers.internal:<s3-port> instead of 127.0.0.1. Under TLS, use the
https:// endpoint (loopback aliases are auto---insecure).
Administering via pg weed
pg weed runs SeaweedFS’s own weed control tool inside a running member’s
container — the replacement for hand-written podman exec pgcli-seaweedfs-<ns>-<name> …. --name selects the instance (default: the
first by name); pgcli’s own flags must come before the subcommand word, and
weed’s flags pass through untouched. Two shapes:
The weed shell menu is the day-2 surface (command names verified against 4.48
— the shell help lists far more):
weed CLIs can print their result and then linger with open gRPC channels;
pgcli cuts such a command off after its output has been quiet for 3 s (10-minute
overall cap), so a hang surfaces as a loud timeout instead — re-run it.
Ports
Each instance takes four consecutive ports from SeaweedFS’s own pool,
starting at seaweedfs_start_port (default 8400): master, volume, filer,
s3 — and every role also opens a gRPC twin at http port + 10000 (8400 →
18400, …), which pgcli reserves in the pool too. A second instance therefore
lands on 8404-8407 / 18404-18407, and a seaweedfs, a garage and a minio
instance never collide (separate pools: 8400 / 3900 / 9000):
--port sets only the master anchor; the rest follow. Only the S3 port is a
client endpoint; master 8400 also serves the /cluster/status JSON API and the
operator UI pages (the admin UI and webdav are switched off in the argv, and
iceberg/lance side listeners are pinned to 0 so weed opens no extra holes).
Configuration
Instances live under the top-level addons.seaweedfs map in pg.yaml:
Edits to listen, ports, root_user, root_password, image_tag,
data_dir, drives, filer_dsn, or the TLS material take effect after the
next pg addon install seaweedfs --name store --force. In a cluster the force-recreate
must be run on every member (each host owns its own pg.yaml and container).
List
Cluster members gain the Cluster / Peers / Leader / Filer store lines;
Leader: is a live /cluster/status read (best-effort — omitted when the
member is not running, and printed as (none yet — still electing / no quorum)
while a still-assembling cluster has no leader). Single-node members show none
of those four lines. A multi-disk instance shows the Drives block (with one
- /dataN <- host line per drive) in place of the single Data: line — see
Multi-disk. With TLS on, a final TLS: native S3 HTTPS on 8403 (cleartext listener off) line is appended.
pg addon list never prints the secret key; reveal it deliberately with
pg addon list --show-password (appends a Secret key: line), or
pg addon password seaweedfs --name store (--file <path> writes it mode
0600). The Filer store line is host:port/db with the password always
hidden.
Start and stop
install skips a still-present container (starting it if stopped); start only
starts an existing one — and re-renders s3.json/filer.toml first, so
hand-edits to the stored config take effect on restart.
Auto-start on Boot
Containers carry a --restart unless-stopped policy (crashes, not reboots). To
bring instances up after a host reboot:
Autostart is start-only — the rendered files and the container already exist. A cluster member that boots before its peers just waits for quorum like any other cold start. SeaweedFS runs as root in its upstream image, so a boot-time start needs no special host privileges beyond running podman itself.
Remove
The data directory is the object storage — losing it means losing every
volume in it — so remove keeps it by default. --clean-data deletes the data
dir, s3.json, and the filer.toml / TLS directory when present (each only
after a stat — absent optionals print nothing), and refuses to touch anything
that is a live mountpoint. The filer metadata database is never dropped, on
any host, ever: the removal prints
so you know exactly what to DROP DATABASE if the data is truly done with
(every member’s remove prints the same notice; the database outlives all of them).
Logs
Limitations
What v1 deliberately does not do:
- Cluster membership changes (add/remove/replacement nodes) are not
orchestrated —
--peeris a static full list; changing it means rebuilding the cluster. - EC erasure coding, tiering, cloud-sync (replication to other S3s), FUSE
mounts, and the standalone IAM server (dynamic
s3.accesskey.*users) are not managed — all remain manualpg weedterritory, and IAM in particular is the only way SeaweedFS itself shares credentials dynamically, out of scope here. - Credential rotation is a re-install with
--forceon every member; pgcli never edits a lives3.jsonin place. - One bucket is seeded (instance name); further buckets are plain
pg mc mbaway — they land in the same filer database automatically. - The
filer.tomlDSN renderssslmode=disable; if your PostgreSQL demands TLS on that leg, pgcli’s render does not fit it (BYO--dsnwith TLS is not a v1 surface).
Troubleshooting
requires explicit shared credentialson a--clusterinstall. Expected by design — run the first node with an explicit--root-user/--root-passwordpair and carry the same values to every remaining host (the install summary prints the ready-made command with them filled in). Single-node installs may omit them and get auto-generated keys.requires a shared filer metadata database: pass --dsn …. A cluster without--dsncannot keep one namespace across gateways. Pre-create the database and pass a host-reachable URI (published port / LAN address — a container-local127.0.0.1:5432is not).[!] no leader yet — normal while the cluster is still assembling. The health gate’s quorum warning, not a failure: finish the remaining members' installs (or re-run this one once they are up) and it goes green.[!] bucket seeding incomplete … re-run this install command. Same posture one step later: the node reached a leader but its gateway was not serving yet. Re-run the identical install after the last member joins;mb --ignore-existingmakes it a clean no-op if the bucket already landed.- Uploads 500 on a healthy-looking single node. Historically the missing
-ipsignature (volumes register an auto-guessed address the data path then cannot dial); pgcli always passes-ip, so if you see it, check for a hand-edited argv or a very old binary. mc: … you already own it. A baremc mbagainst an existing bucket; pgcli’s own seed uses--ignore-existing, so see this only from manualmcuse — add the flag.- Wrong-secret confusion:
The request signature we calculated does not match. The pair does not match the node’s rendereds3.json— in a cluster, that means the nodes were installed with different credentials; re-run the install with the same pair +--forceeverywhere. pulling seaweedfs image …fails on install.docker.io/chrislusf/seaweedfs:4.48_large_diskunreachable — check Docker Hub access orpodman pullit first.