Self-hosted, S3-compatible
object storage.
Cairn speaks the S3 API, stores objects as plain files on a normal filesystem, and keeps all metadata in embedded SQLite. One static binary. No external database, no clustering layer, no separate storage daemon to run.
A single-node object store that speaks S3
Cairn implements the Amazon S3 API with enough fidelity that unmodified S3 clients, SDKs, and the aws CLI work against it without special-casing. Object bytes live as plain files on a POSIX filesystem; all metadata lives in one embedded SQLite database, which is the single source of truth. Point any S3 client at it, or use the built-in web console.
- Plain files on disk
- Object data is ordinary files on whatever filesystem you choose — nothing proprietary to unpack if you ever walk away.
- Embedded SQLite metadata
- All metadata in one database file, the single commit point for every write. Back it up, copy it, reason about it.
- One static binary
- The server, the web console, and the CLI ship as one file — on a homelab box, a VPS, or a small production node. A distroless container image is published too.
How it works
Cairn is one process with two listeners and two durable stores — no clustering layer, no external database, no separate storage daemon. A request flows straight down the stack: authenticated, authorized, and handled, then committed. Reads come off a WAL pool; every write goes through a single group-committing writer, so the database has exactly one linearization point. Each store sits behind a small trait interface, so the SQLite metadata engine, the file BlobStore, and the crypto layer are swappable without touching the S3 surface above them.
Any unmodified S3 client. Requests are signed with SigV4 or carry a Bearer token; the console drives the same API over its session.
Two ports, one process. Health, metrics and the STS mint answer on :7373 ahead of the concurrency limiter.
Each request is authenticated, then run through a real policy-evaluation engine, then handled. Bodies stream; server-side encryption seals on write and opens on read, and fails closed.
All writes funnel through one group-committing writer — the single linearization point. Reads come off a WAL connection pool; a bounded cache fronts both.
Object data is ordinary files on the filesystem. Writes stage, fsync, and rename into place; a bounded reconciliation reclaims orphaned blobs on restart.
Durable outbox-driven workers run off the metadata store: asynchronous replication, lifecycle expiry, webhook delivery, and an opt-in integrity scrub.
Durability is an ordered contract. Object bytes are staged and fsynced before the metadata transaction that references them, and that commit is the single point where a write becomes real — so an acknowledged write has already survived power loss. On restart, Cairn reconciles and reclaims anything a crash left behind.
The metadata commit (step 6) is the linearization point: nothing before it is acknowledged, and nothing after it can lose the object.
What it does
- S3 API
- Buckets and objects, byte-range and conditional reads, multipart upload, copy, and bulk delete. v1/v2 and version listings. Checksums (CRC32, CRC32C, CRC64NVME, SHA-1, SHA-256). Versioning, tagging, CORS, lifecycle expiration, object lock (WORM), and presigned URLs. SigV4 — header and streaming-chunked — and Bearer authentication. The aws CLI and the standard AWS SDKs work against it.
- Web console
- Manage buckets and users on its own port. Browse, upload, download, and share objects. Mint access keys scoped by a policy. Watch storage, compression, and replication status. Jump anywhere with a command palette, and see when a newer release is available.
- Access control
- Bucket policies, ACLs, Block Public Access, and Object Ownership — a real policy-evaluation engine, not a role check. Short-lived credentials through the AWS-STS surface (AssumeRole, GetSessionToken), served on the S3 port or the management API.
- Security & encryption
- Native TLS. AES-256-GCM encryption of secrets at rest, with online master-key rotation. Server-side object encryption: per-bucket SSE-S3, an aws:kms request surface, optional mandatory-encryption enforcement, and transparent encryption of every object at rest.
- Durability
- Writes are staged, fsynced, atomically renamed, and acknowledged only after a single metadata commit. On restart Cairn reconciles and reclaims orphaned data. Acknowledged writes survive power loss.
- Storage efficiency
- Optional per-bucket block compression, with range reads that touch only the blocks they need.
- Operations
- Prometheus metrics, liveness and readiness endpoints, and structured logs. Asynchronous bucket replication to another node or S3 endpoint. Webhook event notifications. One-shot streaming import from another S3-compatible store — MinIO, Garage, R2, AWS, or another Cairn. A CLI for bootstrap, config validation, integrity checks, and backup and restore.
What it looks like
The S3 wire protocol is a commodity; the console is the reason to pick Cairn. It ships inside the same binary — nothing extra to deploy — and covers everything short of the S3 API itself.










