A production-ready, HTTPS-first file-upload server built with Caddy + caddyv2-upload and deployed on Railway.
.
├── Caddyfile # Caddy configuration (upload, auth, headers, health check)
├── Dockerfile # Two-stage build: xcaddy compiles the upload plugin
├── railway.json # Railway build/deploy config (builder, restart, health check)
├── public/
│ └── index.html # Landing page served at /
└── README.md
| Path | Method | Auth | Description |
|---|---|---|---|
/ |
GET | — | Public landing page |
/upload |
GET | Basic Auth | Shows the HTML upload form |
/upload |
POST | Basic Auth | Receives multipart/form-data upload |
/uploads/* |
GET | — | Publicly browse/download uploaded files |
/health |
GET | — | Health probe (returns 200 OK) |
The Dockerfile uses a two-stage build:
caddy:2-builder— runsxcaddyto compile a custom Caddy binary with thecaddyv2-uploadplugin baked in.caddy:2-alpine— swaps in that binary for a minimal final image.
Create a new (private) repository and push these files to it.
- Log into railway.com.
- Click New Project → Deploy from GitHub repository and select your repo.
- Railway detects the
Dockerfileandrailway.jsonautomatically and starts the build.
Navigate to your service → Variables and add:
| Variable | Value |
|---|---|
BASIC_AUTH_PASSWORD_HASH |
bcrypt hash of your chosen password (see below) |
Generate your hash — run one of these locally:
# If you have Caddy installed: caddy hash-password --plaintext "your-secret-password" # Or with Docker (no local install needed): docker run --rm caddy:2-alpine caddy hash-password --plaintext "your-secret-password"Paste the resulting
$2a$…string as the variable value.
⚠️ Railway does not support defining volumes inrailway.json. They must be created through the dashboard.
- In your project canvas, right-click → Add Volume, or use ⌘K → New Volume.
- When prompted, select your Caddy service.
- Set the Mount Path to
/srv/uploads. - Save — Railway will redeploy the service with the volume attached.
Without a volume, uploaded files live only in the container and will be lost on the next deploy.
In your service settings click Generate Domain.
Railway provisions a free *.up.railway.app subdomain with automatic TLS.
Visit https://your-domain.up.railway.app/upload, enter your credentials, pick a file, and click Upload.
# Upload a file
curl -u admin:your-secret-password \
-F "myFile=@/path/to/file.txt" \
https://your-domain.up.railway.app/upload
# List uploaded files
curl https://your-domain.up.railway.app/uploads/
import requests
url = "https://your-domain.up.railway.app/upload"
auth = ("admin", "your-secret-password")
with open("report.pdf", "rb") as f:
resp = requests.post(url, auth=auth, files={"myFile": f})
print(resp.status_code, resp.text)Edit these two values in Caddyfile → upload @post { ... }:
| Directive | Default | Description |
|---|---|---|
max_filesize |
50MB |
Maximum size of a single uploaded file |
max_form_buffer |
50MB |
Maximum in-memory buffer per request |
Remove (or comment out) the handle /uploads/* block in the Caddyfile to
prevent unauthenticated users from browsing or downloading uploaded files.
Add a file_field_name filter or use a Caddy route block with an expression
matcher to reject files whose names don't end with an allowed extension, e.g.:
@bad_ext {
path_regexp ext \.(php|sh|exe|bat)$
}
error @bad_ext "File type not allowed" 403Place that block inside the handle /upload { … } block, before the upload directive.
Replace admin in the basic_auth block with any username you like.
The BASIC_AUTH_PASSWORD_HASH variable holds the hash for that user.
- HTTPS enforced automatically by Caddy + Railway's edge
- Password protected via HTTP Basic Auth with bcrypt
- Password hash stored as an environment variable, never hard-coded
- Security headers (
HSTS,X-Frame-Options,nosniff,XSS-Protection) - Server fingerprinting suppressed (
-Serverheader) - File-size limit enforced by the upload plugin
- Change the default password hash before deploying
- Restrict allowed file extensions if your use-case allows it
- Consider removing
/uploads/*public listing for sensitive uploads