Perses Dashboards as Code Observability Platform on AWS User Guide
Overview
Perses is an open source dashboard and observability visualization platform, and a Cloud Native Computing Foundation project. Like Grafana, it renders dashboards over your observability data, with native support for Prometheus metrics, Tempo traces, Loki logs and Pyroscope profiles. What sets Perses apart is its dashboards as code approach: dashboards follow an open, standardized specification and can be authored, validated and version controlled as code with its CLI and SDKs, which makes it a natural fit for GitOps workflows.
Perses ships no authentication in this deployment shape, so the cloudimg image does not leave it open. The server is bound to the loopback interface only and nginx fronts it on port 80 behind an HTTP Basic Auth gate whose password is generated uniquely and at random on the first boot of every instance. Nothing usable ships inside the image.
What is included:
- Perses 0.53.1 from the official upstream release (Apache License 2.0,
perses/perses), pinned and sha256 verified at build time - The server binary, the
perclicommand line tool, and 24 bundled panel and query plugin modules under/opt/perses - Perses bound to
127.0.0.1:8080, fronted by nginx on:80withX-Forwarded-*headers - A file based dashboard store at
/opt/perses/data, so there is no external database to run - An unauthenticated
/healthzendpoint for load balancer and probe checks perses-firstboot.servicegenerating a 24 character per instance Basic Auth password (bcrypt in/etc/nginx/.perses.htpasswd) plus a per instance at-rest encryption key, and writing/root/perses-credentials.txt(mode 0600 root:root)- A seeded
demoproject andwelcomedashboard so the interface is populated on first boot - 24/7 cloudimg support
Prerequisites
An AWS account, an EC2 key pair, and a VPC subnet. The instance security group should allow inbound TCP 22 (SSH) and TCP 80 (web interface) from your trusted CIDR. m5.large is a comfortable default; Perses is a single lightweight Go service with a file based store, so smaller instances are viable for light use and larger ones only matter once you are driving many concurrent dashboard queries against your datasources.
Connecting to your instance
SSH in as the default login user for the operating system variant you launched. This listing currently ships one variant:
| OS variant | SSH login user |
|---|---|
| Ubuntu 24.04 LTS | ubuntu |
ssh ubuntu@<public-ip>
Step 1: Launch from AWS Marketplace
Subscribe to the cloudimg Perses listing in AWS Marketplace and launch an instance. Pick the m5.large size, choose your VPC and subnet, and attach a security group that allows inbound TCP 22 and TCP 80 from your trusted CIDR.
Step 2: SSH In
ssh ubuntu@<public-ip>
Step 3: Service Status
Confirm both the Perses server and the nginx reverse proxy are active. Perses is a single Go binary and starts in a second or two.
sudo systemctl is-active perses nginx
active
active
Step 4: Confirm the Health Endpoint
nginx serves a static, unauthenticated health endpoint. This is what you point an Application Load Balancer target group or an external uptime check at, so probes never need the instance password.
curl -s -o /dev/null -w 'healthz HTTP %{http_code}\n' -m 10 http://127.0.0.1/healthz
healthz HTTP 200
Step 5: Confirm Secure By Default Authentication
Everything except /healthz sits behind the per instance Basic Auth gate. An unauthenticated request to the API must be rejected.
curl -s -o /dev/null -w 'unauthenticated API HTTP %{http_code}\n' -m 10 http://127.0.0.1/api/v1/projects
unauthenticated API HTTP 401
Confirm the isolation as well: Perses itself listens only on loopback, and nginx is the only process bound to a public address.
sudo ss -tlnp | grep -E ':8080|:80 ' | awk '{print $1, $4, $6}'
LISTEN 0.0.0.0:80 users:(("nginx",pid=2223,fd=5),("nginx",pid=2222,fd=5),("nginx",pid=2221,fd=5))
LISTEN 127.0.0.1:8080 users:(("perses",pid=2214,fd=3))
LISTEN [::]:80 users:(("nginx",pid=2223,fd=6),("nginx",pid=2222,fd=6),("nginx",pid=2221,fd=6))
127.0.0.1:8080 means the Perses API cannot be reached from the network except through the authenticated proxy.
Step 6: Read Per-Instance Credentials
The first boot service generated this instance's web password and wrote the credentials file. Read it now and keep the terminal open while you sign in.
sudo cat /root/perses-credentials.txt
Pick up PERSES_URL, PERSES_USERNAME (always admin) and PERSES_PASSWORD. These values are unique to this instance and exist nowhere in the image; they are generated on the first boot of every instance you launch.
You can confirm the first boot ran cleanly:
sudo journalctl -u perses-firstboot.service --no-pager -o cat | tail -3
Starting perses-firstboot.service - cloudimg Perses first-boot (per-instance Basic Auth password + encryption key + URL/MOTD)...
[2026-07-25T15:10:40+0000] perses-firstboot: First-boot prep complete; systemd will start perses + nginx. UI at http://13.221.114.30/
Finished perses-firstboot.service - cloudimg Perses first-boot (per-instance Basic Auth password + encryption key + URL/MOTD).
Step 7: Sign In to Perses
Browse to http://<public-ip>/. Your browser prompts for a username and password, which is proof the platform is not open to the internet. Enter admin and the PERSES_PASSWORD value from the credentials file.
The home view lists your projects. The seeded demo project is already there with one dashboard, so the interface is populated rather than empty on a fresh launch.

