← Ambasdr Platform Docs
Deployment · Observability

Accessing the container monitoring tools on the DigitalOcean dev box

The dev Droplet runs cAdvisor, Prometheus, and Grafana alongside the app stack, plus Portainer to manage the containers. This page explains what those tools are and which machine they run on, then shows how to actually reach them. Grafana — the dashboards and logs — is reachable in your browser at grafana-dev.ambasdr.com, gated by a Cloudflare Access email one-time PIN (any @juelz.ai address), so day to day you do not need to SSH. Portainer — a browser UI to view, restart, and shell into containers — is published the same email-gated way at portainer-dev.ambasdr.com (and portainer.ambasdr.com for prod). Prometheus and cAdvisor remain loopback-only and are reached with an SSH tunnel for the rare times you need them directly.

01

First, what "the dev box" is

Everything below runs on one machine. Pinning down which machine removes most of the ambiguity in the rest of this page.

"The dev box" is a single cloud server — a DigitalOcean Droplet named ambasdr-dev — that hosts the development / shared instance of the entire Ambasdr stack. It is not your laptop and not production; it is the always-on Linux virtual machine the team points at while building. Everything on it runs as Docker containers managed by one docker compose file at /opt/ambasdr/docker-compose.do-dev.yml: the app (backend, frontend, knowledge-graph), the data stores (postgres, neo4j, minio), the TLS entry point (caddy), the log stack (loki, promtail), the monitoring tools this page is about (grafana, prometheus, cadvisor), and the container-management UI (portainer).

The machine

ambasdr-dev

DigitalOcean Droplet, size s-2vcpu-4gb (2 vCPU, 4 GB RAM, 80 GB disk), Ubuntu 24.04 LTS, region NYC3. It has two public IPs on the one machine — 152.42.153.142 (where dev.ambasdr.com points) and 45.55.86.15. The make do-* targets reach it through the ambasdr-dev SSH alias in ~/.ssh/config (key ~/.ssh/ambasdr_dev_ed25519).

Public hostnames

dev.ambasdr.com

The frontend is served at dev.ambasdr.com and the backend API at api.dev.ambasdr.com, both fronted by Caddy over TLS. Grafana is additionally published at grafana-dev.ambasdr.com through a Cloudflare Tunnel + Access (not Caddy). Prometheus and cAdvisor stay private — see the access note below.

Where it comes from

Two repos

