Self-hosted static site hosting. Upload HTML, get a URL.
Project description
sitedrop
Upload an HTML file. Get a URL. That's it.
Self-hosted static site hosting. Deploy single HTML pages to your own server with one command.
pip install sitedrop
sitedrop login
sitedrop upload portfolio.html
# => Live at https://yourserver.com/portfolio
Features
- One command deploy —
sitedrop upload file.htmland it's live - Clean URLs —
portfolio.htmlbecomes/portfolio - Self-hosted — runs on your own VM, you own your data
- Single package — same
pip installfor both server and client - Simple auth — password login with JWT tokens, managed by the CLI
- Full control — upload, update, delete, and list your sites
- No build step — upload raw HTML, no frameworks or config needed
- Public viewing — anyone can view deployed sites, only you can manage them
Quick Start
Server (on your VM)
pip install sitedrop
sitedrop setup
sitedrop start
Client (on your laptop)
pip install sitedrop
sitedrop login
sitedrop upload portfolio.html
# => Live at https://yourserver.com/portfolio
Same package, different commands.
CLI Reference
Server Commands
Run these on your VM.
sitedrop setup
Interactive setup. Sets the admin password and configures the server bind address.
sitedrop setup
Set admin password: ********
Confirm password: ********
Bind host [127.0.0.1]:
Bind port [8000]:
Config saved to ~/.sitedrop/server.json
Use --dev to skip host/port prompts and default to 127.0.0.1:8000:
sitedrop setup --dev
sitedrop start
Start the server.
sitedrop start
This starts the server as a background process. Use --dev to run in the foreground with auto-reload:
sitedrop start --dev
For direct HTTPS (without a reverse proxy), pass your certificate and key:
sitedrop start --ssl-certfile cert.pem --ssl-keyfile key.pem
SSL paths are saved in server config so sitedrop restart preserves them.
sitedrop stop
Stop the server. Sends SIGTERM for a graceful shutdown, falling back to SIGKILL after 5 seconds.
sitedrop stop
sitedrop restart
Restart the server (stop + start). Supports --dev and SSL options.
sitedrop restart
sitedrop status
Check if the server is running.
sitedrop status
Client Commands
Run these from anywhere.
sitedrop login
Authenticate with your server. Saves credentials locally.
sitedrop login
# Server URL [http://localhost:8000]: https://yourserver.com
# Password: ********
# Logged in to https://yourserver.com
Credentials are saved to ~/.sitedroprc (chmod 600) so you only need to do this once. The CLI automatically refreshes expired tokens using the saved password.
sitedrop upload <file>
Upload an HTML file. The site name is derived from the filename.
sitedrop upload portfolio.html # => /portfolio
sitedrop upload my-blog.html # => /my-blog
sitedrop upload index.html # => /index
Override the name with --name:
sitedrop upload page.html --name about # => /about
If a site with that name already exists, the CLI prompts for confirmation. Use --force to skip the prompt and overwrite:
sitedrop upload page.html --force
sitedrop list
List all deployed sites.
sitedrop list
Name Size Modified
------------------------------------------------------------
portfolio 4200 2026-02-20T10:30:00+00:00
my-blog 12100 2026-02-19T08:15:00+00:00
sitedrop delete <name>
Remove a deployed site. Prompts for confirmation before deleting. Use --yes / -y to skip the prompt.
sitedrop delete portfolio
# Delete portfolio? [y/N]: y
# Deleted portfolio
sitedrop delete portfolio --yes
# Deleted portfolio
sitedrop info
Show the configured server URL and token status (valid, expired, or not logged in).
sitedrop info
sitedrop password
Change the server password. Prompts for your current and new password, then updates saved credentials. All previously issued tokens are invalidated.
sitedrop password
API Reference
The CLI is a wrapper around a REST API. You can also use it directly with curl.
All management endpoints require a JWT token in the Authorization header. Get a token by authenticating with your password.
Authentication
POST /api/auth
Get a JWT token.
curl -X POST https://yourserver.com/api/auth \
-H "Content-Type: application/json" \
-d '{"password": "your-password"}'
{
"token": "eyJhbGciOiJIUzI1NiIs..."
}
Tokens expire after 24 hours.
POST /api/password
Change your password. Requires authentication.
curl -X POST https://yourserver.com/api/password \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"current_password": "current", "new_password": "new-one"}'
GET /api/health
Health check. No authentication required.
curl https://yourserver.com/api/health
{
"status": "ok"
}
Sites
PUT /api/sites/{name}
Upload or update a site. Send the HTML as the request body.
curl -X PUT https://yourserver.com/api/sites/portfolio \
-H "Authorization: Bearer <token>" \
--data-binary @portfolio.html
To prevent accidental overwrites, send If-None-Match: * — the server returns 412 Precondition Failed if the site already exists:
curl -X PUT https://yourserver.com/api/sites/portfolio \
-H "Authorization: Bearer <token>" \
-H "If-None-Match: *" \
--data-binary @portfolio.html
{
"name": "portfolio",
"size": 4200,
"modified": "2026-02-20T10:30:00+00:00"
}
Site names must match ^[a-zA-Z0-9][a-zA-Z0-9_-]*$ and be at most 240 characters. Maximum upload size is 10 MB.
GET /api/sites
List all deployed sites. Requires authentication.
curl https://yourserver.com/api/sites \
-H "Authorization: Bearer <token>"
{
"sites": [
{
"name": "portfolio",
"size": 4200,
"modified": "2026-02-20T10:30:00+00:00"
}
]
}
DELETE /api/sites/{name}
Delete a site. Requires authentication.
curl -X DELETE https://yourserver.com/api/sites/portfolio \
-H "Authorization: Bearer <token>"
Viewing Sites
GET /{name}
View a deployed site. No authentication required.
curl https://yourserver.com/portfolio
Returns the HTML content directly. Non-existent sites return 404 Not Found as plain text.
Served sites include ETag (SHA-256 of content) and Cache-Control: no-cache headers. Browsers cache the content but revalidate on each request — unchanged content returns 304 Not Modified.
Server Prerequisites
- Python 3.10+
- A VM (Google Cloud, AWS, DigitalOcean, etc.)
- A reverse proxy (nginx, caddy, etc.) for HTTPS and public access, or use
--ssl-certfile/--ssl-keyfilefor direct TLS
Configuration
Server
Server configuration is stored in ~/.sitedrop/server.json:
{
"password_hash": "$2b$12$...",
"jwt_secret": "auto-generated-on-first-run",
"host": "127.0.0.1",
"port": 8000,
"sites_dir": "~/.sitedrop/sites",
"ssl_certfile": null,
"ssl_keyfile": null
}
Client
Client configuration is stored in ~/.sitedroprc (chmod 600):
{
"server_url": "https://yourserver.com",
"token": "eyJhbGciOiJIUzI1NiIs...",
"password": "saved-for-auto-refresh"
}
The CLI automatically refreshes expired tokens using the saved password.
Limits
- File size: 10 MB per upload
- Site name: 240 characters max, alphanumeric plus hyphens and underscores, must start with alphanumeric
- File format: Must be valid UTF-8 text
- Auth rate limit: 5 login attempts per minute per IP
API Error Codes
| Code | Meaning |
|---|---|
| 400 | Invalid site name or non-UTF-8 content |
| 401 | Wrong password or expired/invalid token |
| 404 | Site not found |
| 412 | Site already exists (when If-None-Match: * is sent) |
| 413 | File exceeds 10 MB limit |
| 429 | Too many login attempts |
| 500 | Server not configured (run sitedrop setup) |
Security
- Always use HTTPS in production. Either use
--ssl-certfile/--ssl-keyfilefor direct TLS, or put a reverse proxy (nginx, caddy) in front of sitedrop. - Protect
~/.sitedroprc. The client stores your password in plaintext to enable automatic token refresh. The file is created withchmod 600(owner read/write only). Treat it like your SSH keys. - Uploaded HTML is served as-is. Sitedrop does not sanitize HTML content. If you upload a page with malicious scripts, visitors will run them.
- Passwords are hashed with bcrypt on the server. The JWT secret is auto-generated (256-bit) on first setup and rotated on every password change, invalidating all existing tokens.
- Login attempts are rate-limited to 5 per minute per IP to protect against brute-force attacks.
- The server sets
X-Content-Type-Options: nosniffandX-Frame-Options: DENYon all responses. - API responses include a
Content-Security-Policyheader (default-src 'none'; frame-ancestors 'none'). This header is not applied to served sites so their content renders normally.
Environment Variables
| Variable | Default | Description |
|---|---|---|
SITEDROP_CONFIG |
~/.sitedrop/server.json |
Override server config file path |
SITEDROP_CLIENT_CONFIG |
~/.sitedroprc |
Override client config file path |
Useful for Docker deployments or running multiple instances.
How It Works
User CLI Server Visitor
| | | |
|-- sitedrop upload f.html-->| | |
| |-- PUT /api/sites/f -->| |
| | |-- save f.html to disk |
| |<-- 200 OK ------------| |
|<-- Live at /f --------| | |
| | | |
| | |<-- GET /f ------------|
| | |-- serve f.html ------>|
The server is a single FastAPI application that handles both the management API and site serving:
- API routes (
/api/*) require JWT authentication and handle CRUD operations - Site routes (
/{name}) are public and serve stored HTML files directly - HTML files are stored on the filesystem at
~/.sitedrop/sites/{name}.html - Passwords are hashed with bcrypt, JWTs are signed with a server-generated secret
Development
Run the server locally for development:
git clone https://github.com/williamtao345/sitedrop.git
cd sitedrop
python -m venv venv
source venv/bin/activate
pip install -e ".[dev]"
# Set up with a test password
sitedrop setup --dev
# Run the server with auto-reload
sitedrop start --dev
Test the CLI against your local server:
sitedrop login
# Server URL [http://localhost:8000]:
# Password: your-test-password
sitedrop upload test.html
# => Live at http://localhost:8000/test
Run tests:
pytest
Troubleshooting
"Cannot connect to server" — The client can't reach the server. Check that the server is running (sitedrop status) and that the URL in ~/.sitedroprc is correct.
"Authentication failed" — Wrong password or expired token. The CLI auto-refreshes tokens, but if your password changed, run sitedrop login again.
"Too many login attempts" — You've been rate-limited. Wait 60 seconds and try again.
"File too large" — Upload exceeds the 10 MB limit.
"File must be valid UTF-8" — You tried to upload a binary file. Sitedrop only hosts UTF-8 HTML.
"Server failed to start" — Usually means the port is already in use. Check with lsof -i :8000 or change the port with sitedrop setup.
Server appears started but isn't responding — Check sitedrop status. If it says "not running (stale PID file)", the server crashed after starting. Check your reverse proxy logs or run sitedrop start --dev to see errors in the foreground.
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file sitedrop-0.0.5.tar.gz.
File metadata
- Download URL: sitedrop-0.0.5.tar.gz
- Upload date:
- Size: 33.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.5
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
249e3fc2869d598e011658c5df8485252edef8db3526beb6c20968e91ce800ae
|
|
| MD5 |
4b09a978084f0227f47936259e2e97d4
|
|
| BLAKE2b-256 |
28627d4dc177f672a382464422e493ec45691d34dce72c2e5381173a5ecb5753
|
File details
Details for the file sitedrop-0.0.5-py3-none-any.whl.
File metadata
- Download URL: sitedrop-0.0.5-py3-none-any.whl
- Upload date:
- Size: 22.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.5
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
89689c6d2a2a5be2659d103a881885cab8d16373c0e61b75e2ef961e8e7e8342
|
|
| MD5 |
7c064f083c784744788f29f1294b77c4
|
|
| BLAKE2b-256 |
39e7b949792a10693074fb97aeda8c88dc0df959985f5b8242d003b59dd7f157
|