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)
Description
Vibe coded alternative to Google Drive for file sharing
Readme 110 KiB
Languages
TypeScript 85.4%
CSS 14.2%
Dockerfile 0.4%