Playbook: Deploy mailgun-pipeline-exporter to bms-1
Status: STABLE (executed successfully 2026-07-11, issue #3688)
Trigger
Standing up mailgun-pipeline-exporter (issue #3688, PR #3705) on bms-1 for the first time,
or redeploying it after a code change to monitoring/exporters/mailgun-pipeline-exporter/.
Pre-conditions
- PR adding/changing the exporter is merged to
main. bms-1/.env.examplein this repo lists the required env keys.MAILGUN_ADMIN_API_KEY,MAILGUN_PIPELINE_MONGODB_URI, and (since #3752)P24_AUTOMATION_KEYare delivered by CI — do not place them by hand (#4657, #3752).secrets-sync.yml’ssync-bms-1job decrypts them from SOPS and upserts them into/opt/mezmo-agent/.envon every secrets push:Key SOPS source MAILGUN_ADMIN_API_KEYsecrets/monitoring.env.sopsMAILGUN_PIPELINE_MONGODB_URIsecrets/pinbox24-w4.env.sops→V42_NEW_MONGODB_URI(fallback; the job prefersMAILGUN_MONGODB_URLinsecrets/pinbox24-backends.env.sopsautomatically once #3850 adds it there)P24_AUTOMATION_KEY(#3752, Leg 1/Leg 3 CF Worker token minting)secrets/pinbox24-w4-auth.env.sopsA secret-manageroperation is only needed to rotate a value in SOPS, never to place oneon the host. If a key ever needs restoring on bms-1, re-run the sync rather than editing the file: gh workflow run secrets-sync.yml --repo radieu/p24-infra -f target=bms-1.The MAILGUN_E2E_LEG{1,3}_ENABLED/MAILGUN_E2E_LEG3_WEBHOOK_URLflags are plain(non-secret) config in .env, NOT synced by CI — seedocs/mailgun-e2e-probe-operations.md.
Key fact: repo path vs. live server path
bms-1/docker-compose.yml in this repo is the source of truth for what’s deployed live at
/opt/mezmo-agent/docker-compose.yml on bms-1 (94.23.26.113) — NOT
/opt/p24-infra/bms-1/ (that directory on bms-1 is just a staging area for per-service
.env files dropped by secrets-sync.yml/manual placement, not a compose project root).
The compose file’s build: ../monitoring/exporters/<name> and env_file: - .env are both
relative to /opt/mezmo-agent/ on the live host, so:
- exporter source must land at
/opt/monitoring/exporters/<name>/(sibling ofmezmo-agent/) - all env keys (including pre-existing
LOGDNA_AGENT_KEYfor mezmo-agent) share ONE/opt/mezmo-agent/.envfile — do not create a separate per-exporter env file here. - that shared file is written by CI.
secrets-sync.yml’ssync-bms-1job rewrites it on any secrets push. Since #3850 it merges — it upsertsLOGDNA_AGENT_KEYand leaves every other key alone. Before #3850 it>-truncated the file to that single line, so any hand-placed exporter key was deleted on the next run; because the job recreatesmezmo-agentonly, the exporter kept its old env until it was next recreated, making the loss invisible for as long as nothing touched the container. - Since #4657 the job also upserts the exporter’s own two keys from SOPS, and recreates
mailgun-pipeline-exporter(not justmezmo-agent) when they change. That closes the gap that made this recurrent: the #3850 merge preserved unrelated keys correctly, but nothing ever put the exporter keys there, so once the pre-#3850 truncation had removed them they never came back. The tell is the job’s own log line —env merged (49 bytes, 1 keys)means onlyLOGDNA_AGENT_KEYis present and the exporter is running blind; a healthy sync reports 3 keys. A key that fails to decrypt is skipped, never written empty, so a SOPS miss cannot blank a working on-host value. LOGDNA_AGENT_KEYmust stay the last line and must not end with a newline (compose v1 folds it into the value → 33-char key). The sync job maintains that ordering; preserve it if you edit by hand.
Known issue: bms-1’s Docker Engine is severely outdated (20.10.2)
Confirmed 2026-07-11: docker version --format '{{.Server.Version}}' on bms-1 returns
20.10.2 (released ~Feb 2021). That version’s default seccomp profile predates support for
the clone3 syscall, which glibc >= 2.34 (Debian bookworm/trixie) uses internally. The
symptom is a build failure at apt-get update inside any container build using a
bookworm/trixie-based image (python:3.13-slim, python:3.13-slim-bookworm, even bare
debian:trixie-slim all reproduce it identically):
E: Problem executing scripts APT::Update::Post-Invoke 'rm -f /var/cache/apt/archives/*.deb ...'
E: Sub-process returned an error code
Running the same rm -f ... command directly (outside apt’s internal script-runner) succeeds
fine — the failure is specific to how apt’s Post-Invoke hook is spawned, consistent with a
blocked clone3 syscall under the container’s seccomp profile.
Workaround (in place for this exporter): pin the exporter’s Dockerfile FROM to
python:3.11-slim-bullseye (glibc 2.31, predates the clone3 dependency). Confirmed working
on bms-1 2026-07-11.
This affects every future build on bms-1, not just this exporter — any new/changed Dockerfile targeting bms-1 needs a bullseye-or-older base until bms-1’s Docker Engine is upgraded. See issue tbd (filed alongside this playbook) for the tracked upgrade decision — this is a separate, carefully-scheduled maintenance task on a production host (v42-prod, v32-prod, mailgun-v42-prod all run there) and should not be done opportunistically.
Steps
-
Copy updated compose file:
scp -i <ssh-key> bms-1/docker-compose.yml root@94.23.26.113:/opt/mezmo-agent/docker-compose.yml -
Copy exporter source (mkdir -p first;
/opt/monitoring/exporters/<name>/is a sibling of/opt/mezmo-agent/):ssh ... "mkdir -p /opt/monitoring/exporters/mailgun-pipeline-exporter" scp monitoring/exporters/mailgun-pipeline-exporter/{app.py,Dockerfile,requirements.txt} \ root@94.23.26.113:/opt/monitoring/exporters/mailgun-pipeline-exporter/ -
Merge the new exporter’s env keys into the existing
/opt/mezmo-agent/.env(append, don’t overwrite —LOGDNA_AGENT_KEYmust survive). Nevercat/print the file to your own terminal.Since #4657, prefer CI for
mailgun-pipeline-exporterspecifically — rungh workflow run secrets-sync.yml --repo radieu/p24-infra -f target=bms-1and skip this step. The manual merge below remains the pattern for a new exporter whose keyssync-bms-1does not yet deliver; wire such keys into that job in the same PR, or they will be lost on the next sync exactly as 4657 were.Pitfall hit during execution: if the destination file has no trailing newline (check with
wc -lvswc -c— a non-zero byte count withwc -lreturning 0 means no trailing newline), a plaincat new >> existingconcatenates the first appended line onto the end of the last existing line, corrupting BOTH (the existing key’s value gets garbage appended, and the new key becomes unparseable). Always rebuild explicitly instead:{ cat existing.env; printf '\n'; cat new-keys.env; printf '\n'; } > merged.env.new \ && mv merged.env.new existing.env && chmod 600 existing.envVerify with
wc -l,grep -c '=', andcut -d= -f1(key names only) — never print values. Take a timestamped backup of the destination file before any merge. -
Open ufw for the exporter’s port, source-restricted to vps-i1 only:
ufw allow from 217.154.82.162 to any port 9251 proto tcp comment 'mailgun-pipeline-exporter <- vps-i1' -
Build + start (from
/opt/mezmo-agent/):docker-compose up -d --build mailgun-pipeline-exporterNote: even naming the specific service,
docker-composemay still recreatemezmo-agenttoo if the shared.envfile’s content changed (it hashes the resolved config per service). This happened during execution —mezmo-agentrestarted cleanly with zero data loss (verified viadocker inspect --format '{{.RestartCount}}'== 0, i.e. it wasn’t crash-looping, and its logs showed successful log-shipping resuming immediately). Treat this as an accepted, low-risk side effect, not a bug to prevent. -
Verify:
docker ps --filter name=mailgun-pipeline-exporter --format '{{.Names}} {{.Status}}' docker inspect <container> --format '{{.State.Health.Status}}' # wait for "healthy" curl -s http://localhost:9251/metrics | head -5 # from bms-1 itselfFrom vps-i1, confirm Prometheus can reach it:
curl -s 'http://localhost:9090/api/v1/targets?state=active' | \ python3 -c "import sys,json; d=json.load(sys.stdin); [print(t['labels']['job'], t['health']) for t in d['data']['activeTargets'] if t['labels'].get('job')=='mailgun_pipeline']"Expect
mailgun_pipeline up. -
Log the operation (see
.claude/task-playbooks/server-operation.md§Mandatory post-flight).
Common mistakes to avoid
- Assuming
/opt/p24-infra/bms-1/is the compose project root — it isn’t; it’s an env-staging directory only. - Creating a separate
.envfile for the new exporter instead of appending to the existing shared/opt/mezmo-agent/.env. This actually happened (#3817, 2026-07-11): the keys were dropped into/opt/p24-infra/bms-1/mailgun-pipeline-exporter.env(the staging dir), never merged into/opt/mezmo-agent/.env, and the compose still hadenv_file: - .env, so the container ran with onlyPORTset andMailgunPipelineExporterErrorsfired on both sources. Symptom to recognise:docker inspect <container> --format '{{.Config.Env}}'shows the exporter’s keys absent while a populated file exists elsewhere on disk. Fix = merge into the shared.env+docker-compose up -d --force-recreate(env_file is only re-read on recreate). - Appending to a shared env file with plain
cat >>without checking for a missing trailing newline first — see the pitfall in Step 3. - Hand-placing a key that no CI job delivers. The keys then live in exactly one place — a file
that CI rewrites — with no source of truth to restore them from. This is what made #3817 recur as
#4657 on 2026-07-30: the keys were gone from
/opt/mezmo-agent/.envfor at least a day (four consecutive syncs logged1 keys) before a routinedocker-compose up -drecreated the exporter and surfaced it. Because the exporter’s “env not set” path returns{}rather than zeros, its data series went absent, which also silently disarmed thepinbox24.ymlpipeline-integrity alert that depends on them — a monitoring blind spot, not a noisy alert. Always wire a new key intosync-bms-1in the same PR that introduces it. - Assuming the current base image (
python:3.13-slimor similar) will build on bms-1 without checking Docker Engine compatibility first — see “Known issue” above.
Related
docs/playbooks/add-new-monitoring-exporter.md— general “add a new exporter” patterndocs/playbooks/mailgun-mongodb-stale-credential-hang.md— incident this exporter detects