A self-hosted file server for stable, long-lived links. Permanent uploads have no app-level expiration.
Theseus treats URLs as the stable interface; the rest of the deployment is replaceable. Theseus is stateless - all data lives on a swappable storage backend via rclone, in a plain directory layout that can be inspected and migrated with basic tools. The server can be rebuilt, rewritten, or moved to a different host while preserving existing links.
Files are served verbatim and inline by design: image URLs return images, PDF URLs return PDFs, and so on. There are no download pages or intermediate redirects. Documents, scripts, and other tools can reference the same URLs directly.
This rules out end-to-end encryption - the server must read files to serve them. If your storage provider is untrusted, use rclone crypt as a backend wrapper for transparent server-side encryption at rest.
There are two storage tiers: permanent uploads (/p/) are a personal
write-once archive intended to never be deleted, while temporary uploads
(/t/) auto-expire after a configurable TTL.
- CLI and browser uploads -
curl -T file.txt https://files.example.comor use the web form - Rclone storage backends - S3, B2, SFTP, local disk, or anything else rclone supports
- Permanent and temporary uploads -
/p/for your archive,/t/for ephemeral sharing - Archive extraction - upload
.tar.gz,.tar.bz2,.tar.zst, or.tarand extract server-side - Private links - each upload gets a 128-bit random key; no directory listing, no enumeration; only those with the link can access a file
- Directory browsing - navigate extracted archives and multi-file uploads in the browser
- Stateless server - all state lives in the storage backend; plain files, no database
- Security controls - Argon2id auth, sandboxed file I/O, tar bomb protection, CSP headers
- Admin dashboard - view and delete uploads through a web interface
theseus.json (see Configuration for all options):
{
"admin_password": "$argon2id$v=19$m=65536,t=3,p=4$SALT$HASH",
"rclone_remote": "myremote:",
"base_url": "https://files.example.com",
"upload_key": "$argon2id$v=19$m=65536,t=3,p=4$SALT$HASH"
}Generate Argon2id hashes with:
echo -n 'your-password' | argon2 "$(openssl rand -base64 16)" -id -erclone.conf - configure your storage backend with rclone config.
# compose.yml
services:
theseus:
image: ghcr.io/zoickx/theseus:latest
expose:
- 7714
devices:
- /dev/fuse
cap_add:
- SYS_ADMIN
secrets:
- theseus.json
- rclone.conf
secrets:
theseus.json:
file: ./theseus.json
rclone.conf:
file: ./rclone.confpodman compose up -dThe container runs as a non-root user. SYS_ADMIN is required for FUSE
mounts.
Theseus does not handle TLS or rate limiting - put it behind a reverse proxy (Caddy, nginx, etc.) in production.
The container starts rclone (mounting your remote to /mnt/storage), a cron
daemon for TTL cleanup, and the HTTP server - all supervised by the entrypoint
script under tini.
CLI (curl):
# Temporary upload (auto-expires)
curl -T file.txt https://files.example.com
# With authentication
curl -T file.txt -H "Authorization: Bearer YOUR_KEY" https://files.example.com
# Upload and extract an archive
curl -T site.tar.gz https://files.example.com?extract=true
# Permanent upload (if enabled)
curl -T file.txt https://files.example.com/p/Browser: visit the root URL to use the web upload form.
Files are served at their upload path:
https://files.example.com/t/{key}/filename.txt # temporary
https://files.example.com/p/{key}/filename.txt # permanent
Extracted archives serve their full directory tree. Append ?download=true to
force a download instead of inline display.
Navigate to /admin/ and log in with the admin password to view all uploads
and delete individual files or entire upload keys.
| Method | Path | Description |
|---|---|---|
PUT / |
Upload to temp storage | |
PUT /t/ |
Upload to temp storage (explicit) | |
PUT /p/ |
Upload to permanent storage | |
POST / |
Browser multipart upload | |
GET /t/{key}/... |
Download from temp | |
GET /p/{key}/... |
Download from permanent | |
GET /admin/ |
Admin dashboard |
Query parameters: ?filename=<name> (override filename), ?extract=true
(extract archive), ?download=true (force download).
Authentication: Authorization: Bearer <upload_key> header, or via the
browser form. Admin uses session cookies.
Configuration is loaded from /run/secrets/theseus.json (override with CONFIG_FILE env var).
| Option | Type | Description |
|---|---|---|
admin_password |
string | Argon2id hash in PHC format for admin dashboard login |
rclone_remote |
string | Rclone remote name as configured in rclone.conf (include trailing colon) |
| Option | Type | Default | Description |
|---|---|---|---|
upload_key |
string | Argon2id hash in PHC format for upload authentication (empty = anyone can upload). Plaintext key is used as Bearer token with curl or entered via the web upload form. |
|
allow_permanent |
bool | false |
Enable /p/ endpoint for permanent uploads |
max_upload_size |
string | "10G" |
Maximum upload size (e.g., "500M", "2GB") |
temp_ttl |
string | "672h" |
Time-to-live for temporary uploads. Go duration format (h/m/s only). |
base_url |
string | Public URL for generating links in responses (no trailing slash) | |
vfs_cache_max_size |
string | Max size for rclone VFS cache (e.g., "10G") |
| Option | Type | Default | Description |
|---|---|---|---|
listen_addr |
string | ":7714" |
Address and port for the HTTP server |
max_compression_ratio |
int | 20 |
Max extracted size as multiple of max_upload_size. Tar bomb protection. |
cleanup_schedule |
string | "0 4 * * *" |
Cron schedule for TTL cleanup (daily at 4am) |
graceful_shutdown_timeout |
int | 30 |
Seconds to wait for graceful shutdown |
{
"admin_password": "$argon2id$v=19$m=65536,t=3,p=4$SALT$HASH",
"rclone_remote": "myremote:",
"base_url": "https://files.example.com",
"allow_permanent": false,
"listen_addr": ":7714",
"max_upload_size": "10G",
"temp_ttl": "672h",
"cleanup_schedule": "0 4 * * *",
"graceful_shutdown_timeout": 30,
"upload_key": "$argon2id$v=19$m=65536,t=3,p=4$SALT$HASH",
"vfs_cache_max_size": "10G"
}/mnt/storage/
├── t/ # temporary uploads
│ └── {22-char-base62-key}/
│ ├── filename.ext
│ └── filename.ext.meta.json
└── p/ # permanent uploads
└── {22-char-base62-key}/
├── filename.ext
└── filename.ext.meta.json
Each upload gets a unique 22-character base62 key (128 bits of randomness). Metadata sidecars store SHA256 checksum, content type, and upload timestamp.
- Authentication - Argon2id password hashing for both upload keys and admin login
- Sessions - HMAC-SHA256 signed cookies with IP binding
- File I/O - sandboxed with
os.Rootto prevent path traversal - Archive safety - rejects symlinks, hard links, device files; enforces compression ratio limits
- Content serving -
Content-Security-Policy: sandboxon all served files - Cookies - HttpOnly, Secure, SameSite=Strict
- MIME validation - permanent uploads require detected MIME type to match file extension