Serving a MinIO/S3 warehouse over Iceberg-REST — guide
skade-katalog serve fronts a RedbCatalog over Iceberg-REST and/or gRPC. By default it assumes the Iceberg blobs live on the local filesystem. Build it with --features object-store and it can front a warehouse whose blobs live in S3/MinIO — i.e. the warehouse skade::Warehouse::open_with_store writes.
cargo build --release --features object-store # in skade/serve
The two things that must match the writer
A warehouse is a redb file plus a place the blobs live. serve needs both, and both must be what the writer used:
writer (skade::Warehouse) |
serve |
open_with_store(db_path, warehouse_uri, store) |
--db db_path |
↑ 2nd argument, e.g. s3://red/skade-warehouse |
--warehouse-uri … |
the S3Config behind store |
--s3-bucket/--s3-endpoint/… |
catalog name (always skade) |
--catalog (already defaults to skade) |
Mind the argument order.
Warehouse::open_with_storetakes(db_path, warehouse_uri, store)— the local redb file first. Passing them the other way round is silent: the redb lands in a directory literally nameds3:/<bucket>/…under the writer's working directory, and the blobs go to a prefix named after the redb path.serve --dbthen opens a different, empty redb and reports{"namespaces":[]}.
Running it
export MINIO_ACCESS_KEY=… # or SKADE_S3_ACCESS_KEY_ID / AWS_ACCESS_KEY_ID
export MINIO_SECRET_KEY=… # or SKADE_S3_SECRET_ACCESS_KEY / AWS_SECRET_ACCESS_KEY
skade-katalog serve \
--db /var/lib/skade/warehouse.redb \
--warehouse-uri s3://red/skade-warehouse \
--s3-bucket red \
--s3-endpoint https://minio.example:9000 \
--rest 0.0.0.0:8181
Credentials are taken from the environment only — never a flag, so they stay out of ps, shell history and systemd unit files. Path-style addressing is the default (MinIO needs it); pass --s3-virtual-hosted for real AWS-style hosts. --warehouse ROOT is not needed in this shape; it stays required when you serve a plain local file:// warehouse, exactly as before.
Check it:
curl -s localhost:8181/v1/namespaces
curl -s localhost:8181/v1/namespaces/main/tables
curl -s localhost:8181/v1/namespaces/main/tables/personer | head -c 400
loadTable returning a metadata document with an s3://… metadata-location means the whole path is live.
Or let the shipped check do all three and tell you PASS/FAIL. It writes a small table into a throwaway prefix, serves it, loads it back over REST and deletes what it wrote:
export MINIO_ACCESS_KEY=… MINIO_SECRET_KEY=…
export SKADE_LIVE_S3_ENDPOINT=https://minio.example:9000 SKADE_LIVE_S3_BUCKET=red
cargo run --release --features object-store --example live_object_store
It skips with one line (exit 0) when those variables are unset, so it is safe to wire into any pipeline. SKADE_LIVE_S3_KEEP=1 leaves the objects for you to inspect.
serve refuses to start rather than half-work
The failure this feature exists to remove is a catalog that lists every table and loads none. So the two pairings that produce it are checked before the socket binds:
--warehouse-uri s3://…without--s3-bucket— the catalog would come up on the local filesystem while handing outs3://addresses;--warehouse-uri s3://…on a binary built without--features object-store— nothing in it can reach that scheme.
--warehouse-uri file://… is fine in both shapes: relocating a local root is not the same as needing a store.
Pointing Apache Doris at it
CREATE CATALOG skade PROPERTIES (
'type' = 'iceberg',
'iceberg.catalog.type'= 'rest',
'uri' = 'http://<serve-host>:8181',
'warehouse' = 's3://red/skade-warehouse',
's3.endpoint' = 'https://minio.example:9000',
's3.region' = 'us-east-1',
's3.access_key' = '…',
's3.secret_key' = '…',
'use_path_style' = 'true'
);
Doris fetches the table pointer and metadata from serve and then reads the Parquet directly from MinIO with its own credentials — which is why the S3 properties appear on both sides.
Troubleshooting
| symptom | cause |
{"namespaces":[]} |
--db points at a redb that isn't the writer's (redb creates an empty one silently). Check the file exists and is non-empty before starting serve. |
namespaces list, loadTable 404 "No such file or directory" |
serving an object-store warehouse without --features object-store / without the --s3-* flags: the front is looking on the local disk for an s3:// path. |
HTTP 403 … InvalidAccessKeyId |
credentials not in the environment, or the wrong ones. |
s3 put … HTTP 403 while writing |
the same, on the write side. It used to be silent — every put "succeeded" and the breakage surfaced later as an unparseable metadata document. |
HTTP 404 … NoSuchKey |
--warehouse-uri/--s3-bucket don't match what the writer used. |
Failed to parse json string … expected value at line 1 column 1 |
you are on a pre-2026-08 skade: that is S3's XML error document being read as Iceberg metadata. Upgrade and the same run names the status, the operation and the key. |
--warehouse-uri … is not on this filesystem: it needs --s3-bucket |
exactly what it says; serve refuses rather than come up half-working. |
| redb "already open" | another process (often a still-running writer) holds the write lock. serve is the single lock owner by design. |