Skip to content

Repository files navigation

BunnyFile 🐰

Files, shared. That's it.

Buy me a coffee

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.

Non-goals (what BunnyFile deliberately is NOT)

  • ❌ 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.

The pitch

  • 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

Install (production)

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

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

Screenshots

Regenerated automatically on every release by .github/workflows/screenshots.yml against seeded demo data.

File browser Preview Share
File browser Preview Share dialog

Security

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. uploadedByUserId is attribution, not an ACL — use folders like alice/ / 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_HOST etc.) 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_SECRET to 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 :3901 directly.
  • Behind Caddy (or any reverse proxy), set TRUST_PROXY=1 so share rate limits use the real client IP from X-Forwarded-For. Optionally set TRUSTED_PROXIES to a comma-separated IPv4/CIDR allowlist of proxy peers (e.g. 10.0.0.0/8); without it, any peer is trusted when TRUST_PROXY is on.
  • Restrict cross-origin access with WEB_ORIGIN when serving from a custom domain.
  • Do not plant symlinks under DATA_DIR that point outside the volume — path checks use resolve(), not realpath().
  • Back up DATA_DIR before upgrading across the v2 layout change (first boot moves user files into DATA_DIR/files/ and renames .trash/.shares/.multipart). See docs/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.

S3-compatible API

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 = true

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

Stack

  • 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)

Development

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:3900

Server 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

Observability

  • 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

Docs

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

Roadmap

See PLAN.md for development phases 0–6.

About

Self-hosted file hosting with an S3-compatible API and upload progress — replaces the "files" half of Nextcloud, nothing more. Bun, minimal by design.

Topics

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Sponsor this project

Packages

Contributors

Languages