Files
portainer/docs/dev-stand.md
T
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

6.1 KiB

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:

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)

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

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:

# 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-imagebuild-allbuild-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.