Files, shared. That's it.
Lightweight self-hosted file hosting and sharing, built on Bun. S3-compatible API, upload progress feedback, and a minimal architecture. Replaces the "files" half of Nextcloud — nothing more.
License: AGPL-3.0.
- ❌ Nextcloud clone
- ❌ Sync client (use Syncthing / rclone)
- ❌ WebDAV / SFTP / FTP (use SFTPGo or rclone in front)
- ❌ Calendar / contacts / mail / Talk / collaborative editing
- ❌ Plugin marketplace
- ❌ Enterprise SSO / LDAP
If you need those, Nextcloud and Seafile are great. BunnyFile wins by being less.
- Fast: cold start <500ms, idle RAM <100MB
- Compatible: first-class S3 API — rclone, aws-cli, restic, kopia, Cyberduck all just work — plus an in-app S3 console for buckets and keys
- Minimal: Bun + SQLite + local filesystem. No Redis, no MariaDB, no Elasticsearch
- Reliable: upload progress feedback in the SPA plus byte-exact integrity testing
One container. Elysia serves /api/* and the built SPA on / — no second server.
Pre-built images publish to GHCR on every GitHub Release:
docker run -d --name bunnyfile \
-p 3901:3901 \
-v bunnyfile-data:/data \
-e BETTER_AUTH_SECRET="$(openssl rand -hex 32)" \
ghcr.io/samuelloranger/bunnyfile:latestOpen http://localhost:3901 — the first account you create becomes the admin.
Compose stacks in deploy/compose/:
| File | Use case |
|---|---|
standalone.yml |
Single container + volume |
caddy.yml |
HTTPS reverse proxy (recommended for any internet-facing deploy) |
tinyauth.yml |
Forward-auth layout (see PLAN.md) |
Copy deploy/compose/.env.example → .env and set BETTER_AUTH_SECRET before first boot.
Build from source instead:
docker build -t bunnyfile .
Regenerated automatically on every release by
.github/workflows/screenshots.ymlagainst seeded demo data.
| File browser | Preview | Share |
|---|---|---|
![]() |
![]() |
![]() |
Designed to be safe behind a reverse proxy. What's in the box:
- Shared workspace: every authenticated user can list, download, and delete the whole files tree. Any valid S3 access key reaches every bucket.
uploadedByUserIdis attribution, not an ACL — use folders likealice//bob/as a convention if you want separation. - Auth: native email/password via better-auth (scrypt hashing, cookie sessions, 30-day expiry). First signup becomes admin; admins manage users on
/people. Alternatively run in forward-auth mode behind Tinyauth/Caddy — one mode at a time. - Password reset: self-service email-link flow (1-hour token, all sessions revoked on reset, rate-limited, no email enumeration). Configure SMTP (
SMTP_HOSTetc.) to send mail; without it, reset links are logged to stdout for an admin to relay. - Origin policy: a single trusted-origin allowlist (localhost + RFC1918 LAN + explicit
WEB_ORIGIN/ env entries) backs both the CSRF check and CORS, so they can't disagree. - Share links: optional password, expiry, and max-download count; expired/exhausted links render a 410. Public share access is rate-limited (in-memory token bucket). Passwords are never accepted via query string.
- Data integrity: every file write is write-then-rename with a checksum recorded in SQLite; integration tests verify byte-exact round trips.
Operator checklist:
- Set
BETTER_AUTH_SECRETto a random 32-byte value before first boot — the process refuses to start without it. Changing it later invalidates encrypted S3 access-key secrets stored in SQLite. - Terminate TLS at a reverse proxy (see
deploy/compose/caddy.yml) — don't expose:3901directly. - Behind Caddy (or any reverse proxy), set
TRUST_PROXY=1so share rate limits use the real client IP fromX-Forwarded-For. Optionally setTRUSTED_PROXIESto a comma-separated IPv4/CIDR allowlist of proxy peers (e.g.10.0.0.0/8); without it, any peer is trusted whenTRUST_PROXYis on. - Restrict cross-origin access with
WEB_ORIGINwhen serving from a custom domain. - Do not plant symlinks under
DATA_DIRthat point outside the volume — path checks useresolve(), notrealpath(). - Back up
DATA_DIRbefore upgrading across the v2 layout change (first boot moves user files intoDATA_DIR/files/and renames.trash/.shares/.multipart). Seedocs/s3-compatibility.md.
S3 signing note: mutating requests may use x-amz-content-sha256: UNSIGNED-PAYLOAD (rclone/aws-cli streaming). Payload-hash binding for that case is deferred so known clients keep working; prefer clients that send a real payload hash when practical.
Found a vulnerability? Open a private security advisory on GitHub rather than a public issue.
BunnyFile speaks enough S3 for rclone, aws-cli, restic, and kopia. The API lives at /api/s3 on the same host as the web app.
Quick rclone config:
[bunnyfile]
type = s3
provider = Other
env_auth = false
access_key_id = YOUR_KEY
secret_access_key = YOUR_SECRET
endpoint = http://localhost:3901/api/s3
region = us-east-1
force_path_style = trueCreate per-user keys in the app under S3 → Access keys, or set S3_ACCESS_KEY_ID / S3_SECRET_ACCESS_KEY in the environment for a single global key. Browse and manage buckets in the S3 nav without a CLI.
Full client setup, supported operations, and known limitations: docs/s3-compatibility.md.
- Runtime: Bun ≥ 1.3
- Server: Elysia (serves
/api/*and the built web app on/) - Web: React 19 + Vite + TanStack Router + TanStack Query + Tailwind CSS (SPA — no SSR)
- Storage: Local filesystem, SQLite (
bun:sqlite) for metadata - Tests:
bun:test - Lint/format: Biome
- Typed API client: Elysia Eden (web → server)
bun install
# Run server + web in parallel (web proxies /api to server)
bun run dev
# Or individually:
bun run dev:server # → http://localhost:3901
bun run dev:web # → http://localhost:3900Server exposes GET /api/health. In dev, the web app runs on Vite and proxies /api to the server.
Docker (dev) — Compose assigns random available host ports to avoid collisions:
bun run docker:up # starts containers
bun run docker:ports # prints the assigned URLs
bun run docker:down| Script | What |
|---|---|
bun run dev |
Run server + web in parallel (Bun workspaces filter) |
bun run build |
Build web → apps/web/dist and bundle server |
bun test |
Run bun:test suites across the workspace |
bun run typecheck |
tsc --noEmit in every package |
bun run lint / lint:fix |
Biome |
- OpenAPI / Swagger UI:
/api/docs(REST routes; S3 excluded — use AWS docs) - Prometheus metrics:
GET /metrics - Load smoke test:
bun scripts/load-test.ts http://localhost:3901
| Doc | Contents |
|---|---|
docs/s3-compatibility.md |
S3 client setup, supported ops, limitations |
docs/migrating-from-nextcloud.md |
Files-only migration guide |
docs/plans/password-reset.md |
Self-service password reset — design + status |
See PLAN.md for development phases 0–6.


