Self-hosted cloud drive on top of your own AWS S3 bucket. iCloud Drive for people who'd rather hold their own keys.
- Continuous two-way sync between local folders and S3
- Client-side AES-256-GCM encryption (your passphrase, your keys)
- Local web UI + CLI; daemon runs under launchd
- macOS only at the moment (Apple Silicon + Intel via universal binary)
brew tap goodylili/tap
brew install dropboygit clone https://github.com/goodylili/dropboy
cd dropboy
make release-build # builds the frontend + embeds + builds the binary
make install # /usr/local/bin/dropboy (or ~/.local/bin)Requires Go 1.25+ and Node.js 20+.
dropboy init # walks you through bucket, region, passphrase, folders
dropboy start # installs and starts the launchd service
dropboy ui --open # opens the local web UI in your browser
dropboy status # tldr: queue depth, last sync, conflictsSee config.example.yaml for the on-disk format. The file lives at
~/.dropboy/config.yaml.
- Files are encrypted before upload with a per-object AES-256-GCM key. The data encryption key is wrapped with a master key derived from your passphrase; only ciphertext, nonces, and the wrapped key leave the machine.
- The loopback HTTP API on
127.0.0.1:7777is protected by a per-session token at~/.dropboy/ui.token(mode0600), CSRF header, host-header rebinding guard, and security headers (CSP, X-Frame-Options, etc.). - Passphrase entries can be cached in the macOS Keychain
(
service=com.dropboy); usedropboy ui→ Forget passphrase to clear. - The
/api/v1/unlockand/api/v1/forget-passphraseendpoints are per-source rate-limited.
| Command | What it does |
|---|---|
dropboy init |
Generate a fresh config interactively |
dropboy add <path> |
Add a folder to sync |
dropboy remove <path> |
Stop syncing a folder |
dropboy list |
Show synced folders |
dropboy start / stop / restart |
Manage the launchd service |
dropboy sync |
Force one reconciliation pass now |
dropboy pause / resume |
Hold / resume background sync |
dropboy status |
Daemon health, queue, last sync time |
dropboy conflicts |
List unresolved conflicts |
dropboy restore <path> |
Pull the latest remote copy of a file |
dropboy logs -f |
Tail ~/Library/Logs/dropboy/dropboy.log |
dropboy doctor |
Diagnose config / AWS / keychain issues |
dropboy uninstall |
Remove the service file |
- Create an S3 bucket.
- Enable bucket versioning (so dropboy can offer point-in-time restore).
- Enable default SSE-S3 or SSE-KMS encryption (defence in depth — dropboy encrypts client-side as well).
- Create an IAM user/role with
s3:GetObject,s3:PutObject,s3:DeleteObject,s3:ListBucket,s3:GetObjectVersion, ands3:ListBucketVersionson the bucket and its objects. - Add credentials via the AWS shared config (
~/.aws/credentials) orAWS_*environment variables.aws_profilein the dropboy config picks which profile to use.
See config.example.yaml. Notable fields:
limits.max_upload_mbpsthrottles upload bandwidth (0 = unlimited).limits.delete_grace_hoursis the tombstone window — a missing local file isn't deleted from S3 until this many hours have passed.poll.remote_seconds/poll.full_scan_minutescontrol polling cadence.ui.portsets the loopback port for the web UI (default 7777).
dropboy statussays locked. Rundropboy uiand enter your passphrase, ordropboy start --foregroundwithDROPBOY_PASSPHRASE=...set.- Conflicts piling up.
dropboy conflictslists them; resolve in the UI (Keep local / Keep remote / Keep both). - Service won't stay running. Tail
~/Library/Logs/dropboy/dropboy.log. Common cause: AWS credentials expired or the profile in~/.dropboy/config.yamlis wrong. - Reset everything.
dropboy uninstall && rm -rf ~/.dropboy— note this clears local state, not the bucket.
make frontend-dev # Next.js dev server on :3000 with API proxy
make build-go # Go binary only (no UI rebake)
make test # go test ./...
make release-build # the same path the release pipeline takesThe frontend lives in frontend/; see frontend/AGENTS.md for Next 16
conventions. The Go daemon serves the embedded UI in production
(backend/internal/server/spa.go).
- Bump the entry in
CHANGELOG.md. git tag v0.1.0 && git push --tags.- The
releaseworkflow runs goreleaser on macos-latest: builds the universal binary, archives it, publishes the GitHub release, and pushes an updated formula togoodylili/homebrew-tap(uses theHOMEBREW_TAP_TOKENrepo secret — a PAT withreposcope on the tap).
The tap repo just needs an empty Formula/ directory on main; goreleaser
creates Formula/dropboy.rb on each release.
Apple Developer ID signing is wired up but commented out in
backend/.goreleaser.yaml — enable it once you have a Developer Program
membership. Unsigned binaries still run after one Gatekeeper override.
MIT — see LICENSE.
