Public sandbox#
A demo instance anyone can try, with limits, that is deleted and rebuilt from nothing every 3 hours. It is an ordinary
workspace (see creating-workspaces.md) created with the profile sandbox: the profile
(deploy/profiles/sandbox.env) is a block of variables appended to the workspace's .env.<slug>. Nothing else
distinguishes it: the product reads the variables, the hardening overlay docker-compose.sandbox.yml is activated by
COMPOSE_FILE in that same file, so every command on the workspace (up, down, remove-workspace.sh) uses it.
This is an internal tool, not a supported feature for third parties: whoever wants one follows the procedure below.
What visitors get#
- One shared demo account (e-mail and password are public: put them on the page that links to the sandbox).
- The editor, the DAM, websites, pages, snippets, themes, the API Gateway towards the allowed hosts.
- A banner on every screen with the time of the next reset.
The profile: every limit is a variable#
Variable (in .env.<slug>) |
Default of the profile | Effect |
|---|---|---|
SANDBOX_MODE |
1 |
Switches the restrictions on; without it the same stack is a normal workspace |
ASSET_MAX_FILE_BYTES |
5242880 (5 MB) |
Per-file upload cap, answers 413 (usable outside the sandbox too; default there 50 MB) |
ASSET_MAX_COUNT |
50 |
Maximum number of assets; a new asset or _edited copy over it answers 409, overwriting a path is allowed |
ASSET_MAX_INPUT_PIXELS |
16000000 |
Image dimensions |
SANDBOX_ALLOW_HOSTS |
dummyjson.com |
The only host names outbound HTTP may reach (exact match, comma-separated); empty = none |
PUBLIC_RATE_LIMIT_PER_MINUTE, PUBLIC_SEARCH_..., PUBLIC_QUERY_... |
60 / 30 / 30 | Public API rate limits |
WS_MAX_BODY |
6M |
nginx request body cap (a 5 MB file plus multipart overhead) |
WS_EDGE_LIMITS |
10 requests/s per address with a burst of 40, 20 connections, 60 s timeouts | nginx directives rendered into the workspace's locations include |
SANDBOX_NO_NEW_PRIVILEGES |
true |
Container flag; see "snap Docker" below |
COMPOSE_FILE |
docker-compose.yml:docker-compose.sandbox.yml |
Activates the hardening overlay |
SANDBOX_RESET_AT |
set by reset.sh |
Time shown in the banner |
Change a value at creation with --set KEY=VALUE (repeatable), or edit .env.<slug> and recreate the containers
(docker compose -p ws-sandbox --env-file .env.sandbox up -d). The hardening itself (read-only root file system for the Node
services, no capabilities, memory/CPU/process limits) is in docker-compose.sandbox.yml and is not a variable.
Features that are off (answer 403 "disabled in the public sandbox")#
AI (/ai/*), mail (/mail-*, and sending itself), object events (/object-event-*: they call outside the stack), the Figma import, the migration sources, the CDN mirror settings,
PWA generation, regenerating a website's gateway key. Outbound HTTP through safeFetch is refused for every host that is not
in SANDBOX_ALLOW_HOSTS, checked before any DNS lookup and on every redirect hop; a listed host still goes through the
normal SSRF checks (no private or loopback addresses). So a visitor can create gateway endpoints and routes, but only towards
the allowed hosts: Cloudinary, Brevo and CDN mirroring stay off. The path list lives in packages/core/src/sandbox.ts.
Users and roles are read-only for the demo account (it can list them, not create, change or delete them, reset passwords or
change its own MFA), so visitors cannot lock each other out.
Procedure#
Needs a host with Docker Compose 2.24 or newer, a DNS name pointing to it, and nothing else of value on it.
- Clone the repository and enter it. The reset timer expects
/opt/whoobe_project; anywhere else, editWorkingDirectoryandExecStartin the service file. - First start, by hand:
SANDBOX_HOST=sandbox.example.com ./scripts/sandbox/reset.sh(SANDBOX_SCHEME=httpor--scheme httpon a LAN without TLS). It removes a previous sandbox with its volumes, provisions the workspacesandboxwith--profile sandbox, waits for/healthto report the sandbox block, and printssandbox is up. Options, all environment variables:SANDBOX_SCHEME(httpon a LAN, defaulthttps),SANDBOX_INDEX(ports are derived from it: check they are free withss -ltn),SANDBOX_DEMO_EMAIL,SANDBOX_DEMO_PASSWORD,SANDBOX_INTERVAL_HOURS, and the overridesSANDBOX_NO_NEW_PRIVILEGES,SANDBOX_ALLOW_HOSTS,ASSET_MAX_FILE_BYTES,ASSET_MAX_COUNT,ASSET_MAX_INPUT_PIXELS. - TLS on a public host:
scripts/enable-tls.shas for any workspace. The proxy then also serves port 443: if another service owns it on that host, use a dedicated host for the sandbox. - Check it from outside:
scripts/sandbox/verify.sh https://<host> <demo e-mail> <demo password>(limits, disabled features, read-only users, public settings; it deletes the test assets it uploads). - Reset every 3 hours: copy
scripts/sandbox/whoobe-sandbox.env.exampleto/etc/whoobe-sandbox.env(mode 600) and fill in the variables of step 2 (a misspelled name is silently ignored); copyscripts/sandbox/systemd/whoobe-sandbox-reset.serviceand.timerto/etc/systemd/system/. If the clone is not in/opt/whoobe_project, or the user that owns it is not root, install the drop-inscripts/sandbox/systemd/override.conf.exampleas/etc/systemd/system/whoobe-sandbox-reset.service.d/override.conf(user and paths; running as the repository's owner keeps the generated files from belonging to root, and that user must be in thedockergroup). Thensystemctl daemon-reload,systemctl start whoobe-sandbox-reset.service(a first reset, which deletes the sandbox's data) andsystemctl enable --now whoobe-sandbox-reset.timer. Check withsystemctl list-timers 'whoobe*'andjournalctl -u whoobe-sandbox-reset -n 40. The service is stopped after 15 minutes and tried again after one: a dependency download that stalls does not leave the sandbox off until the next cycle.reset.shtakes a lock (a second reset exits at once), stops thedocker compose ... upof a killed earlier reset, and on a stop signal terminates everything it started: a snap-installed Docker client is not stopped with its systemd service and would otherwise stay behind waiting, blocking the next attempt. - Host firewall: in, only 80 and 443. Out, keep 443 and DNS open: the allowed API (
dummyjson.com) is behind a CDN whose addresses change, so the application list (SANDBOX_ALLOW_HOSTS) is the control, not an address filter. Not exercised yet on a public host.
To stop it: ./scripts/remove-workspace.sh sandbox --yes (deletes the data, as every reset does).
How the reset works#
scripts/sandbox/reset.sh removes the workspace with all its volumes (database and object store), provisions it again with new
random secrets and the demo account, and waits until /health reports the sandbox block. If anything fails the stack is
removed and the script exits non-zero: the sandbox stays off, it never keeps serving the previous cycle. Expect about a minute
of downtime (longer when the images must be rebuilt); the banner shows the next reset time.
Known limits of this design#
no-new-privilegesis on by default. With a snap-installed Docker (AppArmor) it makes images such as Postgres fail withexec ... docker-entrypoint.sh: operation not permitted; on such a host passSANDBOX_NO_NEW_PRIVILEGES=false(in/etc/whoobe-sandbox.env), and prefer the Docker Engine packages for a public host.- All visitors share one workspace: they see each other's content. Content is public for up to 3 hours, so moderate the page that advertises the sandbox accordingly and watch the host's bandwidth.
- Visitors can make the server call the allowed API from its address; the rate limits bound it, they do not remove it.
- The egress firewall and TLS have not been exercised yet; do it when the sandbox goes on a public host.
Last updated