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.
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.
Architecture Diagram Flow
- 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.
- 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.
- 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.
- Caddy to Control Plane (:8080): Dashboard and management API traffic addressed to console.localhost is proxied to the Go control-plane on port 8080.
- 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.
- Control Plane to SQLite: The control-plane stores domains, admin accounts, and user sessions in a local SQLite database at /data/control.db.
- 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.
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.
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.
More capabilities
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.
More capabilities
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.
More capabilities
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.
More capabilities
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.
More capabilities
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.
More capabilities
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.
Register domain for a bucket
Submit your domain via the dashboard or POST /api/domains. The domain enters pending state while waiting for DNS.
Create DNS records
Add the two DNS records provided in the registration response:
| Type | Host / Name | Value / Target |
|---|---|---|
| CNAME | <domain> | EDGE_HOSTNAME (apex: A/AAAA) |
| TXT | _ostra.<domain> | ostra-verify=<token> |
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.
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.
POST /api/domains — bucket alias created
Deploy Ostra your way
Choose between the standalone stack with bundled Caddy or deploy behind your existing reverse proxy or PaaS platform.
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
docker compose up -d
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
docker compose -f docker-compose.external-proxy.yml up -d
Coolify
Tested & DocumentedCreate 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 & DocumentedIdeal 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.
Get started in three steps
You need a server with Docker and ports 80/443 reachable from the internet.
Configure DNS records
Point these records at your server (replace example.com with your domain):
| Record | Purpose |
|---|---|
| console.example.com | Dashboard |
| s3.example.com | S3 API |
| edge.example.com | CNAME target for users' custom domains |
| *.web.example.com | Per-bucket web access (optional) |
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.
# 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.comTo pin a specific version, set CONTROL_IMAGE=ghcr.io/byhandi/ostra:<version> in .env. To build from source, run docker compose up -d --build.
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.shServices 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
MANIFESTandSHA256SUMS restore.shvalidates checksums before modifying volumes
./scripts/backup.sh
./scripts/restore.sh backups/ostra-<timestamp>
Predictable Upgrades & Rollbacks
Version pinning & Goose migrationsPin 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_IMAGEin.env
More details
- Goose migrations run automatically on container startup
- Atomic rollback: revert image and run
./scripts/restore.sh
docker compose pull
docker compose up -d
Multi-Arch Releases on GHCR
linux/amd64 & linux/arm64Release images are automatically compiled for multiple CPU architectures and published to the GitHub Container Registry (ghcr.io/byhandi/ostra).
x86_64 servers & cloud VPS
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 & /healthzEvery container includes explicit health checks. Services wait for their dependencies to become healthy before starting, preventing startup race conditions.
- CLI command
/control healthcheckvalidates database connectivity
More details
- HTTP endpoint
/healthzexposes readiness checks - Docker Compose
service_healthyensures startup order
docker compose ps
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 sourceOstra 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 & PRsWant to contribute? Review our guidelines for bug fixes, feature proposals, and development workflow in CONTRIBUTING.md.
Security
Responsible DisclosureSecurity reports are taken seriously. Find vulnerability reporting procedures and contact information in SECURITY.md.