For LLM agents
Operator cheat sheet for the live VM. SSH access is via Tailscale only; the user
opshas passwordless sudo. All app containers are managed by systemd units namedapps-<app_name>.{service,timer}and live in/opt/apps/<app_name>/.
Connect
ssh ops@polymarket-infra(Or ssh ops@ops-vm-1 if the rename’s Tailscale cleanup isn’t done yet — see gcp-terraform-ansible-gotchas #15.)
Use /usr/bin/ssh if your shell wraps the ssh command in some way (zsh aliases, etc.).
Quick status — all apps
sudo systemctl list-units 'apps-*' --no-pagerOutput is a table of unit name, load state (loaded/not-found), active state (active/failed/inactive), sub-state (running/dead/start-pre/…).
For containers specifically:
sudo docker ps --filter 'name=apps-' --format 'table {{.Names}}\t{{.Status}}\t{{.Image}}'Healthy steady-state (post Spec 2a+2b, ~2026-05-27):
NAMES STATUS IMAGE
apps-polymarket-fetch-bot Up XX .../polymarket-fetch:latest
apps-polymarket-fetch-wallet-monitor Up XX .../polymarket-fetch:latest
apps-pm-arb-validate Up XX .../pm-arb:latest
(apps-polymarket-fetch-scheduled-run is a timer-driven one-shot — its container only exists while the job is running, every 30 min.)
Inspect one app
APP=polymarket-fetch-bot
sudo systemctl status apps-$APP.service
sudo docker logs --tail 50 apps-$APP # last 50 lines
sudo docker logs -f apps-$APP # follow live
sudo docker exec apps-$APP env | grep ^OTEL_ # confirm OTel auto-injectionFor a kind: job:
APP=polymarket-fetch-scheduled-run
sudo systemctl list-timers apps-$APP.timer --no-pager # next scheduled fire
sudo journalctl -u apps-$APP.service --since '1 hour ago' --no-pagerjournalctl shows the systemd unit’s stdout (the docker compose run --rm app invocation’s output). For services that have logged a lot, prefer docker logs over journalctl — the latter shows ExecStart wrapping noise.
Bounce a single app
sudo systemctl restart apps-$APP.serviceThis re-runs ExecStartPre=docker pull then docker compose up -d. If the AR image’s :latest has been updated, you’ll get the new digest. The unit’s state: restarted behavior baked into the Ansible role is also systemctl restart-equivalent.
For a clean teardown without removing the unit:
sudo systemctl stop apps-$APP.service
sudo docker compose -f /opt/apps/$APP/docker-compose.yml down(Stops the unit AND removes the container. start again brings it back.)
Inspect compose + env
Every app’s working directory:
ls /opt/apps/$APP/
# docker-compose.yml .env.env is rendered from secrets/<app>.env in the terraform repo with mode 0600 and ownership ops:ops. The compose file is templated by Ansible from ansible/roles/apps/templates/docker-compose.yml.j2.
Don’t hand-edit either — re-run make deploy APP=<name> (or gh workflow run deploy-apps.yml ... -f app=<name>) to re-render from source. Any edits get clobbered on next deploy.
OTel collector health
curl -s http://127.0.0.1:8888/metrics | grep -E '^otelcol_(receiver_accepted|exporter_sent|exporter_send_failed)_(log_records|metric_points|spans)' | head -10Three pairs to watch: logs/metrics/spans. accepted should equal sent. send_failed should be absent or 0. See Verifying signals are flowing for the canonical signal.
Tailscale on the VM
sudo systemctl status tailscaled
sudo tailscale status | head -5
sudo tailscale netcheck # UDP + DERP latency
sudo cat /etc/default/tailscaled # shows the --debug=127.0.0.1:5252 flag for Prometheus metrics
curl -s http://127.0.0.1:5252/debug/metrics | grep -E '^(goroutines|magicsock_|derp_)' | head -10If the tailnet hostname collides (see gcp-terraform-ansible-gotchas #15), check tailscale status from your laptop AND from the VM (sudo tailscale status). The VM’s own view shows it as <hostname> (no suffix); peers see whatever Tailscale assigned (may have -1).
Docker resource budget
free -h # host RAM
sudo docker stats --no-stream --format 'table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.MemPerc}}'
df -h /Reference for polymarket-infra (e2-small, 2GB):
| State | Used MEM | Available |
|---|---|---|
| Idle (just collector + tailscale + dockerd) | ~350 MB | ~1.5 GiB |
| 4 services running steady-state | ~650 MB | ~1.3 GiB |
| Scheduled-run firing | spikes by ~150 MB for ~30s | dips to ~1.1 GiB |
available dropping below ~200 MB is when to consider e2-medium (4 GB) — see Resizing.
Common debug paths
”App is up but not doing anything”
docker logs --tail 100 apps-$APP— recent activity. Look for the startup brief, then steady-state work.- SigNoz logs view, filter
service.name = $APP, sort by timestamp — see what the app thinks it’s doing. - Supabase MCP
execute_sqlto check whether DB writes are happening:SELECT count(*), MAX(<timestamp_col>) FROM pm_<table>for whichever table the app feeds.
”App keeps crash-looping”
sudo systemctl status apps-$APP.serviceshows the most recent exit code + restart count.sudo docker logs --tail 50 apps-$APPshows the last error before crash.- Common: Supabase unreachable (IPv6/IPv4 issue — see gcp-terraform-ansible-gotchas #18), Tokio runtime panic (Rust app’s init order — see gcp-terraform-ansible-gotchas #21), or env var missing.
”Service shows active but container shows Restarting”
The systemd unit succeeded its ExecStart (the docker compose up -d call returned 0 even though the container then exited). Use docker logs and docker ps to see the actual container state. systemctl is-active is unreliable for compose-managed containers.
”Trace gap in SigNoz”
- Was the service bounced recently? Check
docker psStatus column for “Up X seconds”. - Did the OTel collector restart?
sudo journalctl -u otelcol-contrib -n 50 --no-pager. - Did the network drop?
sudo journalctl -u tailscaled -n 50 --no-pager | grep -iE 'derp|magicsock'.
Logs by service in SigNoz
The structured-logging conversion (gcp-terraform-ansible-gotchas #22) means logs are filterable by attribute. Useful queries:
| Goal | SigNoz logs query |
|---|---|
| All logs from one service in last hour | service.name = '<app_name>' |
| Errors only | service.name = '<app_name>' AND severity_text = 'ERROR' |
| Slow queries | severity_text = 'WARN' AND body CONTAINS 'slow statement' |
| Single Telegram chat’s activity | service.name = 'polymarket-fetch-bot' AND chat_id = '-...' |
| pm-arb signals for one asset | service.name = 'pm-arb-validate' AND slug LIKE '%btc-updown%' |
Restart EVERYTHING
make redeploy-all from operator laptop OR gh workflow run deploy-apps.yml ... -f app=all from anywhere. Bounces all kind: service apps. Timers re-fire at their next OnCalendar tick.
Related
- agent-guide-configure-app-deploy — deploying NEW apps
- agent-guide-redeploy-app — image bump
- agent-guide-remove-app — clean teardown
- observability-flow — full OTel pipeline
- gcp-terraform-ansible-gotchas
- spec-1-operations-runbook — Spec 1-era ops (still valid for VM-level concerns)
- levandor-infra