Step 8: Open the Seeded Dashboard
Click into DEMO and open the Welcome dashboard. It uses Markdown panels, which render standalone with no datasource attached, so the dashboard is meaningful before you have connected anything to it.

Step 9: Explore the Project
The project view lists that project's dashboards with creation and update times, plus tabs for Variables, Datasources and Secrets scoped to the project. This is where you add the Prometheus, Tempo, Loki or Pyroscope endpoint your dashboards will query.

Step 10: Confirm the API Round-Trip
The same REST API drives the interface, the CLI and any automation you write. Read the projects and dashboards back with the per instance password.
PASS=$(sudo grep '^PERSES_PASSWORD=' /root/perses-credentials.txt | cut -d= -f2-)
curl -s -u "admin:${PASS}" -m 15 http://127.0.0.1/api/v1/projects | python3 -c "import sys,json;d=json.load(sys.stdin);print('projects:',len(d));[print(' -',p['metadata']['name']) for p in d]"
projects: 1
- demo
PASS=$(sudo grep '^PERSES_PASSWORD=' /root/perses-credentials.txt | cut -d= -f2-)
curl -s -u "admin:${PASS}" -m 15 http://127.0.0.1/api/v1/projects/demo/dashboards | python3 -c "import sys,json;d=json.load(sys.stdin);print('dashboards in demo:',len(d));[print(' -',x['metadata']['name'],'|',x['spec'].get('display',{}).get('name','')) for x in d]"
dashboards in demo: 1
- welcome | Welcome
Step 11: Create Your Own Project
Projects are how Perses scopes dashboards, datasources and secrets, typically one per team or per system. Create one over the API (the Create Project button in the interface does the same thing).
PASS=$(sudo grep '^PERSES_PASSWORD=' /root/perses-credentials.txt | cut -d= -f2-)
curl -s -o /dev/null -w 'create project HTTP %{http_code}\n' -m 15 -u "admin:${PASS}" \
-H 'Content-Type: application/json' -X POST http://127.0.0.1/api/v1/projects \
-d '{"kind":"Project","metadata":{"name":"my-team"}}'
curl -s -u "admin:${PASS}" -m 15 http://127.0.0.1/api/v1/projects | python3 -c "import sys,json;d=json.load(sys.stdin);print('projects now:',[p['metadata']['name'] for p in d])"
curl -s -o /dev/null -w 'cleanup delete HTTP %{http_code}\n' -m 15 -u "admin:${PASS}" -X DELETE http://127.0.0.1/api/v1/projects/my-team
create project HTTP 200
projects now: ['demo', 'my-team']
cleanup delete HTTP 204
The last line removes the throwaway project again. Drop it when you are creating a project you intend to keep.
Step 12: Dashboards as Code with percli
The bundled percli command line tool is what makes the dashboards as code workflow real: dashboard definitions live in your Git repository, and the CLI applies them to a Perses instance from CI.
/opt/perses/percli version
client:
buildTime: "2026-03-12"
version: 0.53.1
commit: 2b65a98ba3febd6342d5d99c1b498c6ff7ae08f2
From a workstation, log in to this instance and apply a dashboard definition from a file. Basic Auth credentials go on the URL for the CLI, and the file is the same JSON the API accepts:
percli login http://admin:<password>@<public-ip>
percli apply -f my-dashboard.json --project my-team
Keep those definitions in version control and run percli apply from your pipeline, and dashboard changes get reviewed like any other change.
Step 13: Review the Server Configuration
The Config view in the top navigation renders the running server configuration and the installed plugin set. Note enable_auth: false (the nginx gate is the single credential surface), the file based store at /opt/perses/data, and the at-rest encryption_key shown only as <secret>.