ambasdr-infra (Terraform + cloud-init) creates the droplet, its reserved IP, DNS and firewall. ambasdr (this repo's deploy/) runs the compose stack on it.

Don't confuse it with the other environments

dev = the ambasdr-dev DigitalOcean droplet described here. prod = a separate, larger DigitalOcean droplet (Managed Postgres + Spaces), built later from the same definitions (see the deployment runbook). An earlier temporary staging host — a GMKtec Linux mini-PC on the home LAN exposed via ngrok — has been retired. Whenever this page says "the box," it means ambasdr-dev.

02

What these tools are, and how they fit together

Container monitoring is three cooperating programs, each its own container, defined in deploy/docker-compose.do-dev.yml alongside the existing log stack. The clearest way to understand them is by what each one hands to the next.

cAdvisor (short for "Container Advisor", made by Google) is a small program that measures how much of the machine's resources each running container is using. Concretely: the Linux kernel already tracks per-container counters — CPU time consumed, memory in use, network bytes sent and received — in special files under /sys/fs/cgroup. Every time you start a container the kernel creates a "cgroup" for it and keeps live counters there. cAdvisor reads those kernel counters (that is why its container is given read-only access to host paths like /sys, /var/run, and /var/lib/docker), turns them into per-container numbers labelled with each container's name, and publishes them on an HTTP page at cadvisor:8080/metrics. It keeps no history — it only reports the current values whenever it is asked. It does not run your app, route traffic, or change anything; it only observes.

So the full path the data travels is: kernel counters → cAdvisor reads them and exposes /metrics → Prometheus scrapes that page every 30s and stores it with timestamps → Grafana queries Prometheus and draws the graphs. cAdvisor is the first link; without it, Prometheus would have no per-container data to collect and the dashboard would be empty.

As an intuition on top of that literal description: think of cAdvisor as the meter-reader who walks around noting each container's "electricity usage," Prometheus as the logbook that records those readings over time, and Grafana as the wall chart that plots the logbook for you to read.

cAdvisor

Produces the metrics

Reads the host's cgroup and Docker state read-only and exposes per-container CPU, memory, and network numbers. It is not published — nothing on your laptop talks to it directly; Prometheus scrapes it internally over the compose network at cadvisor:8080. Image gcr.io/cadvisor/cadvisor:v0.49.1, with most metric families disabled to keep memory small on the box.

Prometheus

Stores the metrics

Scrapes cAdvisor (and itself) every 30s and keeps the time-series. Retention is deliberately short — 7d or 1GB, whichever comes first — so it stays a few hundred MB. UI binds to 127.0.0.1:9090 on the box. Image prom/prometheus:v3.1.0.

Grafana

Graphs the metrics

Already running for logs. It boots auto-provisioned with a Prometheus datasource and a "Containers (cAdvisor)" dashboard (in the Ambasdr folder) — no manual import or datasource click-through. UI binds to 127.0.0.1:3001 on the box. Image grafana/grafana:11.4.0.

How each UI is reached

Both UIs still bind only to the box's loopback interface (127.0.0.1:3001 for Grafana, 127.0.0.1:9090 for Prometheus) — no port is opened to the internet. Grafana is published to the team a different way: a cloudflared container makes an outbound-only connection to Cloudflare and forwards grafana-dev.ambasdr.com to grafana:3000 over the internal Docker network, with Cloudflare Access requiring an email one-time PIN before any request reaches it. Prometheus and cAdvisor are deliberately not routed through the tunnel; the only way to them is an SSH port-forward, which the make do-prometheus target sets up.

03

Look at the dashboards (Grafana)

This is the day-to-day path, and it needs no SSH and no make target — just a browser and your @juelz.ai email.

Open the URL

Browse to https://grafana-dev.ambasdr.com. Cloudflare Access intercepts the request first and shows a login screen.

Log in with the email PIN

Enter your @juelz.ai email. Cloudflare emails you a one-time PIN; paste it back. Any address ending in @juelz.ai is allowed (the Access policy on the ambasdr.cloudflareaccess.com org); anyone else is refused before Grafana is ever reached. The session lasts about 30 days (730h), so you rarely re-enter the PIN. (This login — the Cloudflare Access application, its one-time PIN method, and the allow policy — is now defined in ambasdr-infra Terraform, not click-ops; see that repo's "Cloudflare Access, DNS & Credentials" doc.)

Open the Containers dashboard

Once Grafana loads, go to Dashboards → Ambasdr → "Containers (cAdvisor)". It is provisioned from deploy/observability/grafana/provisioning/dashboards/containers.json, so it's there on a fresh boot with no import step.

Same place for logs

Grafana is also the log viewer: the Loki datasource (logs) is the default; Prometheus (metrics) sits alongside it. So the one grafana-dev.ambasdr.com login gets you both the container dashboard and the existing logs.

Break-glass: admin login over SSH

If Cloudflare is unavailable, or you need Grafana's local admin account (e.g. to change a datasource), tunnel to the loopback bind instead: make do-grafana DO_HOST=ambasdr-dev runs ssh -N -L 3001:localhost:3001, then browse to http://localhost:3001 and log in as admin with the GRAFANA_ADMIN_PASSWORD from /opt/ambasdr/.env.host. The browser URL above logs you into the same Grafana, so this is only for the rare admin/outage case.

04

Reading the "Containers (cAdvisor)" dashboard

It defaults to the last 3 hours and auto-refreshes every 30s (which matches the scrape interval — there's no benefit to refreshing faster).

At the top is a container dropdown — a multi-select with an "All" option, populated from the live container names cAdvisor has seen. Use it to focus on one service (e.g. backend, neo4j) or leave it on All. Every panel below filters on this selection.

PanelWhat it shows / how to read it
CPU usage (cores) Per-container CPU as a fraction of a core, averaged over a 5-minute window. 1.0 means one full core; the box is s-2vcpu-4gb, so total headroom is ~2.0 across all containers.
Memory (working set) Per-container working-set memory in bytes. This is the "actively used" memory cAdvisor reports — the number to watch against the 4 GB box total. Neo4j is typically the heaviest.
Network received Inbound bytes/sec per container, rate over 5 minutes.
Network transmitted Outbound bytes/sec per container, rate over 5 minutes.
Container restarts (last 24h) A bar per container that restarted in the last 24h (derived from each container's start time). A bar here is the cue to go read that service's logs — a crash-looping container shows up as repeated restarts.
If a panel is empty

"No data" usually means the metric simply hasn't been scraped yet (give it a scrape interval or two), the selected time range predates the container starting, or the container dropdown is filtered to a name that isn't running. Confirm the pipeline is healthy on the Prometheus targets page (the Prometheus section below) before assuming the dashboard is broken.

05

View logs (Loki) in Grafana

Logs and metrics live in the same Grafana. You read container logs with LogQL through the Loki datasource — there is no separate Loki UI, and you never query Loki directly.

Open the same Grafana tunnel as above (make do-grafanahttp://localhost:3001), then go to Explore (the compass icon) and pick the Loki datasource (it is the default). Promtail discovers every container through the Docker socket and ships its stdout/stderr to Loki with labels attached automatically, so any running container is queryable with no per-service setup.

The labels Promtail attaches — these are what you filter on:

LabelMeaning
compose_serviceThe docker-compose service name: backend, frontend, knowledge-graph, neo4j, etc. The one you'll filter on most.
container_nameThe full container name, e.g. ambasdr-backend-1.
levelLog level where Promtail could parse one (info, warn, error, ...).
streamstdout or stderr.

Example LogQL queries (paste into the Explore query box):

# All backend logs
{compose_service="backend"}

# Backend logs containing "error" (case-sensitive substring match)
{compose_service="backend"} |= "error"

# Only error-level lines across backend + knowledge-graph
{compose_service=~"backend|knowledge-graph", level="error"}

# One specific container, dropping noisy health-check lines
{container_name="ambasdr-backend-1"} != "/health"

# Rate of error lines per service over 5m (a metric derived from logs)
sum by (compose_service) (rate({level="error"}[5m]))
Labels vs. line filters

Anything inside { } is an indexed label match (fast, and the part Loki narrows on first). |= (contains), != (doesn't contain), and |~ / !~ (regex) are line filters applied afterward. Always include at least one label matcher — an empty {} query is rejected. Retention follows the same light footprint as metrics; older logs age out.

06

Query raw metrics & check scrape health (Prometheus)

Reach for Prometheus directly when you want to run an ad-hoc PromQL query, or to confirm the metrics pipeline itself is healthy.

Open the tunnel

make do-prometheus DO_HOST=root@<reserved-ip>

Runs ssh -N -L 9090:localhost:9090 $(DO_HOST). Then browse to http://localhost:9090. No login.

Confirm scrapes are working

Go to Status → Targets. You should see two jobs both UP: cadvisor (target cadvisor:8080) and prometheus (target localhost:9090). If cadvisor is down, the dashboard will be empty — that's the first thing to check.

Run a query

Use the Graph tab. These are the exact expressions the Grafana panels use, so they're a good starting point:

# CPU cores per container
sum by (name) (rate(container_cpu_usage_seconds_total[5m]))

# Working-set memory per container (bytes)
sum by (name) (container_memory_working_set_bytes)
07

Manage the containers (Portainer)

Grafana, Prometheus, and cAdvisor tell you how much each container is using; they don't let you act on one. Portainer is the companion for that — a browser UI to see every container's state, read its logs, restart it, or open a shell — reached the same email-gated way as Grafana.

Portainer runs as one more container on the box: the portainer service (image portainer/portainer-ce:lts) in deploy/docker-compose.do-<env>.yml. It is a full Portainer Server, not the older agent: it talks to the box's Docker straight through the mounted socket (-H unix:///var/run/docker.sock), so that box's containers, stacks, volumes, and networks show up automatically as the "local" environment with no wiring. This replaced the previous model — an on-box portainer-agent reached from a Portainer Server on your laptop over an SSH tunnel — so there is now one hosted Server per environment and the laptop Server is gone.

dev

portainer-dev.ambasdr.com

Manages the ambasdr-dev box. Published through that box's ambasdr-dev Cloudflare tunnel.

prod

portainer.ambasdr.com

Manages the prod box, through the ambasdr-prod tunnel. Same @juelz.ai email-PIN policy.

How it's published

Tunnel, not a port

Bound to 127.0.0.1:9000 on the box; the same cloudflared container forwards the hostname to portainer:9000. No inbound port is opened.

Reaching Portainer means passing two separate logins, owned by two different systems, in order. This is the part that trips people up, so it's worth being explicit — the email PIN is not the Portainer login:

GateSystemWhat it checksWho sets it up
1. Email PIN Cloudflare Access — at Cloudflare's edge, before the request reaches the box Intercepts the request, emails a one-time PIN, and admits any @juelz.ai address (the allow policy is the email domain). Issues a ~30-day session cookie. Nobody, per-person. Anyone with a @juelz.ai email is admitted automatically — there is no account to pre-create.
2. Username + password Portainer's own login, inside the app A completely separate credential. Portainer only knows the users in its own database (the portainer_data volume); clearing the email gate does not log you in here. The Portainer admin creates one user per teammate in Settings → Users.

So a teammate's first visit is:

Open the URL

Browse to https://portainer.ambasdr.com (or portainer-dev.ambasdr.com). Cloudflare Access intercepts it first.

Clear the email gate

Enter your @juelz.ai email and paste back the one-time PIN Cloudflare emails you. This is the same gate as Grafana — if you already have a session from Grafana, you pass straight through.

Log in to Portainer

Now Portainer's own login appears. Enter the username + password the admin created for you under Settings → Users. (On a brand-new box the very first step is instead the admin creating their own account on the "New Portainer installation" screen.)

Break-glass: reach Portainer over SSH

If Cloudflare is unavailable — or for the very first admin setup on a fresh box — tunnel to the loopback bind instead: make do-portainer DO_HOST=ambasdr-<env> runs ssh -N -L 9000:localhost:9000, then browse to http://localhost:9000. The admin account and users persist in the portainer_data volume, so this is a one-time setup per box.

08

Operating notes & footprint

Quick reference

Grafana (team) → https://grafana-dev.ambasdr.com → email PIN (@juelz.ai) → Dashboards → Ambasdr → Containers (cAdvisor).
Grafana (admin/break-glass) → make do-grafana DO_HOST=ambasdr-devhttp://localhost:3001 (admin / GRAFANA_ADMIN_PASSWORD).
Prometheus → make do-prometheus DO_HOST=ambasdr-devhttp://localhost:9090 → Status → Targets.
Portainer (team) → https://portainer-dev.ambasdr.com / portainer.ambasdr.com → email PIN (@juelz.ai) → Portainer user login.
Portainer (break-glass) → make do-portainer DO_HOST=ambasdr-devhttp://localhost:9000.