A self-hosted alternative to Vercel Drop and Cloudflare Pages direct uploads. Drop a folder with a built static site, a zip of one, or a single HTML file, and get a shareable address, with no build step.
Try it at redoost.whyredfire.dev
- Drop a folder, get a URL. Drop a folder, a zip, or a single HTML file
anywhere on the page. Each site is published at
https://<slug>.<sites domain>, for examplebrave-otter-1a2b. - Uploads go straight to storage. The browser uploads each file to S3 (Garage) with its own signed policy, bound to the file's path, size, content type, and SHA-256 checksum. The API never handles file contents.
- Fast and resumable. Up to 32 files upload in parallel with retries. If an upload is interrupted, selecting the same folder again resumes it.
- Single-page apps just work. Sites without a top-level
404.htmlserveindex.htmlfor unknown pages; sites with one get their own 404 page. - Sign in with Google. People sign in through an OIDC provider, and their sites stay online for as long as they keep them.
- Manage your sites. Each site has its own page, where you can update it with a new version, take it offline, or delete it.
- A command line too.
redoost loginsigns in through a link you can open on any device, thenredoost publish ./distpublishes a folder. Seecli/README.md. - Works with AI agents.
npx skills add whyredfire/redoostinstalls the redoost skill, so agents like Claude Code, Codex, and Cursor can publish and manage sites for you. - Sensible limits. 20 MiB per file, 150 MiB and 2000 files per site, and 1 GiB per user, all configurable. The dashboard checks the per-site limits as soon as files are selected.
flowchart LR
browser[Browser] -->|manifest, complete| api[API]
browser -->|signed uploads| garage[(Garage)]
visitor[Visitor] --> sites[Sites Nginx]
sites -->|is the site ready?| api
sites -->|files| garage
api --- db[(SQLite or Postgres)]
- The dashboard hashes the folder's files and sends a manifest to the API, which returns one signed upload policy per file.
- The browser uploads the files directly to the bucket; Garage checks each checksum.
- The API confirms every file arrived and marks the site ready.
- Nginx serves
<slug>.<sites domain>from Garage's website endpoint, after checking with the API (cached briefly) that the site exists.
Built with FastAPI, SQLModel, and Alembic on SQLite or Postgres; React, TanStack Router, Tailwind, and shadcn/ui, built with Vite and Bun; Garage for storage; and Nginx for serving sites.
The Helm chart in chart/ runs the API, the frontend, and the sites
Nginx, routed with Gateway API HTTPRoutes. Garage isn't part of the chart; it
expects:
- a bucket and a key with owner permission on it (the setup Job enables the bucket's website endpoint and CORS),
- Garage's website endpoint (
[s3_web], port 3902) fors3.websiteUpstream, - a public route to Garage's S3 API for
s3.publicEndpoint, since browsers upload to it directly, - a Gateway listener and wildcard certificate covering
*.<sites host>.
# values.yaml
appOrigin: https://redoost.example.com
sitesOrigin: https://sites.example.com
httpRoute:
app:
parentRefs:
- name: gateway
namespace: gateway
sectionName: https
sites:
parentRefs:
- name: gateway
namespace: gateway
sectionName: https-sites
s3:
endpoint: http://garage.garage.svc.cluster.local:3900
publicEndpoint: https://s3.example.com
websiteUpstream: garage.garage.svc.cluster.local:3902
existingSecret: redoost-s3helm install redoost ./chart -f values.yamlPushes that change the backend or frontend build both images for amd64 and
arm64 and publish them to ghcr.io/whyredfire/redoost-api and
redoost-frontend, tagged dev and with the last release and commit, e.g.
0.1.6-abc1234. Running the release workflow builds both images again tagged
with the version and latest, then commits that tag to chart/values.yaml, so
anything deploying the chart from main rolls out on releases only. Each image
reports the version it was built with, in the dashboard footer and from
/health.
See chart/values.yaml for all settings. By default the
API uses SQLite on a volume, so it runs as a single replica with the Recreate
strategy, and the hourly cleanup CronJob runs on the same node. Setting
database.url (or database.existingSecret) switches to Postgres, with no
volume and any number of replicas.
Everything runs in Docker Compose:
cp .env.example .env
docker compose up --build --wait
docker compose watchdocker compose watch syncs source changes into the containers, where the API
and frontend reload in place; lockfile changes rebuild the image. The
Dockerfile.dev images are for development only; backend/Dockerfile and
frontend/Dockerfile build the production images.
Locally, REDOOST_DEV_SIGN_IN=true from .env.example replaces the OIDC
provider: the dashboard shows "Sign in as dev user", which signs you in as
dev@localhost without any setup. The API refuses to start with both dev
sign-in and an OIDC issuer set, and the chart never enables it.
Only two ports are published. The gateway service stands in for the cluster's
ingress and routes by host:
| URL | Routes to |
|---|---|
| http://localhost:8081 | Dashboard, with /api/ going to the API |
http://<slug>.sites.localhost:8081 |
Published sites |
http://s3.localhost:8081 |
Garage's S3 API, for browser uploads |
| http://localhost:8080 | Garage UI (log in with GARAGE_ADMIN_TOKEN) |
On startup, the one-off init service enables the bucket's website endpoint
and CORS, and migrate applies database migrations. See
backend/README.md and
frontend/README.md for the API, migrations, and checks.
The API reads REDOOST_* environment variables; the chart sets them from its
values.
| Variable | Default | Description |
|---|---|---|
REDOOST_APP_ORIGIN |
Dashboard origin, allowed to upload to the bucket | |
REDOOST_LOG_LEVEL |
INFO or VERBOSE |
|
REDOOST_DATABASE_URL |
sqlite+aiosqlite:///./redoost.db |
Async SQLAlchemy database URL, sqlite+aiosqlite:// or postgresql+asyncpg:// |
REDOOST_S3_ENDPOINT |
S3 endpoint used by the API | |
REDOOST_S3_PUBLIC_ENDPOINT |
S3 endpoint used by browsers for uploads | |
REDOOST_S3_REGION |
Region used for signing | |
REDOOST_S3_BUCKET |
Bucket that stores the sites | |
REDOOST_S3_ACCESS_KEY_ID |
S3 access key ID | |
REDOOST_S3_SECRET_ACCESS_KEY |
S3 secret access key | |
REDOOST_MAX_FILE_SIZE |
20MiB |
Largest allowed file |
REDOOST_MAX_DEPLOYMENT_SIZE |
150MiB |
Largest allowed site |
REDOOST_MAX_DEPLOYMENT_FILES |
2000 |
Most files allowed in a site |
REDOOST_MAX_ACCOUNT_SIZE |
1GiB |
Largest total size of a user's sites, counting uploads in progress |
REDOOST_UPLOAD_WINDOW |
PT1H |
How long upload policies stay valid (at most 24 hours) |
REDOOST_JWT_SECRET |
Signs the API's tokens, e.g. from openssl rand -hex 32 |
|
REDOOST_OIDC_ISSUER |
OIDC issuer users sign in with | |
REDOOST_OIDC_PROVIDER |
google |
Who the issuer signs users in with, for the sign-in button |
REDOOST_OIDC_CLIENT_ID |
OIDC client ID, with <app origin>/auth/callback as its redirect URI |
|
REDOOST_OIDC_CLIENT_SECRET |
OIDC client secret | |
REDOOST_DEV_SIGN_IN |
false |
Sign in as a local dev user instead of with an OIDC provider; for development only |
In Compose, the sites Nginx also reads REDOOST_SITES_DOMAIN,
REDOOST_API_UPSTREAM, and REDOOST_S3_WEBSITE_UPSTREAM, and the dev server
reads REDOOST_SITES_ORIGIN for site links.