Measured against MinIO
A like-for-like comparison against MinIO, both on the same 2 vCPU / 16 GB host, both single-node and single-drive over plaintext HTTP, using MinIO's own warp benchmark. Each figure is the median of three runs. Absolute numbers depend on the host; the ratio is what to read.
| Op | Size | Cairn | MinIO | Ratio |
|---|---|---|---|---|
| PUT | 4 KiB | 1696 obj/s | 1308 obj/s | Cairn 1.30× |
| PUT | 8 MiB | 115 MiB/s | 128 MiB/s | MinIO 1.12× |
| GET | 4 KiB | 3431 obj/s | 1815 obj/s | Cairn 1.89× |
| GET | 8 MiB | 1463 MiB/s | 801 MiB/s | Cairn 1.83× |
| STAT | 4 KiB | 3494 obj/s | 1824 obj/s | Cairn 1.92× |
| DELETE | 4 KiB | 7442 obj/s | 5236 obj/s | Cairn 1.42× |
| LIST | 4 KiB | 21183 obj/s | 10288 obj/s | Cairn 2.06× |
| MIXED | 1 MiB | 285 MiB/s | 271 MiB/s | Cairn 1.05× |
Serving the same workload, Cairn peaked at 106 MB of memory to MinIO's 1072 MB — about a tenth — at76% mean CPU versus 88%.
Built to replace a single-node MinIO
If you run a single-node MinIO, or any single-drive or single-host S3 endpoint, Cairn is built to replace it: a simpler operational model, a smaller and safer binary, transparent compression, an embedded console, and asynchronous replication for redundancy — without losing S3 compatibility. The wire protocol is a commodity; the console and the operational model are the product.
- Written in Rust
- Predictable, garbage-collector-free tail latency, and memory safety on a surface that parses untrusted input — SigV4 chunk framing, S3 XML, policy JSON — with no runtime cost.
- Self-contained
- The web console is compiled into the binary; there is no separate service to deploy. The CLI gives the same control from a terminal.
- Honest about what it is
- Single-node by design, and clear about the line. See Scope.
Single-node by design
Cairn is single-node by design: one process, one data filesystem, one metadata database. That's the line, and it's deliberate — it's what keeps Cairn simple to operate.
- Cross-host redundancy comes from asynchronous bucket replication — eventually consistent, with observable lag — not from clustering. There is no consensus, sharding, or erasure coding.
- Drive redundancy is left to the storage underneath: ZFS, RAID, or a replicated block volume.
- Server-side encryption is real, but the
aws:kmskey id is a validated label over the one node master key, not independent key material — so it is SDK compatibility, not cryptographic tenant isolation. A dedicated external KMS with distinct per-key material isn't implemented yet. - The target is homelab and small-to-mid production that wants the S3 API and a console without running a distributed system.
Deploy Cairn
One command sets Cairn up on a host or with Docker, and updates an existing install when you run it again. The installer asks which you want, or take --host or--docker --yes to skip the prompt; --uninstall removes it and keeps your data.
curl -fsSL https://raw.githubusercontent.com/Harsh-2002/Cairn/main/install.sh | sudo shRun the container directly
The image ships a nonroot /data and defaults to serve. The only variable you need to set is the master key.
docker run -d --name cairn \
-p 7373:7373 -p 7374:7374 \
-v cairn-data:/data \
-e CAIRN_MASTER_KEY="$(openssl rand -hex 32)" \
ghcr.io/harsh-2002/cairn:latestOr with Docker Compose
Copy this, set a real CAIRN_MASTER_KEY, and run docker compose up -d. The optional variables are commented with their defaults — uncomment what you need.
services:
cairn:
image: ghcr.io/harsh-2002/cairn:latest
container_name: cairn
restart: unless-stopped
ports:
- "7373:7373" # S3 API
- "7374:7374" # console + API
volumes:
- cairn-data:/data
environment:
# Required — openssl rand -hex 32
CAIRN_MASTER_KEY: "REPLACE_WITH_A_32_BYTE_HEX_KEY"
# Optional (defaults shown)
# CAIRN_ROOT_ACCESS_KEY: "cairn" # admin key
# CAIRN_ROOT_SECRET_KEY: "cairnadmin" # admin secret
# CAIRN_REGION: "us-east-1" # region label
# CAIRN_ENCRYPT_AT_REST: "true" # encrypt at rest
# CAIRN_S3_DOMAIN: "s3.example.com" # virtual-host style
# CAIRN_TLS_CERT_PATH: "/certs/tls.crt" # TLS (set both)
# CAIRN_TLS_KEY_PATH: "/certs/tls.key"
# CAIRN_WEB_ADDR: "off" # headless
volumes:
cairn-data:The S3 API is then on :7373 and the web console on :7374. Default login is cairn / cairnadmin — override it before exposing a node.
Configure with the environment
Cairn is configured entirely by CAIRN_* environment variables — no config file, no server flags — so a deployment is reproducible whether it runs on a host or in a container. Every variable has a default and an invalid one fails fast at startup with a clear message. The one you should set for a real node is CAIRN_MASTER_KEY; the rest are optional.
| Variable | Default | What it's for |
|---|---|---|
| CAIRN_MASTER_KEY | a dev key (insecure) | A 32-byte hex key that seals every secret at rest. Set it in production — a node still on the built-in key refuses to bind a public address. Keep it constant for the life of your data. |
| CAIRN_ROOT_ACCESS_KEY | cairn | The root administrator's access key, ensured on every startup. |
| CAIRN_ROOT_SECRET_KEY | cairnadmin | The root administrator's secret. Override it before exposing a node. |
| CAIRN_LISTEN_ADDR | 0.0.0.0:7373 | Where the S3 API binds — it also serves /healthz, /readyz, /metrics and the AWS-STS mint. |
| CAIRN_WEB_ADDR | 0.0.0.0:7374 | Where the web console and management API bind. Set to off / none / disabled to run headless. |
| CAIRN_DATA_DIR | /data | Root of the object (blob) storage. Must share a filesystem with the metadata database. |
| CAIRN_DB_PATH | /data/cairn.db | The SQLite metadata file — the single source of truth. Back it up together with the data directory. |
| CAIRN_REGION | us-east-1 | The region label returned by the location API and checked in the SigV4 credential scope. |
| CAIRN_TLS_CERT_PATH / _KEY_PATH | unset (plaintext) | Set both to enable built-in TLS; otherwise run plaintext behind a TLS-terminating proxy. |
| CAIRN_ENCRYPT_AT_REST | off | Transparently encrypt every stored object at rest, even when the client sends no SSE header. |
| CAIRN_S3_DOMAIN | unset | Base domain for virtual-host-style addressing (bucket.domain); unset serves path-style only. |
| CAIRN_PUBLIC_BASE_URL | unset | External base URL used when generating links behind an ingress or reverse proxy. |
| CAIRN_META_SYNCHRONOUS | full | full survives power loss; normal is ~1.7× faster on writes but may lose the last few acknowledged writes on power loss. Neither ever corrupts. |
| CAIRN_ALLOW_INSECURE | off | Permit the built-in insecure defaults on a public bind — for a closed demo rig only, never production. |
That's the common set. There are roughly fifty knobs in all — replication, lifecycle, webhooks, KMS allow-listing, metadata sharding, runtime and durability tuning. See the full CAIRN_* reference.
Use it with the AWS CLI
Because Cairn speaks the S3 API, the aws CLI and the AWS SDKs work against it unmodified — you just point them at the endpoint. Configure a profile once (the default credentials shown; use your own in production):
aws configure set aws_access_key_id cairn
aws configure set aws_secret_access_key cairnadmin
aws configure set region us-east-1Then every S3 command works as usual:
# A short alias for the endpoint
alias s3="aws --endpoint-url http://localhost:7373"
s3 mb s3://demo # create a bucket
s3 cp ./report.pdf s3://demo/ # upload a file
s3 ls s3://demo/ # list objects
s3 cp s3://demo/report.pdf ./out.pdf # download it back
s3 sync ./site s3://demo/site # sync a directory
s3 presign s3://demo/report.pdf --expires-in 3600 # shareable linkEverything the SDKs expect is here — multipart for large objects, byte-range reads, versioning, tagging, and presigned URLs. See the S3 API support matrix for the exact surface.
One version, always the latest
Cairn is calendar-versioned. A release is tagged vYYYY.MM.DD, and exactly one release is ever active: cutting one publishes today's tag, attaches the staticlinux/amd64 and linux/arm64 binaries with a SHA256SUMSmanifest, pushes the multi-arch container image as :latest, and retires every prior release and tag. A release is a deliberate, manual act, gated on a green CI run for the exact commit it ships.
Why keep only one active version? Cairn is single-node, self-hosted infrastructure — not a library other code compiles against — so there is no dependency graph pinning old versions and no back-catalogue of stale binaries to pick the wrong one from. One line moves forward: the latest release is the supported release. The version is baked into the binary and reported bycairn --version and the console footer, and always equals the tag it shipped in; a local build reports a -dev string so it is never mistaken for a release. Each node checks for a newer version hourly and surfaces a hint in the console — best-effort, and off if you set CAIRN_UPDATE_CHECK_ENABLED=false.