Skip to main content
Deploy Now
Self-hosted S3-compatible object storage

Self-hosted object storage with per-bucket custom domains

Garage stores the data, Caddy sits in front with automatic TLS, and a control-plane manages buckets, access keys, and per-bucket custom domains. If you know Cloudflare R2: same idea, an S3 API plus public buckets on your own domains, running on your own server.

System Architecture

How Ostra Works

Incoming traffic enters through Caddy, which routes S3 API requests to Garage, website endpoints to public buckets, and dashboard traffic to the control-plane.

Internet
S3 clients • custom domains • console visitors
↓
Caddy:80 / :443Automatic TLS
Edge reverse proxy & on-demand TLS per domain
↓
Garage Storage EngineRust • single-node
S3 API:3900
s3.localhost (data store & retrieve)
Web Endpoint:3902
Public custom domains & website buckets
Admin API:3903
Internal bucket aliases & website toggles
↓
Control PlaneGo + React Dashboard
Dashboard + /api:8080
console.localhost (buckets & access keys)
Internal /caddy/ask:9000
Caddy TLS authorization endpoint
SQLite Database/data/control.db
Domains, admin accounts, sessions

Architecture Diagram Flow

  1. Internet to Caddy: Client requests (S3 API clients, public bucket visitors, custom domain visitors, and administrators) enter the server through Caddy on ports 80 and 443 with automatic TLS certificate provisioning.
  2. Caddy to Garage S3 API (:3900): S3 API requests addressed to s3.localhost are proxied directly to Garage on port 3900 for object storage and retrieval.
  3. Caddy to Garage Web Endpoint (:3902): Custom domain and public bucket requests are routed to Garage on port 3902 to serve website content over HTTPS.
  4. Caddy to Control Plane (:8080): Dashboard and management API traffic addressed to console.localhost is proxied to the Go control-plane on port 8080.
  5. Caddy to Internal /caddy/ask (:9000): When Caddy receives a request for an unknown custom domain, it queries the control-plane on internal port 9000 to verify domain ownership before issuing a Let's Encrypt certificate.
  6. Control Plane to SQLite: The control-plane stores domains, admin accounts, and user sessions in a local SQLite database at /data/control.db.
  7. Control Plane to Garage Admin API (:3903): The control-plane configures Garage via its internal admin API on port 3903 to attach or detach custom domain bucket aliases and enable website access.
Platform Features

Everything you need from an S3 platform

S3-compatible storage backed by Garage v2, on-demand HTTPS with Caddy, and complete bucket management through the control-plane dashboard.

Core Engine

S3 Storage Backed by Garage v2

Standard S3 API compatibility powered by Garage v2 running single-node. Low memory and CPU overhead, zero vendor lock-in, and full support for AWS CLI, rclone, boto3, and Terraform.

Single-node (replication = 1)
More capabilities
Auto layout configurationPath-style S3 endpoint
Edge Routing

Custom Domains with On-Demand TLS

Map custom domain hostnames directly to any bucket. Caddy issues Let's Encrypt certificates on the first request once DNS ownership is verified.

Automatic Let's Encrypt TLS
More capabilities
CNAME & TXT DNS verification
Web Console

Interactive Web Dashboard

Full-featured Files browser with drag-and-drop uploads, streaming progress, in-browser previews for images, video, and PDFs, and inline metadata editing.

In-browser file preview
More capabilities
Rename, move & copy
Bucket Controls

Quotas, CORS & Lifecycle Automation

Configure per-bucket storage size and object count limits with visual progress bars. Manage browser CORS rules for direct client uploads and automate object expiration or cleanup.

Storage size & object quotas
More capabilities
Cross-origin CORS editorLifecycle expiration rules
Security

Granular Access Keys & Expiration

Generate S3 credentials scoped to specific buckets with read, write, or owner permissions. Set key expiration dates with active, expiring soon, and expired visual badges.

Per-bucket read/write/owner
More capabilities
Expiring access keys
Operations

Security by Default & Operations

Argon2id passwords, rotating JWT cookies, login rate limiting, strict CSP, and non-root scratch image. Production Docker Compose, health checks, and cold backup/restore scripts.

Argon2id & rotating JWTs
More capabilities
Backup & restore scriptsMulti-arch images (GHCR)
Custom Domain Flow

Serve buckets on your own custom domain

Deliver public assets and static websites with automatic HTTPS. Configure two DNS records, and Ostra handles ownership verification, Garage bucket aliasing, and TLS certificate provisioning.

1

Register domain for a bucket

Submit your domain via the dashboard or POST /api/domains. The domain enters pending state while waiting for DNS.

2

Create DNS records

Add the two DNS records provided in the registration response:

TypeHost / NameValue / Target
CNAME<domain>EDGE_HOSTNAME (apex: A/AAAA)
TXT_ostra.<domain>ostra-verify=<token>
3

Automated verification & alias

The worker checks DNS automatically in the background (or click "Verify now"). Once verified, the control-plane enables website access on the bucket and attaches the domain alias in Garage; status becomes active.

4

On-demand HTTPS with Caddy

On the first HTTPS request, Caddy queries /caddy/ask to verify domain approval, then obtains and caches a Let's Encrypt certificate automatically.

Domain Status Pipeline
cdn.brandku.example
1. Domain Registered

POST /api/domains — bucket alias created

Just now
Waiting for DNS recordspending
Deployment Options

Deploy Ostra your way

Choose between the standalone stack with bundled Caddy or deploy behind your existing reverse proxy or PaaS platform.

docker-compose.yml

Bundled Caddy Stack

