Files
meshcore-analyzer/DEPLOY.md
T
750b8742a7 fix(staging-compose): decouple in-container mosquitto from standalone broker (#1813)
Red commit: `3898dbc5` (verified locally — CI run URL pending)

## Problem

A standalone `mqtt-broker` container (`eclipse-mosquitto:2`) was
provisioned out-of-band on the staging VM. It now owns MQTT, is attached
to external docker network `meshcore-net`, and binds host port `8883`.
The current `docker-compose.staging.yml` still:

- Publishes `1883:1883` on the host (dead weight; conflicts the moment
the broker moves to that port).
- Defaults `DISABLE_MOSQUITTO=false`, so the in-container mosquitto
burns RAM and briefly contests the `mqtt-broker` docker DNS name on cold
start.
- Doesn't join `meshcore-net`, so the ingestor can't resolve
`mqtt-broker:1883` via docker DNS without manual surgery.

## Fix (`docker-compose.staging.yml` only)

1. Remove the `1883:1883` host port publish from `staging-go`.
2. Flip `DISABLE_MOSQUITTO` default from `false` to `true`. Operators
can opt back in with the env var.
3. Attach `staging-go` to both `default` and `meshcore-net`; declare
`meshcore-net` as `external: true` so the file never tries to
create/destroy operator state.

Healthcheck and Caddy/443 plumbing untouched (out of scope).

## Test added (TDD framing: Option A — Go shape-asserts)

`cmd/server/staging_compose_broker_test.go:1` adds four regex-based
assertions on the compose file shape:

- staging-go does **not** bind port `1883` in ANY form (quoted/unquoted
short form, or long-form `target: 1883` / `published: 1883`).
- `DISABLE_MOSQUITTO` uses the interpolated default form
`${DISABLE_MOSQUITTO:-true}` (preserves operator override). Bare literal
`true`, or a later `=false` override in the same env block, is rejected.
- Top-level `networks:` declares `meshcore-net` as `external: true`.
- `staging-go` attaches to `meshcore-net` via a real
`services.staging-go.networks:` sub-key (comment-stripped so an
in-comment example can't masquerade).

Regex (not YAML byte-equality) so cosmetic edits don't break the guard.
No new go module deps. Red commit `3898dbc5` fails all 4 assertions on
master. Green commit `38297ff4` makes them pass. Round-1 hardening
commit `9f7155e2` tightens the regexes (per adversarial + kent-beck
must-fixes) and was verified against master's YAML shape — all 4 tests
fail on `origin/master`'s compose, pass on branch, proving the tightened
regexes still gate a real regression.

## Risk

Low, with one intentional semantic change.

- **Semantic change (v3.7+):** `DISABLE_MOSQUITTO` in
`docker-compose.staging.yml` now defaults to `true`. This is a
**deliberate flip** — the standalone `mqtt-broker` container is now
authoritative on the staging host, and running the in-container
mosquitto alongside it wastes RAM and races the docker DNS name
`mqtt-broker` on cold start. Operators who want the pre-v3.7 shape
(in-container mosquitto + host-published `1883`) must explicitly opt
back in via env override AND re-add the `1883:1883` port mapping
(concrete snippet is inline in the compose file and in `DEPLOY.md` under
"Standalone MQTT broker (staging)"). This intent is called out in a
`SEMANTIC CHANGE (v3.7+)` header comment at the top of
`docker-compose.staging.yml`.
- **Deploy prereq:** the external `meshcore-net` docker network MUST
already exist on the host before `docker compose up`. If it doesn't,
compose refuses to start `staging-go`. This is documented inline in the
compose file (with the `docker network create meshcore-net` one-liner)
and in `DEPLOY.md`.
- **Only takes effect where the standalone broker is deployed** — which
it already is on staging today. The legacy `DISABLE_MOSQUITTO=false`
path remains reachable via env override; the ingestor's upstream config
is untouched.

Partial fix — no tracking issue; follow-up to operator-side broker
provisioning.

---------

Co-authored-by: corescope-bot <bot@corescope.local>
Co-authored-by: openclaw-bot <bot@openclaw.local>
2026-06-30 18:21:21 -07:00

9.1 KiB

Deploy CoreScope

Pre-built images are published to GHCR for linux/amd64 and linux/arm64 (Raspberry Pi 4/5).

Quick Start

Docker run

docker run -d --name corescope \
  -p 80:80 \
  -v corescope-data:/app/data \
  -e DISABLE_CADDY=true \
  ghcr.io/kpa-clawbot/corescope:latest

Open http://localhost — done.

Docker Compose

curl -sL https://raw.githubusercontent.com/Kpa-clawbot/CoreScope/master/docker-compose.example.yml \
  -o docker-compose.yml
docker compose up -d

Image Tags

Tag Description
v3.4.1 Pinned release (recommended for production)
v3.4 Latest patch in v3.4.x
v3 Latest minor+patch in v3.x
latest Latest release tag
edge Built from master — unstable, for testing

Configuration

Settings can be overridden via environment variables:

Variable Default Description
DISABLE_CADDY false Skip internal Caddy (set true behind a reverse proxy)
DISABLE_MOSQUITTO true in docker-compose.staging.yml; false elsewhere Skip internal MQTT broker. Default flipped to true for the staging deploy in v3.7+ because a standalone mqtt-broker container owns MQTT on that host — see "Standalone MQTT broker (staging)" below.
HTTP_PORT 80 Host port mapping
DATA_DIR ./data Host path for persistent data

For advanced configuration, mount a config.json into /app/data/config.json. See config.example.json in the repo.

Updating

docker compose pull
docker compose up -d

Data

All persistent data lives in /app/data:

  • meshcore.db — SQLite database (packets, nodes)
  • config.json — custom config (optional)
  • theme.json — custom theme (optional)

Backup: cp data/meshcore.db ~/backup/

TLS

Option A — External reverse proxy (recommended): Run with DISABLE_CADDY=true, put nginx/traefik/Cloudflare in front.

Option B — Built-in Caddy: Mount a custom Caddyfile at /etc/caddy/Caddyfile and expose ports 80+443.


Standalone MQTT broker (staging)

Starting in v3.7, docker-compose.staging.yml assumes a standalone mqtt-broker container (image: eclipse-mosquitto:2) already runs on the staging VM, out-of-band from this repo. That container:

  • Owns port 8883 externally (TLS-terminated MQTT for real observers).
  • Is attached to a shared docker network named meshcore-net.
  • Is operator-managed state — it is not defined in any compose file in this repository. Its config, TLS certs, and ACLs live on the host, outside git.

corescope-staging-go reaches it in-network at mqtt-broker:1883 via docker DNS (no host port hop). To make that work, docker-compose.staging.yml joins the external meshcore-net network and defaults DISABLE_MOSQUITTO=true so the built-in mosquitto stays off.

Prereq — one-time provisioning on the staging host

docker network create meshcore-net
# ...then bring up the operator-managed mqtt-broker container on that
# network (not covered here; that's operator state). THEN:
docker compose -f docker-compose.staging.yml up -d

If meshcore-net doesn't exist when compose starts, docker will refuse to bring staging-go up (external: true — compose won't create it).

Reverting to the old single-container behaviour

Third-party operators cloning this repo who want the legacy shape (in-container mosquitto + 1883:1883 on the host, no external broker) should override both the env default and re-add the port mapping.

In .env (or the shell):

DISABLE_MOSQUITTO=false

And in docker-compose.staging.yml, restore the 1883:1883 mapping under services.staging-go.ports:

    ports:
      - "${STAGING_GO_HTTP_PORT:-80}:80"
      - "${STAGING_GO_MQTT_PORT:-1883}:1883"   # ← re-added
      - "6060:6060"
      - "6061:6061"

That gives you back the pre-v3.7 self-contained staging shape. In that mode you do not need meshcore-net, but note the compose file still declares it as external: true, so either remove that declaration in your fork or ensure the network exists.


Migrating from manage.sh (existing admins)

If you're currently deploying with manage.sh (git clone + local build), you have two options going forward:

Option A: Keep using manage.sh (no changes needed)

manage.sh update continues to work exactly as before — it fetches the latest tag, builds locally, and restarts. Nothing breaks.

./manage.sh update          # latest release
./manage.sh update v3.5.0   # specific version

Pre-built images skip the build step entirely — faster updates, no Go toolchain needed.

One-time migration:

  1. Stop the current deployment:

    ./manage.sh stop
    
  2. Your data is in ~/meshcore-data/ (or whatever PROD_DATA_DIR is set to). It's untouched — the database, config, and theme files persist.

  3. Copy docker-compose.example.yml to where you want to run from:

    cp docker-compose.example.yml ~/docker-compose.yml
    
  4. Start with the pre-built image:

    cd ~ && docker compose up -d
    
  5. Verify it picked up your existing data:

    curl http://localhost/api/stats
    

Updates after migration:

docker compose pull && docker compose up -d

What about manage.sh features?

manage.sh command Pre-built equivalent
./manage.sh update docker compose pull && docker compose up -d
./manage.sh stop docker compose down
./manage.sh start docker compose up -d
./manage.sh logs docker compose logs -f
./manage.sh status docker compose ps
./manage.sh setup Copy docker-compose.example.yml, edit env vars

manage.sh remains available for advanced use cases (building from source, custom patches, development). Pre-built images are recommended for most production deployments.

Staging VM — disk-usage monitor & cleanup (#1684)

The staging VM ran out of disk during a hot-patch (#1684). To prevent repeats, two scripts live in scripts/staging/:

  • disk-monitor.sh <mount> — reads df -P, classifies usage against <80 ok / >=80 warn / >=90 error / >=95 alert, emits to stderr + journald (via logger). Returns non-zero on error|alert so systemd surfaces the unit as failed.
  • disk-cleanup.sh — removes /tmp snapshot files (*.db, staging-snap.*, cs-*, node-compile-cache) older than 7 days and runs docker builder prune + docker image prune with --filter "until=72h" --filter "label!=keep". Set CORESCOPE_CLEANUP_DRY_RUN=1 to log without deleting.

Install on the staging host

SSH to <STAGING_HOST> as the staging operator user and:

sudo install -m 0755 scripts/staging/disk-monitor.sh  /usr/local/bin/corescope-disk-monitor
sudo install -m 0755 scripts/staging/disk-cleanup.sh  /usr/local/bin/corescope-disk-cleanup

# 15-minute monitor
sudo tee /etc/systemd/system/corescope-disk-monitor.service >/dev/null <<'UNIT'
[Unit]
Description=CoreScope staging disk-usage monitor (issue #1684)
[Service]
Type=oneshot
ExecStart=/usr/local/bin/corescope-disk-monitor /
UNIT

sudo tee /etc/systemd/system/corescope-disk-monitor.timer >/dev/null <<'UNIT'
[Unit]
Description=Run CoreScope disk-usage monitor every 15 minutes
[Timer]
OnBootSec=5min
OnUnitActiveSec=15min
Unit=corescope-disk-monitor.service
[Install]
WantedBy=timers.target
UNIT

# Daily cleanup at 03:30 local
sudo tee /etc/systemd/system/corescope-disk-cleanup.service >/dev/null <<'UNIT'
[Unit]
Description=CoreScope staging disk cleanup (issue #1684)
[Service]
Type=oneshot
ExecStart=/usr/local/bin/corescope-disk-cleanup
UNIT

sudo tee /etc/systemd/system/corescope-disk-cleanup.timer >/dev/null <<'UNIT'
[Unit]
Description=Run CoreScope disk cleanup daily at off-peak
[Timer]
OnCalendar=*-*-* 03:30:00
Persistent=true
Unit=corescope-disk-cleanup.service
[Install]
WantedBy=timers.target
UNIT

sudo systemctl daemon-reload
sudo systemctl enable --now corescope-disk-monitor.timer corescope-disk-cleanup.timer

<STAGING_HOST> is the staging VM hostname/IP — operator supplies it, not committed to the repo.

Inspecting alerts

journalctl -t corescope-disk-monitor   --since '-1d'
journalctl -t corescope-disk-cleanup   --since '-7d'
systemctl list-timers | grep corescope-disk

logger priorities map: ok→info, warn→warning, error→err, alert→alert (syslog severity 1, the highest level). Wire journalctl -p alert ... to whatever ops channel the operator prefers; use -p err to also catch the error tier.

Notes on staging-snap.db root cause (#1684 phase 3)

grep -rn staging-snap.db cmd/ public/ scripts/ returns zero hits in the repo. The 4.4 GB orphan was a manual debugging artifact, not produced by any committed code. The disk-cleanup.sh retention rule (anything matching staging-snap.* in /tmp older than 7 days) prevents recurrence without needing source-side TTL changes.

If a future feature legitimately needs persistent snapshot DBs, put them under /var/lib/corescope/snapshots/ with explicit rotation — not in /tmp, which is ephemeral by definition.