# 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_store` takes
> **`(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
> named `s3:/<bucket>/…` under the writer's working directory, and the blobs go
> to a prefix named after the redb path. `serve --db` then opens a *different*,
> empty redb and reports `{"namespaces":[]}`.
## Running it
```sh
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:
```sh
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:
```sh
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 out `s3://` 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
```sql
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. |