A small upload endpoint I run on my home server so ShareX can drop files into a plain folder and get a link back.
The reason it exists is that I was tired of S3 keeping all my uploads as opaque blobs when all I wanted was the real file sitting in a directory I can open and serve myself. ShareX could do this over SFTP, but I'd rather not expose that on a box at home, and a plain HTTP endpoint feels cleaner anyway. So this is the piece in between. It checks a token, writes the upload to disk under a random name, and returns the URL.
It picked up a few things along the way like token auth so I can hand out separate keys, quotas so one key can't eat the whole disk, a built-in file browser with instant search, and web dashboard logins for both admins and upload users to check their usage or browse their files.
| Admin Dashboard | File Browser & Search |
|---|---|
![]() |
![]() |
| Token Creation & Management | User Profile & Quotas |
![]() |
![]() |
There's a published image at ghcr.io/skidoodle/uploadserver. Grab
compose.yaml from the repo, set the environment how you want, and bring it up:
docker compose up -dUploads go to the data volume, tokens to the state volume.
Releases has prebuilt Linux/amd64 tarballs. Download the latest one, extract, run.
git clone https://github.com/skidoodle/uploadserver
cd uploadserver
go build -o uploadserver .The first run prints a root token once. Save it, that's your admin login:
$ uploadserver run
uploadserver: (default root token: Xq3...9fZ - saved to ./state/tokens.db)
uploadserver: listening on :8080, storing uploads in ./dataMake an upload token for ShareX from the dashboard: open the server's URL and log in with the root token. Or make one from the CLI:
$ uploadserver add --label laptop --role upload
created upload token a1b2c3d4
secret (shown once): <your-upload-token>Then import uploadserver.sxcu into ShareX, set the URL to your domain, and
drop the token into the Authorization header. A plain curl does the same job:
$ curl -H "Authorization: Bearer <token>" -F "file=@cat.png" https://u.example.com/
https://u.example.com/a1b2c3d4.pngThe link is BASE_URL plus the random filename. uploadserver writes the files
but doesn't serve them, so point your web server at the upload folder (nginx,
caddy, whatever) and set BASE_URL to wherever that lands.
$ uploadserver
usage: uploadserver <command> [<args>]
Commands:
run Start the web server
list List all tokens, with usage and quotas
info <id> Show detailed status and usage for a single token
add [--label L] [--role R] Create a new token
rm <id> Delete a token
disable <id> Disable a token
enable <id> Enable a token
limit <id> [flags] Set upload quotas for a token (use --help for flags)
global [flags] Show or set the server-wide default quota
scan [--token ID] Find untracked files on disk and optionally import them
migrate --token <id> Move flat uploads into per-user directories
prune [--days N] [--dry-run] Purge temporary upload files older than N days
export [--out file.json] Export token store metadata to JSON
import [--in file.json] Import token store metadata from JSON
dump Decode the binary store and print everything in it
reset Delete all tokens and reset store
version Show version and runtime info
limit and global take the quota flags:
--total-size 5GB Lifetime size cap (B/KB/MB/GB/TB; 0 clears it)
--total-uploads 1000 Lifetime upload-count cap
--monthly-size 5GB Size cap per calendar month
--monthly-uploads 500 Upload-count cap per calendar month
limit also has --bypass to exempt a token from all quotas and --clear to
wipe its caps. Only the flags you pass get changed.
You can run CLI commands straight against the running container:
docker compose exec uploadserver scanIf the container is stopped, docker compose run works just as well:
docker compose run --rm uploadserver scanIf you already have files in the upload directory from a previous setup or
manual copy, scan finds them and optionally imports them into a token's
history so quotas and the admin dashboard reflect the real state of the disk.
$ uploadserver scan
found 3 untracked file(s) in ./data:
FILE SIZE MODIFIED
a1b2c3d4e5f6.png 1.2 MB 2025-03-14 09:22
deadbeef1234.jpg 340 KB 2025-03-15 11:05
cafebabe5678.mp4 48.7 MB 2025-04-01 16:30
to import these files, re-run with --token <id>To adopt them into a token:
$ uploadserver scan --token a1b2c3d4
imported 3 file(s) into token a1b2c3d4Everything is set through environment variables:
| Variable | Description | Default |
|---|---|---|
LISTEN_ADDR |
Address to listen on | :8080 |
UPLOAD_DIR |
Directory uploads are written to | ./data |
BASE_URL |
Public URL prefix for returned links | the request's host |
ALLOWED_HOSTS |
Comma-separated list of allowed Host headers for returned URLs when BASE_URL is empty |
empty (accepts request Host) |
UPLOAD_FIELD |
Multipart form field name | file |
TOKEN_STORE |
Path to the bbolt token database | ./state/tokens.db |
MAX_UPLOAD_BYTES |
Max upload size in bytes | 1073741824 (1 GiB) |
RANDOM_NAME_LENGTH |
Length of the random filename in hex chars | 32 |
STRIP_EXTENSION |
Drop the file extension from returned URLs | false |
ENABLE_ADMIN |
Mount the admin dashboard and API | true |
SERVE_FILES |
Serve uploaded files | false |
TRUST_PROXY_HEADERS |
Trust X-Forwarded-Proto and X-Forwarded-For from your reverse proxy |
false |
REQUEST_TIMEOUT |
Deadline for non-upload HTTP responses (Go duration) | 30s |
PURGE_GRACE_PERIOD |
Safety delay / grace period before scheduled media purges execute (24h, 7d, or 0s to disable) |
24h |
CONTROL_SOCKET |
Control socket path (or off to disable) |
./state/control.sock |
Leave TRUST_PROXY_HEADERS=false when clients can connect directly. Enable it only
when a trusted reverse proxy overwrites forwarded headers. Streamed uploads are
excluded from REQUEST_TIMEOUT; MAX_UPLOAD_BYTES and quota reservations bound
their request bodies instead.



