Compare commits

...

1 Commits

Author SHA1 Message Date
agent_coder
4d8fb82e15 docs: add local demo stand guide + reference it from CLAUDE.md
Documents building a Portainer image from one or more feature branches and
running it, with the gotchas that bite: build-image must get ENV=production
(else it ships a dev eval-source-map client bundle that the default CSP blocks,
hanging the UI on "Loading Portainer…"), CSP is on by default, first-run admin
init needs --no-setup-token, and the admin password must be simple but >=12
chars. Referenced from CLAUDE.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 18:14:49 +03:00
2 changed files with 138 additions and 0 deletions

View File

@@ -57,3 +57,12 @@ make format # Format code
- Frontend: http://localhost:8999
- Backend: http://localhost:9000 (HTTP) / https://localhost:9443 (HTTPS)
## Local demo stand
To build an image from one or more feature branches and run it (e.g. to demo open
PRs together), see [docs/dev-stand.md](docs/dev-stand.md). **Read its Gotchas
first** — most importantly, build the image with `make build-image ENV=production`
(without it, `build-image` ships a development client bundle that the CSP blocks,
leaving the UI stuck forever on "Loading Portainer…"), and note that the admin
password must be simple/special-char-free but at least 12 characters long.

129
docs/dev-stand.md Normal file
View File

@@ -0,0 +1,129 @@
# Running a local demo stand
How to build a Portainer image from one or more feature branches, run it, and log
in — plus the non-obvious gotchas that will otherwise eat an hour. Written from
real setup pain — read the **Gotchas** section before you start, especially
gotcha #1 (a development client bundle silently kills the whole UI).
## Prerequisites
- **Go 1.26+** (backend, `go.mod` at repo root), **Node 20+ / pnpm 10+**
(frontend), and **Docker** (to build the image and to run it against the local
Docker socket).
- Frontend deps installed: `pnpm install --no-frozen-lockfile` then
`git checkout pnpm-lock.yaml` (never commit the lockfile).
## 1. Pick what goes in the image
For a single branch just check it out. To demo **several open PRs together**,
make an integration branch off `develop` and merge each PR branch into it:
```bash
git checkout -B stand/all-prs origin/develop
git merge --no-edit origin/feat/2-stream-logs # PR #6
git merge --no-edit origin/feat/3-auto-update # PR #19
# resolve any conflicts, then build from stand/all-prs
```
## 2. Build the image — pass `ENV=production` (see gotcha #1)
```bash
export PATH=/path/to/go/bin:$PATH
make build-image ENV=production TAG=stand
# → builds client (webpack, production), server binary, and the Docker image
# → produces portainerci/portainer-ce:stand (~190 MB, FROM portainer/base)
```
`make build-image` depends on `build-all`, so it **re-runs** `build-client` and
`build-server` itself — you do **not** need to run them first. But `build-all`
uses the Makefile's default `ENV`, which is `development`. **You must pass
`ENV=production` to `build-image`** or it ships a development client bundle that
the browser refuses to run (gotcha #1). The client webpack build takes a couple
of minutes; the node-version warning (`wanted 22, current 20`) and the babel
`isModuleDeclaration` deprecation warning are both harmless.
## 3. Run it
```bash
docker run -d \
--name portainer-stand \
-p 9000:9000 -p 9443:9443 -p 8000:8000 \
-v /var/run/docker.sock:/var/run/docker.sock \
-v portainer-stand-data:/data \
--restart unless-stopped \
portainerci/portainer-ce:stand --no-setup-token # --no-setup-token: gotcha #3
```
- UI: `http://localhost:9000` (HTTP) or `https://localhost:9443` (HTTPS).
- To reach it from another machine, use the box's LAN/VPN IP — the server binds
`0.0.0.0`, so no extra flag is needed (unlike a Vite dev server).
## 4. Seed the admin and a Docker environment
You can click through the first-run wizard, or seed it via the API so the login
is ready and there is something to manage:
```bash
# Create the admin (needs --no-setup-token from step 3, or the setup token header)
curl -s -X POST http://localhost:9000/api/users/admin/init \
-H 'Content-Type: application/json' \
-d '{"Username":"admin","Password":"portainer1234"}'
# Log in to get a JWT
JWT=$(curl -s -X POST http://localhost:9000/api/auth \
-H 'Content-Type: application/json' \
-d '{"Username":"admin","Password":"portainer1234"}' | jq -r .jwt)
# Add the local Docker socket as an environment so the UI has live containers
curl -s -X POST http://localhost:9000/api/endpoints \
-H "Authorization: Bearer $JWT" \
-F "Name=local" -F "EndpointCreationType=1"
```
> **Use a simple password with no special characters** — but note Portainer
> enforces a **minimum length of 12** for the admin account, so it can't be a
> single short word. Use a plain alphanumeric string of 12+ chars (e.g.
> `portainer1234`), not `Str0ng!Pass@2026`. Demo/test credentials get passed
> through shells, JSON payloads, and URLs by scripts and automation, where `!`
> `@` `$` `&` etc. get mangled or need escaping — a plain alphanumeric string
> avoids a whole class of "wrong password" confusion.
The `-v portainer-stand-data:/data` volume persists the admin and environments,
so rebuilding/replacing the container keeps your login.
## Gotchas (the грабли)
1. **`make build-image` without `ENV=production` ships a broken UI.**
`build-image``build-all``build-client` runs with the Makefile default
`ENV=development`, whose webpack config uses `devtool: 'eval-source-map'`. That
wraps **every module in an `eval()` call**. Portainer serves a
Content-Security-Policy with `script-src` that does **not** include
`'unsafe-eval'` (gotcha #2), so the browser blocks every module, the app never
bootstraps, and you're stuck forever on **"Loading Portainer…"** with **zero
API calls** in the network tab (all static assets load `200`, which makes it
look like a backend/network problem — it isn't). Always build the image with
`ENV=production`. To confirm a good bundle: `dist/public/main.*.js` should have
a hashed filename and **not** contain thousands of `eval(` calls or an
`"eval-source-map devtool has been used"` header.
2. **CSP is on by default and omits `'unsafe-eval'`.** The `--csp` flag
(env `CSP`) defaults to `true`, so the server sends
`Content-Security-Policy: script-src 'self' …` on every response
(`api/http/security/bouncer.go`). The **production** bundle is CSP-safe; only
the development (eval) bundle trips over it. You normally should not need to
touch this — fix the bundle (gotcha #1), don't disable CSP. If you truly must,
run with `--csp=false`, but then you're not testing what ships.
3. **First-run admin init requires a setup token.** Recent Portainer refuses
`POST /api/users/admin/init` unless you send the `X-Setup-Token` header (the
token is printed in the server logs at startup). For a throwaway demo, start
the container with `--no-setup-token` to disable that requirement.
4. **Admin password minimum length is 12.** Unlike some stands where a short
one-word password is fine, Portainer rejects an admin password shorter than 12
characters (`RequiredPasswordLength` in `/api/settings/public`). Keep it
simple and special-char-free, just make it 12+ (e.g. `portainer1234`).
5. **`build-image` double-builds the client.** Because it re-runs `build-all`, if
you already ran `make build-client` yourself the client compiles twice. Just
run `make build-image ENV=production` and skip the separate `build-client`.