Standalone production deployment where ports 80 and 443 are directly accessible on the host. Caddy terminates TLS and requests Let's Encrypt certificates on demand with zero manual proxy configuration.

  • Dedicated VPS, bare-metal server, or cloud instance
  • Automatic on-demand Let's Encrypt TLS certificates
More details
  • Automatic single-node Garage layout initialization
  • Production-safe defaults and internal ports protected
# Deploy standalone stack

docker compose up -d

docker-compose.external-proxy.yml

Behind an External Proxy

For servers where ports 80/443 and TLS are already managed by Coolify's Traefik, a Cloudflare Tunnel, or Nginx Proxy Manager. Routes hostnames over the Docker network without bundled Caddy.

  • Route console.example.com → control:8080
  • Route s3.example.com → garage:3900
More details
  • Catch-all for custom domains → garage:3902
  • Tested with Coolify and Cloudflare Tunnel
# Deploy with external proxy compose

docker compose -f docker-compose.external-proxy.yml up -d

Coolify

Tested & Documented

Create a Docker Compose resource in Coolify pointing to /docker-compose.external-proxy.yml. Traefik labels handle routing automatically. Configure BASE_DOMAIN, secrets, and admin credentials in the environment variables.

Cloudflare Tunnel

Tested & Documented

Ideal for homelabs or servers behind CGNAT without open ports 80/443. Routes external traffic securely through an outbound tunnel to the local proxy and services without exposing your server IP address directly.

Cloudflare Proxy Considerations

• 100 MB request body limit: Free and Pro plans cap request bodies at 100 MB. Set MAX_UPLOAD_SIZE_MB=95 so single-request browser uploads reject with a clear error; S3 multipart clients (AWS CLI, boto3) are unaffected.

• Custom domains: Routing custom domains through a Cloudflare Tunnel requires Cloudflare for SaaS (Custom Hostnames) with edge.<domain> set as the fallback origin.

Production Deployment

Get started in three steps

You need a server with Docker and ports 80/443 reachable from the internet.

1

Configure DNS records

Point these records at your server (replace example.com with your domain):

RecordPurpose
console.example.comDashboard
s3.example.comS3 API
edge.example.comCNAME target for users' custom domains
*.web.example.comPer-bucket web access (optional)
2

Configure, launch & create admin

Generate secrets with ./scripts/gen-env.sh, pull release images from GHCR, and launch the stack. The Garage single-node layout is created automatically.

bash
# 1. Generate secrets and start the stack
# create .env and fill in every secret
# edit .env: set BASE_DOMAIN=example.com and ACME_EMAIL
# release image from ghcr.io/byhandi/ostra
[+] Running 3/3: garage, control, caddy healthy
# 2. Create the first admin account
✔ Created admin account. Open https://console.example.com

To pin a specific version, set CONTROL_IMAGE=ghcr.io/byhandi/ostra:<version> in .env. To build from source, run docker compose up -d --build.

Day-Two Operations

Built for production reliability

Cold backups with checksum verification, atomic rollbacks, multi-arch releases, and integrated health monitoring.

Consistent Cold Backups

scripts/backup.sh & restore.sh

Services stop briefly while copying so Garage metadata, data blocks, and the SQLite control database stay completely consistent. Services restart automatically even if a backup fails.

  • Archives garage-meta, garage-data, and TLS certs
More details
  • Generates MANIFEST and SHA256SUMS
  • restore.sh validates checksums before modifying volumes
# Create cold backup & restore

./scripts/backup.sh

./scripts/restore.sh backups/ostra-<timestamp>

Predictable Upgrades & Rollbacks

Version pinning & Goose migrations

Pin target release images in .env. Database migrations run automatically when the control-plane starts, and rollback is guaranteed via pre-upgrade cold backups.

  • Pin release with CONTROL_IMAGE in .env
More details
  • Goose migrations run automatically on container startup
  • Atomic rollback: revert image and run ./scripts/restore.sh
# Upgrade image tag

docker compose pull

docker compose up -d

Multi-Arch Releases on GHCR

linux/amd64 & linux/arm64

Release images are automatically compiled for multiple CPU architectures and published to the GitHub Container Registry (ghcr.io/byhandi/ostra).

linux/amd64

x86_64 servers & cloud VPS

linux/arm64

Apple Silicon, Graviton, Pi 4/5

Minimal attack surface: control-plane runs as a non-root binary in a stripped scratch image without shell or extra utilities.

Service Health Checks

/control healthcheck & /healthz

Every container includes explicit health checks. Services wait for their dependencies to become healthy before starting, preventing startup race conditions.

  • CLI command /control healthcheck validates database connectivity
More details
  • HTTP endpoint /healthz exposes readiness checks
  • Docker Compose service_healthy ensures startup order
# Verify stack health status

docker compose ps

Open Source

Free, open source, and self-hosted

Ostra is built in the open under the Apache-2.0 license. Retain full sovereignty over your storage infrastructure and data without vendor lock-in.

Apache License, Version 2.0

Permissive open source

Ostra is licensed under the Apache License, Version 2.0. Garage is licensed separately under AGPL-3.0 and is used by Ostra as an independent service through its API; Ostra's code is not bound by Garage's license.

Because Garage runs as an independent daemon communicating strictly over network protocols (S3 API on port 3900, Web endpoint on 3902, and Admin API on 3903), Ostra does not bundle or link against AGPL code. You can run, deploy, and integrate Ostra according to standard Apache-2.0 terms.

Contributing

Community & PRs

Want to contribute? Review our guidelines for bug fixes, feature proposals, and development workflow in CONTRIBUTING.md.

Security

Responsible Disclosure

Security reports are taken seriously. Find vulnerability reporting procedures and contact information in SECURITY.md.