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.

What it is

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.
Architecture

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.

Clients
S3 SDKsaws CLIConsolepresigned URLs

Any unmodified S3 client. Requests are signed with SigV4 or carry a Bearer token; the console drives the same API over its session.

Listeners
:7373 — S3 data plane:7374 — console + /api/v1/healthz · /readyz · /metricsAWS-STS

Two ports, one process. Health, metrics and the STS mint answer on :7373 ahead of the concurrency limiter.

Request pipeline
authenticateauthorize — policy · ACL · BPA · ownershipS3 handler + lifecyclechunked decoderSSE seal / open

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.

Metadata
Writer — group-commitWAL read poolread-through cacheSQLite (WAL)

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 bytes
BlobStoreplain files (POSIX)block compressionreconciliation

Object data is ordinary files on the filesystem. Writes stage, fsync, and rename into place; a bounded reconciliation reclaims orphaned blobs on restart.

Background
replicationlifecyclewebhooksintegrity scrubmultipart sweepkey re-wrap

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.

1stage bytes2fsync file3rename4fsync dir5validate hashes6commit metadata7reclaim

The metadata commit (step 6) is the linearization point: nothing before it is acknowledged, and nothing after it can lose the object.

Features

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.
Console

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.

The Overview: bucket, object, and version counts beside node details, compression savings, and per-bucket storage.
The Overview: bucket, object, and version counts beside node details, compression savings, and per-bucket storage.
Browsing a bucket, with per-object preview, download, tagging, copy, share and delete.
Browsing a bucket, with per-object preview, download, tagging, copy, share and delete.
Objects preview in place — images, video, audio, PDFs, Markdown, JSON, CSV and text.
Objects preview in place — images, video, audio, PDFs, Markdown, JSON, CSV and text.
A validated bucket-policy editor, object ownership, and Block Public Access.
A validated bucket-policy editor, object ownership, and Block Public Access.
Encryption at rest, Object Lock, and lifecycle rules, per bucket.
Encryption at rest, Object Lock, and lifecycle rules, per bucket.
Minting a short-lived scoped credential, with a policy builder that says what it can do.
Minting a short-lived scoped credential, with a policy builder that says what it can do.
Requests, errors, throughput and latency over time, from Cairn's own stored metrics.
Requests, errors, throughput and latency over time, from Cairn's own stored metrics.
Every object tag in use, and the objects carrying the one you pick.
Every object tag in use, and the objects carrying the one you pick.
An audit log of administrative actions, with actor, bucket and key.
An audit log of administrative actions, with actor, bucket and key.
Replication rules and remote targets for asynchronous cross-host copies.
Replication rules and remote targets for asynchronous cross-host copies.
Performance

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.

OpSizeCairnMinIORatio
PUT4 KiB1696 obj/s1308 obj/sCairn 1.30×
PUT8 MiB115 MiB/s128 MiB/sMinIO 1.12×
GET4 KiB3431 obj/s1815 obj/sCairn 1.89×
GET8 MiB1463 MiB/s801 MiB/sCairn 1.83×
STAT4 KiB3494 obj/s1824 obj/sCairn 1.92×
DELETE4 KiB7442 obj/s5236 obj/sCairn 1.42×
LIST4 KiB21183 obj/s10288 obj/sCairn 2.06×
MIXED1 MiB285 MiB/s271 MiB/sCairn 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%.

Why Cairn

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.
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:kms key 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.
Get started

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 sh

Run 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:latest

Or 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.

docker-compose.yml
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.

Configuration

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.

VariableDefaultWhat it's for
CAIRN_MASTER_KEYa 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_KEYcairnThe root administrator's access key, ensured on every startup.
CAIRN_ROOT_SECRET_KEYcairnadminThe root administrator's secret. Override it before exposing a node.
CAIRN_LISTEN_ADDR0.0.0.0:7373Where the S3 API binds — it also serves /healthz, /readyz, /metrics and the AWS-STS mint.
CAIRN_WEB_ADDR0.0.0.0:7374Where the web console and management API bind. Set to off / none / disabled to run headless.
CAIRN_DATA_DIR/dataRoot of the object (blob) storage. Must share a filesystem with the metadata database.
CAIRN_DB_PATH/data/cairn.dbThe SQLite metadata file — the single source of truth. Back it up together with the data directory.
CAIRN_REGIONus-east-1The region label returned by the location API and checked in the SigV4 credential scope.
CAIRN_TLS_CERT_PATH / _KEY_PATHunset (plaintext)Set both to enable built-in TLS; otherwise run plaintext behind a TLS-terminating proxy.
CAIRN_ENCRYPT_AT_RESToffTransparently encrypt every stored object at rest, even when the client sends no SSE header.
CAIRN_S3_DOMAINunsetBase domain for virtual-host-style addressing (bucket.domain); unset serves path-style only.
CAIRN_PUBLIC_BASE_URLunsetExternal base URL used when generating links behind an ingress or reverse proxy.
CAIRN_META_SYNCHRONOUSfullfull 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_INSECUREoffPermit 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.

Usage

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-1

Then 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 link

Everything 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.

Releases

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.