Deployed
Scratch
A platform for deploying and browsing experimental web apps. Each app is either a container or a set of static files.
A Flask application that serves as a public scratchpad for small, self-contained web experiments. Each mini-app lives in its own subdirectory with independent HTML, CSS, and JavaScript, or as a pre-built React bundle dropped in as static output. Runs in a Podman container behind the shared NGINX reverse proxy.
Tech Stack
- Flask + Gunicorn (Python web server)
- UV (Python package manager)
- Podman (containerization)
Each hosted experiment has its own tech stack. The blog post covers each one individually.
Architecture
- Flask serves each mini-app from its own subdirectory under a named route.
/token-bucketservestoken-bucket/index.html/settlers-reactserves the pre-built React SPA.
- Vanilla apps are fully self-contained (one
index.html,style.css,script.jsper experiment). Flask’ssend_from_directorydelivers them with no special handling. - React apps are built with Vite locally and their
dist/output is committed directly into this repo as static files. - Flask returns
index.htmlas an SPA fallback for any path that doesn’t map to a real file on disk. - The Flask container runs on a dedicated port behind the shared NGINX reverse proxy.
- Traffic to
scratch.alexandershank.comroutes to this container. - Other apps on the same VPS are isolated from this traffic.
- Traffic to
- UV handles Python dependency resolution inside the container, keeping the image build fast and reproducible.
Challenges
- React apps served from a subpath require something like
base: '/settlers-react'invite.config.tsso that Vite generates asset URLs like/settlers-react/assets/index.jsinstead of/assets/index.js.- Without this, the browser requests assets from the root and the SPA fails to load.
- Flask does not automatically handle SPA client-side routing. A catch-all route checks whether the requested path maps to a real file on disk and falls back to
index.htmlif not.- Direct navigation and page refreshes return a 404.
Learnings
- Flask works well as a minimal static file server. For a scratchpad with no server-side logic, routing paths through Flask adds little overhead.
- Committing the React
dist/output into the repo decouples the settlers-react build from the scratch-app deployment. The scratch app can be redeployed without rebuilding the React app, and vice versa. - Structuring each experiment as a self-contained subdirectory keeps them isolated. A broken experiment does not affect others, and adding a new one only requires a new directory and a route.
For the Future
- Automate the React build-and-copy step so
settlers-react/dist/is updated automatically rather than committed manually after each change. - Stateful experiments.
- Leverage sever-side capabilities, instead of static web pages.