Install
Pull the signed image, verify it, and run it: a running product with a fabricated estate, then the same stack for a real one.
Access, today. The product is pre-release and self-hosted. The quickstart below pulls the published, signed image and, once that image is public, needs no access to anything; building from source needs access to the repository, which stays private until the first pilot. For a trial licence, ask through the contact on the site and say which estate you would evaluate it against. Nothing on this page phones home and nothing is collected until you connect an account.
Quickstart
The platform is one published, signed image. You need Docker with Compose v2,
openssl for the passwords, and cosign
3 or later to check the signature. Nothing is built on your machine, and no
repository access or registry login is needed: the image is public.
A release is a version and the digest it names; the digest is in the release record. This page installs 0.1.2, released on 2026-10-24, the first release whose image carries every deployment file these pages name:
IMAGE=ghcr.io/cloudpanorama/panorama/panorama:0.1.2@sha256:f487a34623502b37bbed14b3a2dfbde9f3d773e3c5715a94a99e2fd294c446c2 # the release's version and digest
docker pull "$IMAGE"
cosign verify "$IMAGE" \
--certificate-identity https://github.com/cloudpanorama/panorama/.github/workflows/platform-release.yml@refs/tags/platform/v0.1.2 \
--certificate-oidc-issuer https://token.actions.githubusercontent.com
cosign verify accepts the digest only if it was signed by the repository's
release workflow, on the tag of that version, through GitHub's own identity
provider, and the signature is in the public transparency log. A digest that
was not, or a different identity, fails here — stop and ask. Use
cosign 3 or later: the release is signed in cosign 3's bundle format, and an
older cosign does not read it and reports "no signatures found" for a
correctly signed image.
The image runs natively on linux/amd64 and linux/arm64 (an Apple-silicon
Mac, an ARM VM): the digest is a multi-platform index, docker pull takes your
host's platform from it, and the release signs the index and each platform's
image, so cosign verify passes on either digest
(docker buildx imagetools inspect "$IMAGE" lists them). Each platform's image
carries its own attested CycloneDX SBOM, attested to the index as well:
cosign verify-attestation --type cyclonedx with the same two flags returns
both.
The deployment files come out of the image you have just verified, and the passwords are generated on the spot:
mkdir -p panorama/deploy && cd panorama
id=$(docker create "$IMAGE") && docker cp "$id:/deploy/." deploy/ && docker rm "$id"
( umask 077; {
echo "PANORAMA_IMAGE=$IMAGE"
for v in POSTGRES_PASSWORD PANORAMA_APP_PASSWORD PANORAMA_CONTROL_PASSWORD; do
echo "$v=$(openssl rand -hex 24)"
done
echo "SEED_DEMO=true" # the demo estate; leave it out for a real stack
} > deploy/.env )
sh deploy/compose-worker-token.sh deploy/.env deploy/docker-compose.yml # the workers' ingest credential, into deploy/.env
docker compose -f deploy/docker-compose.yml up -d # Postgres + every role, migrated, seeded
docker compose -f deploy/docker-compose.yml logs seed # the demo admin's password, printed once
The copied deploy/ is everything the rest of these pages use, in one layout,
signed with the image: the stack itself (deploy/docker-compose.yml,
deploy/bootstrap-roles.sql, deploy/compose-worker-token.sh,
deploy/.env.example); the TLS edge's proxy example,
deploy/proxy/nginx.conf.example; the reader templates —
deploy/cloudformation/reader-role.yaml and
deploy/cloudformation/cost-report-role.yaml for AWS,
deploy/gcp/reader-role.yaml for Google Cloud, deploy/azure/reader.bicep
with its role files for Azure; deploy/cloudformation/platform-identity.yaml,
the platform's own AWS identity, with its guide
deploy/cloudformation/platform-identity.md; the Kubernetes manifests,
deploy/k8s/ and deploy/overlays/example/; and, for the provisioning agent,
its stack deploy/cloudformation/agent-runtime.yaml and the runner templates
deploy/cloudformation/library-runner-roles.yaml, deploy/gcp/library-runners/
and deploy/azure/library-runners/. Each later step names its file as
deploy/<path>, so run it from this directory. The image carries all of
them from platform 0.1.2; 0.1.1's carries only the first three files named here.
Then open http://127.0.0.1:18080 and sign in as admin@demo.invalid with
that password. The public status page — no login — is
http://127.0.0.1:18080/status/demo.
The demo tenant contains three fabricated accounts, 200 EC2 instances,
Kubernetes nodes and pods, and thirty days of operational history. It also
includes four tag-defined applications and teams, sixty days of spend,
budgets, anomalies, recommendations, Kubernetes cost allocation, and
provisioning templates, requests, approvals, simulated logs and drift.
Account IDs start at 000000000001 and example names use demo-.
The seed makes no AWS calls, executes no provisioning and sends no
notifications. The example agent is revoked and the webhook is disabled.
Re-running the seed upgrades an inventory-only demo and preserves its login.
No credential is shipped and there is no default password anywhere, the demo
admin's included. Ingest is authenticated, so the install issues the worker
fleet's credential before the roles start (the compose-worker-token.sh step
above: panorama admin create-worker-token, run from the image against the
migrated database, written to deploy/.env as WORKER_TOKEN and printed by
nobody). The script refuses a deploy/.env that is not mode 0600 rather than
append a credential to it, and does nothing when a credential is already
chosen. The workers reach serve inside the compose network, and the proxy
example (deploy/proxy/nginx.conf.example) refuses the ingest routes at the
edge. Behind that proxy, set CLIENT_ADDRESS_SOURCE=proxy and
TRUSTED_PROXIES in deploy/.env (deploy/.env.example says which address),
or the login throttle counts every client as the proxy. /readyz answers 200
about ten seconds after up. The host ports are 18080 and 55432 rather than
8080 and 5432 because the other pillars of this workspace hold the obvious
ones; override with SERVE_PORT and POSTGRES_PORT.
docker compose -f deploy/docker-compose.yml logs -f # follow every role
docker compose -f deploy/docker-compose.yml down # stop; --volumes also deletes the database
An upgrade is the same three steps for the new release: set PANORAMA_IMAGE
in deploy/.env to its version and digest, docker pull and cosign verify
it, and docker compose -f deploy/docker-compose.yml up -d — the migration
runs before the roles start. Copy the deployment files out of the new image
again when its release notes say they changed.
Without SEED_DEMO=true the same command brings up an empty, real stack. Its
first tenant is created with panorama admin bootstrap, which prints the
administrator's generated password once:
docker compose -f deploy/docker-compose.yml run --rm serve admin bootstrap \
--tenant-slug acme --tenant-name 'Acme' --email you@example.com
A real deployment sets its settings before the first up (the clean-install
drill). Leave SEED_DEMO=true out of deploy/.env and add, before the
worker-token step and up:
cat >> deploy/.env <<'EOF'
ENTITLEMENTS_UNRESTRICTED=false # compose defaults it to true; a licence refuses it
COOKIE_SECURE=true # TLS terminates in front of serve
PORTAL_URL=https://panorama.example.com
OPERATOR_CONTACT=ops@example.com # the Help menu's contact; the pilot gate requires one
EOF
A source build also sets LICENSE_PUBLIC_KEY there before make compose-up
(it is baked into the image at build); make compose-up writes deploy/.env
only when it is absent, so run make deploy/.env first to generate the
passwords, append the settings, then make compose-up.
Compose publishes serve on 127.0.0.1:18080 only; put a TLS terminator on the
same host in front of it — deploy/proxy/nginx.conf.example, with its upstream
set to that port — and get its certificate however you get certificates
(Let's Encrypt's certbot works). The example already refuses the ingest
routes. For a proxy on the host, TRUSTED_PROXIES is the compose network's
gateway, which exists only once the stack has started once:
docker network inspect panorama_default -f '{{range .IPAM.Config}}{{.Gateway}}{{end}}'
Once the stack is up, append CLIENT_ADDRESS_SOURCE=proxy and
TRUSTED_PROXIES=<that gateway> to deploy/.env and run
docker compose -f deploy/docker-compose.yml up -d again. Set the two
together: serve refuses proxy without TRUSTED_PROXIES, and with peer
the login throttle counts every client as the proxy.
From there, people are added, reset and removed without SQL — the portal's
Users page, or panorama admin create-user, reset-password, set-role,
disable-user, enable-user and revoke-sessions (docs/runbook.md,
"Onboarding and removing people").
Building from source instead
The alternative to the published image, for a checkout of the repository
(which needs access to it). It builds the same image on the machine, so it
needs Docker with BuildKit (on Ubuntu, the docker-buildx package), make,
openssl, and about 6 GB of memory with swap — on 4 GB the Go build is killed
(compile: signal: killed).
git clone https://github.com/cloudpanorama/panorama && cd panorama/observability
SEED_DEMO=true make compose-up # builds panorama:local, then the same stack
The image is built once, by the migrate role; the other roles run the
image it tags and never try to pull panorama:local from a registry.
make compose-up generates deploy/.env with random passwords and
PANORAMA_IMAGE=panorama:local the first time it runs, and adds
deploy/docker-compose.build.yml to build it; every other command on this page
is unchanged. make compose-logs, make seed (RESET=1 replaces the demo) and
make compose-down (WIPE=1 deletes the database volume) are its shortcuts. A
source build accepts licences only when LICENSE_PUBLIC_KEY is set in
deploy/.env before it; the published image carries the product's key.
Kubernetes
The same image runs each role as its own Deployment, with migrate as a Job before them, against a Postgres you provide — the compose shape, productionised. The manifests are deploy/k8s, in the deploy/ the quickstart copies out of the image (from platform 0.1.2), a kustomization whose image names no usable tag: you deploy a copy of deploy/overlays/example, beside it in deploy/overlays/, which pins the image by digest, and add the namespace and the components you use — eks-irsa for IRSA on EKS, external-secrets to fill the Secret from your secret store.
What it renders
configmap.yamlmigrate-job.yamlserve.yamlschedule.yamlwork.yamlnotify.yamlcleanup.yamlserviceaccounts.yamlpdb.yamlnetworkpolicy.yaml
The Secret
Named panorama-secrets, with exactly these keys — three database URLs for three database roles, and the worker fleet's credential:
CONTROL_DATABASE_URLDATABASE_URLMIGRATE_DATABASE_URLWORKER_TOKEN
Apply
kubectl create secret generic panorama-secrets --from-literal=DATABASE_URL=... \
--from-literal=CONTROL_DATABASE_URL=... --from-literal=MIGRATE_DATABASE_URL=... \
--from-literal=WORKER_TOKEN=placeholder
deploy/k8s/deploy.sh <your overlay> <namespace> # serve waits for a credential
kubectl exec deploy/panorama-work -- /panorama admin create-worker-token --name 'cluster workers' --raw
# put the token in the Secret's WORKER_TOKEN, then:
deploy/k8s/deploy.sh <your overlay> <namespace>
On a first install the worker token cannot exist yet: minting it needs the migrated schema, which deploy.sh creates, and serve refuses to start until a credential exists. So the first deploy names serve and exits 1 with every other role up; the token is minted from a running work pod (--raw prints it alone), and the second deploy, with the Secret's new version, rolls every role. deploy.sh deploys in two phases: it recreates and applies the migrate Job on its own, waits for it to complete — a failed migration stops there, with the roles untouched — and only then applies the roles and waits for every Deployment's rollout, naming, describing and logging any role that did not get there before it exits 1; one kubectl apply -k would roll new binaries out regardless of the schema and could not update a finished Job's immutable template on the next release. Each pod template carries a digest of the ConfigMaps and the Secrets' versions, so a release that only changes configuration, or only rotates the Secret, rolls the pods that read it — rotating a credential is running deploy.sh again.
Before the first apply, fill TRUSTED_PROXIES in the ConfigMap with your ingress controller's addresses — serve refuses to start while it is empty, because a proxy header trusted from anywhere would let any pod choose the login throttle's key — and run bootstrap-roles.sql once as the database owner. What is deliberately not in the manifests: an Ingress (TLS terminates at your edge), the database, autoscaling, and the hosted track's environments.
The provisioning agent
Provisioning runs in your account, never in ours: a small agent you install where your infrastructure credentials already are. Its own page covers install, configuration, verification and upgrade.
Then
Connect a real AWS account, install the licence you were issued, and keep the runbook to hand.
Generated from observability/README.md (972fb451057a), observability/deploy/k8s/kustomization.yaml (341999132dcf), observability/deploy/k8s/secret.example.yaml (d40c5939f819), observability/deploy/k8s/deploy.sh (10fd3770ea54) by www/tools/docs; the value in brackets is a hash of the source's content, so a page changes only when its source does.