Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Nanodrop
A minimal self-hosted file sharing platform. Upload a file, get a shareable link. No frills.
- Server-rendered HTML, no client-side framework
- SQLite database, files stored on disk
- JWT authentication via httpOnly cookies
- REST API alongside the web UI
Stack
- Runtime: Node.js 22
- Framework: Fastify
- Database: SQLite (better-sqlite3)
- Language: TypeScript (ESM, run directly via tsx)
Quick start (local)
npm install
JWT_SECRET=changeme npm run dev
Create a user first:
npm run register-user -- --username alice --password secret
Then open http://localhost:3000.
Docker
Build and run
docker compose up -d
Requires a .env file (or environment variables) with at minimum:
JWT_SECRET=your-secret-here
All data (database + uploads) is stored in the nanodrop-data Docker volume.
Register a user in Docker
docker compose run --rm register-user --username alice --password secret
Environment variables
| Variable | Default | Description |
|---|---|---|
JWT_SECRET |
(required) | Secret key for signing JWTs |
JWT_EXPIRY |
7d |
JWT token lifetime |
PORT |
3000 |
Port to listen on |
HOST |
0.0.0.0 |
Host to bind |
BASE_URL |
http://localhost:3000 |
Public base URL (used in share links) |
DB_PATH |
./data/nanodrop.db |
SQLite database path |
UPLOAD_DIR |
./data/uploads |
Upload storage directory |
LOG_FILE |
./data/nanodrop.log |
Auth and access log path |
MAX_FILE_SIZE |
104857600 |
Max upload size in bytes (100 MB) |
COOKIE_SECURE |
false |
Set true when serving over HTTPS |
TRUST_PROXY |
false |
Set true when behind a reverse proxy |
Reverse proxy (nginx example)
When running behind nginx, set TRUST_PROXY=true so Nanodrop sees the real client IP in logs.
server {
server_name files.example.com;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
client_max_body_size 110M;
}
}
Fail2ban
Nanodrop logs failed login attempts to LOG_FILE (default ./data/nanodrop.log) in this format:
[2026-03-03T12:00:00.000Z] AUTH_FAILURE ip=203.0.113.42 user-agent="curl/8.0" username="admin"
1. Create a filter
/etc/fail2ban/filter.d/nanodrop.conf:
[Definition]
failregex = ^.* AUTH_FAILURE ip=<HOST> .*$
ignoreregex =
2. Create a jail
/etc/fail2ban/jail.d/nanodrop.conf:
[nanodrop]
enabled = true
filter = nanodrop
logpath = /path/to/nanodrop.log
maxretry = 5
findtime = 60
bantime = 600
Adjust logpath to wherever your LOG_FILE is. With Docker, the log file lives inside the nanodrop-data volume — mount it to a host path or bind-mount a host directory instead of the named volume to make it accessible to fail2ban:
# docker-compose.yml override
volumes:
- /var/lib/nanodrop:/app/data
Then set logpath = /var/lib/nanodrop/nanodrop.log.
3. Reload fail2ban
sudo fail2ban-client reload
sudo fail2ban-client status nanodrop
Development
npm run dev # start with hot reload via tsx
npm test # run all tests (vitest)
npm run build # type check (tsc --noEmit)