You can confirm the plugin set over the API. All 24 bundled panel and query modules, including Markdown, TimeSeriesChart, StatChart and the Prometheus datasource, are extracted and loaded at startup.
PASS=$(sudo grep '^PERSES_PASSWORD=' /root/perses-credentials.txt | cut -d= -f2-)
curl -s -u "admin:${PASS}" -m 15 http://127.0.0.1/api/v1/plugins | python3 -c "import sys,json;d=json.load(sys.stdin);print('plugin modules loaded:',len(d))"
plugin modules loaded: 24
Step 14: Run the Built-In Self-Test
The image ships a diagnostic that proves the whole stack end to end: the health endpoint, the authentication gate (unauthenticated and wrong password both rejected), the seeded demo resources, and a full create, read and delete round-trip through the proxy. Run it any time you want to confirm the instance is healthy.
PASS=$(sudo grep '^PERSES_PASSWORD=' /root/perses-credentials.txt | cut -d= -f2-)
sudo /usr/local/sbin/perses-selftest.sh "$PASS"
OK perses gate + REST round-trip + demo resources verified
Step 15: Connect Your First Datasource
Perses queries your existing observability backends; it does not store metrics itself. In the interface, open your project, go to the Datasources tab, and add a datasource pointing at your Prometheus, Tempo, Loki or Pyroscope endpoint. Then create a dashboard with a TimeSeriesChart panel whose query targets it.
Two things worth knowing:
- Reachability. The datasource URL is resolved from this instance, so the security group and routing between this instance and your metrics backend must allow it.
- Secrets. Credentials for a datasource go in the project's Secrets tab. They are encrypted at rest with the per instance encryption key generated at first boot, which is why that key never ships inside the image.
Step 16: Production Hardening Checklist
Before exposing Perses beyond a trusted network, work through these:
- TLS termination. Add a certificate and redirect port 80 to 443, either on an Application Load Balancer in front of the instance or on the instance itself. Basic Auth over plain HTTP sends the password reversibly encoded, so terminate TLS before this is reachable over an untrusted network.
- Restrict the security group. Limit inbound TCP 80 and 22 to your office or VPN CIDR rather than leaving them open to the world.
- Rotate or add web users. The gate is a standard bcrypt htpasswd file. Add users with
sudo htpasswd -B /etc/nginx/.perses.htpasswd <user>and reload nginx; rotate theadminentry the same way, then update your records. - Back up
/opt/perses/data. That directory is the entire dashboard store: projects, dashboards, datasources and encrypted secrets. Snapshot the volume or copy the directory on a schedule. - Protect the encryption key.
/etc/perses/perses.envholds the at-rest key for datasource secrets. If you restore/opt/perses/dataonto a different instance, carry that key across or the stored secrets will not decrypt. - Keep it patched. Unattended upgrades is enabled for OS packages. Perses itself is a pinned upstream release under
/opt/perses; review the upstream release notes before replacing the binary.
Architecture
| Component | Path | Notes |
|---|---|---|
| Perses server | perses.service |
Go server with the React interface embedded, bound to 127.0.0.1:8080, runs as the non-login perses user |
| Server config | /etc/perses/config.yaml |
File database backend, enable_auth: false (nginx is the gate) |
| Dashboard store | /opt/perses/data |
Projects, dashboards, datasources and encrypted secrets as JSON |
| Plugin modules | /opt/perses/plugins-archive, /opt/perses/plugins |
24 bundled panel and query modules, extracted at startup |
| CLI | /opt/perses/percli |
Dashboards as code client |
| nginx vhost | /etc/nginx/sites-available/cloudimg-perses |
Reverse proxy on :80, Basic Auth gate, unauthenticated /healthz |
| Basic Auth file | /etc/nginx/.perses.htpasswd |
bcrypt admin entry, generated per instance at first boot (mode 0640 root:www-data) |
| Encryption key | /etc/perses/perses.env |
Per instance at-rest key for datasource secrets (mode 0640) |
| Customer creds | /root/perses-credentials.txt |
Mode 0600 root:root; PERSES_URL, username, password |
| Self-test | /usr/local/sbin/perses-selftest.sh |
Proves gate, demo resources and REST round-trip |
| Firstboot script | /usr/local/sbin/perses-firstboot.sh |
Generates the per instance password and encryption key, writes the credentials file |
| Firstboot sentinel | /var/lib/cloudimg/perses-firstboot.done |
Created when first boot completes |
Support
cloudimg provides 24/7 support for this image. Open a ticket at https://www.cloudimg.co.uk/support/ with your instance ID and a brief description of the issue. Common asks: connecting a first datasource, moving dashboards into version control with percli, adding TLS and a domain, and backing up or migrating the dashboard store.