Run a public demo
A read-only Shotstash with synthetic sample media, reset every night, that anyone can try without an account of their own.
Demo mode turns an instance into a public showroom: visitors sign in with shared, read-only demo accounts and browse a sample project made of synthetic media. Nothing they do changes anything, and the sample data is restored every night. Your own super admin account keeps full rights.
Run a demo on its own server and its own database. Demo mode is meant for an instance that holds nothing but the demo.
What demo mode does
Demo mode is on when the app starts with SHOTSTASH_DEMO_MODE=true and DEMO_ADMIN_PASSWORD set (at least 10 characters).
-
Demo accounts. The seed creates three accounts that share
DEMO_ADMIN_PASSWORD:Account Role demo-admin@example.comAdmin demo-editor@example.comEditor demo-viewer@example.comViewer -
Read-only. All three are read-only: every write, upload and profile change answers
FORBIDDEN. Demo accounts cannot upload at all, so there is no upload limit to tune. -
The roles still show. The demo admin opens the Admin Panel in view-only mode (the demo accounts only, controls switched off), so visitors see what an admin can do that an editor cannot.
-
Password on the sign-in page. In demo mode the sign-in page lists the demo accounts and their password. That is the point: the accounts cannot change anything, so the password is not a secret.
-
No sign-up. Public sign-up is forced off and
registeranswersFEATURE_DISABLED, whateverSHOTSTASH_FEATURE_SIGNUPsays. -
A banner in the app says it is a public demo, read-only, reset every night.
-
Your account is untouched. The super admin you create at
/setupis not a demo account and keeps every right.
What you need
- A small Linux server with Docker Engine and the Compose plugin 2.24.4 or later (the demo override uses
!override). 2 GB of memory is enough. - A domain name pointing at it, for example
demo.example.com. - A TLS reverse proxy in front of the app. The Networking guide has Caddy and nginx examples; Caddy gets certificates by itself.
1. Get the code and create .env
git clone https://github.com/Aldyernesto/shotstash.git
cd shotstash
sh docker/init.shdocker/init.sh copies .env.example to .env and fills every generated secret with a random value, then runs the preflight. It never overwrites an existing .env, and it prints no secret.
Then add the demo settings to .env:
APP_URL=https://demo.example.com
TRUST_PROXY=true
DEMO_ADMIN_PASSWORD=pick-a-friendly-password
SHOTSTASH_CORS_ORIGINS=https://aldyernesto.github.io
COMPOSE_FILE=docker-compose.yml:docker-compose.images.yml:docker-compose.demo.ymlAPP_URLandTRUST_PROXY=true: the app sits behind your TLS proxy (see Networking for what the proxy must do beforeTRUST_PROXY=trueis safe).DEMO_ADMIN_PASSWORD: every visitor sees it, so make it easy to type.SHOTSTASH_CORS_ORIGINS: the origin of the docs site, and nothing else. It is only needed for the docs try-it console (see below).COMPOSE_FILE: replace the line.env.examplebrought along. It keeps the published images and adds the demo override, so every plaindocker composecommand below uses both.
2. Start with the demo override
docker compose up -ddocker-compose.demo.yml sets SHOTSTASH_DEMO_MODE=true, refuses to start without DEMO_ADMIN_PASSWORD in .env, and publishes the app on 127.0.0.1 only, so it is reached through your TLS proxy and nowhere else.
Point the proxy at 127.0.0.1:3005. A Caddy site for the demo:
demo.example.com {
reverse_proxy 127.0.0.1:3005 {
header_up -Cf-Connecting-Ip
header_up X-Forwarded-For {remote_host}
header_up X-Forwarded-Proto {scheme}
flush_interval -1
}
}Running inside an unprivileged LXC container
Docker works inside an unprivileged Proxmox LXC container when nesting is on. Two differences from a plain host:
-
The container may not raise the memlock limit, so the cache service fails with "error setting rlimit type 8". Add a local override that drops it (Dragonfly runs fine without):
# docker-compose.local.yml, appended to COMPOSE_FILE after the demo override services: cache: ulimits: !reset {} -
When other sites share the host, give the demo memory and CPU limits in the same override (
mem_limit,cpusper service) so it never starves them.
3. Finish setup
Open https://demo.example.com. You land on /setup, which creates your owner account (the super admin). docker/init.sh set a SETUP_TOKEN, so the form asks for it:
grep SETUP_TOKEN .env4. Seed the demo
docker compose exec -u node app node dist/demo.js seedThe seed runs inside the app container as the app user (-u node: as root it refuses, so the media files keep the right owner) and generates everything on the server. It creates the three demo accounts and a sample project with:
- images made with sharp and short ffmpeg test clips, plus a PDF document;
- thumbnails and one 720p proxy version;
- a public share link and a discussion thread.
No real photos, faces or footage are involved. The seed refuses to run (and exits with an error) when demo mode is off or setup is not finished.
Sign out, sign in as demo-viewer@example.com with the demo password, and check that the project opens, a clip plays and the share link works.
Resets
Nightly. While demo mode is on, the app resets the demo by itself every night at 03:00 in SHOTSTASH_DEFAULT_TIMEZONE. With several app processes, only one runs it (a Dragonfly lock).
By hand.
docker compose exec -u node app node dist/demo.js resetA reset deletes the demo project's rows and bytes, the demo accounts' sessions and notifications, then seeds again. It never touches instance_settings (so setup never reopens), your super admin or any account that is not a demo account.
Without Docker (a source checkout with npm run dev or npm start): set SHOTSTASH_DEMO_MODE=true and DEMO_ADMIN_PASSWORD in .env (the override sets the first one for Docker only), then npm run demo:seed and npm run demo:reset do the same.
A seed or reset and the nightly reset never run at the same time: they share one Dragonfly lock. Without a reachable Dragonfly the nightly reset is skipped with a warning in the log.
The docs try-it console
The REST reference can show a try-it console that sends requests to your demo. It is off unless the docs site is built with DEMO_ORIGIN, and when it is on it talks to that origin only, never to a visitor's own install.
- Allow the docs origin on the demo.
SHOTSTASH_CORS_ORIGINStakes comma-separated exact origins (scheme://host[:port], no path, no wildcard; empty by default). For the default GitHub Pages site the origin ishttps://aldyernesto.github.io: an origin never has a path, so not.../shotstash/. Note thathttps://<owner>.github.iocovers every Pages site of that user, not only these docs; a custom docs domain (DOCS_DOMAIN) is a tighter origin. Matching origins getAccess-Control-Allow-Originechoed on/api/*, with credentials off and an answer to the preflight. Any other origin gets no CORS headers. - Build the docs with the demo origin. In the repository settings, set the variable
DEMO_ORIGIN(for examplehttps://demo.example.com) and rerun thedocs.ymlworkflow. The REST reference then shows the console pointed at that origin, with a Get a demo token button.
The button calls POST /api/v1/demo/session, which:
- exists only in demo mode (
404otherwise); - is rate limited to 10 per hour per IP (
429withRetry-Afterbeyond that); - refuses a request whose
Originis neither the app's own nor listed inSHOTSTASH_CORS_ORIGINS; - answers
{ token, expiresAt }: a 60-minute session of the read-only demo viewer that never slides. Reads work; every write is refused.
Keep an eye on it
Shotstash has no telemetry and makes no outbound calls. Watch the demo from the outside with your own uptime monitor on https://demo.example.com/api/health, which answers { ok, setupRequired, version } and returns 503 when the database, cache or storage is down.
No monitoring account? The repository includes .github/workflows/demo-uptime.yml: set the repository variable DEMO_ORIGIN to your demo's address and GitHub checks /api/health every 15 minutes from outside your network. A failed run sends you GitHub's usual failed-workflow email.
Upgrade a demo like any other install (Upgrading). Keep the demo override in COMPOSE_FILE; a command with its own -f flags ignores COMPOSE_FILE, so name all three files there or none.