- Added HTTPS Options section: auto (Caddy), bring your own cert, Cloudflare Tunnel, behind existing proxy, HTTP-only - Expanded MQTT Security into its own section with 3 options + recommendation - Fixed DB backup to use volume path not docker cp - Added restore instructions - Expanded troubleshooting table (rate limits → use own cert or different subdomain) - Clarified that MQTT 1883 is NOT exposed by default in quick start - Added tip to save docker run as a script - Restructured for cleaner TOC - Removed condescension, kept clarity
14 KiB
Deploying MeshCore Analyzer
Get MeshCore Analyzer running with automatic HTTPS on your own server.
Table of Contents
- What You'll End Up With
- What You Need Before Starting
- Installing Docker
- Quick Start
- Connecting an Observer
- HTTPS Options
- MQTT Security
- Database Backups
- Updating
- Customization
- Troubleshooting
- Architecture Overview
What You'll End Up With
- MeshCore Analyzer running at
https://your-domain.com - Automatic HTTPS certificates (via Let's Encrypt + Caddy)
- Built-in MQTT broker for receiving packets from observers
- SQLite database for packet storage (auto-created)
- Everything in a single Docker container
What You Need Before Starting
A server
A computer that's always on and connected to the internet:
- Cloud VM — DigitalOcean, Linode, Vultr, AWS, Azure, etc. A $5-6/month VPS works. Pick Ubuntu 22.04 or 24.04.
- Raspberry Pi — Works, just slower to build.
- Home PC/laptop — Works if your ISP doesn't block ports 80/443 (many residential ISPs do).
You'll need SSH access to your server. Cloud providers give you instructions when you create the VM.
A domain name
A domain (like analyzer.example.com) pointed at your server's IP:
- Buy one (~$10/year) from Namecheap, Cloudflare, etc.
- Or use a free subdomain from DuckDNS or FreeDNS
After getting a domain, create an A record pointing to your server's IP address. Your domain provider's dashboard will have a "DNS" section for this.
Important: DNS must be configured and propagated before you start the container. Caddy will try to provision certificates on startup and fail if the domain doesn't resolve. Verify with dig analyzer.example.com — it should show your server's IP.
Open ports
Your server's firewall must allow:
- Port 80 — needed for HTTPS certificate provisioning (Let's Encrypt ACME challenge)
- Port 443 — HTTPS traffic
Cloud providers: find "Security Groups" or "Firewall" in the dashboard, add inbound rules for TCP 80 and 443 from 0.0.0.0/0.
Ubuntu firewall:
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
Installing Docker
Docker packages an app and all its dependencies into a container — an isolated environment with everything it needs to run. You don't install Node.js, Mosquitto, or Caddy separately; they're all included in the container.
SSH into your server and run:
# Install Docker
curl -fsSL https://get.docker.com | sh
# Allow your user to run Docker without sudo
sudo usermod -aG docker $USER
Log out and SSH back in (the group change needs a new session), then verify:
docker --version
# Should print: Docker version 24.x.x or newer
Quick Start
flowchart LR
A[Clone repo] --> B[Create config] --> C[Create Caddyfile] --> D[Build & run] --> E[Open site]
style E fill:#22c55e,color:#000
1. Download the code
git clone https://github.com/Kpa-clawbot/meshcore-analyzer.git
cd meshcore-analyzer
2. Create your config
cp config.example.json config.json
nano config.json
Change the apiKey to any random string. The rest of the defaults work out of the box.
{
"apiKey": "change-me-to-something-random",
...
}
Save: Ctrl+O, Enter, Ctrl+X.
3. Set up your domain for HTTPS
mkdir -p caddy-config
nano caddy-config/Caddyfile
Enter your domain (replace analyzer.example.com with yours):
analyzer.example.com {
reverse_proxy localhost:3000
}
Save and close. Caddy handles certificates, renewals, and HTTP→HTTPS redirects automatically.
4. Build and run
docker build -t meshcore-analyzer .
docker run -d \
--name meshcore-analyzer \
--restart unless-stopped \
-p 80:80 \
-p 443:443 \
-v $(pwd)/config.json:/app/config.json:ro \
-v $(pwd)/caddy-config/Caddyfile:/etc/caddy/Caddyfile:ro \
-v meshcore-data:/app/data \
-v caddy-data:/data/caddy \
meshcore-analyzer
What each flag does:
| Flag | Purpose |
|---|---|
-d |
Run in background |
--restart unless-stopped |
Auto-restart on crash or reboot |
-p 80:80 -p 443:443 |
Expose web ports |
-v .../config.json:...ro |
Your config (read-only) |
-v .../Caddyfile:... |
Your domain config |
-v meshcore-data:/app/data |
Database storage (persists across restarts) |
-v caddy-data:/data/caddy |
HTTPS certificate storage |
5. Verify
Open https://your-domain.com. You should see the analyzer home page.
Check the logs:
docker logs meshcore-analyzer
Expected output:
MeshCore Analyzer running on http://localhost:3000
MQTT [local] connected to mqtt://localhost:1883
[pre-warm] 12 endpoints in XXXms
The container runs its own MQTT broker (Mosquitto) internally — that localhost:1883 connection is inside the container, not exposed to the internet.
Connecting an Observer
The analyzer receives packets from observers via MQTT.
Option A: Use a public broker
Add a remote broker to mqttSources in your config.json:
{
"name": "public-broker",
"broker": "mqtts://mqtt.lincomatic.com:8883",
"username": "your-username",
"password": "your-password",
"rejectUnauthorized": false,
"topics": ["meshcore/SJC/#", "meshcore/SFO/#"]
}
Restart: docker restart meshcore-analyzer
Option B: Run your own observer
You need a MeshCore repeater connected via USB or BLE to a computer running meshcoretomqtt. Point it at your analyzer's MQTT broker.
⚠️ If your observer is remote (not on the same machine), you'll need to expose port 1883. Read the MQTT Security section first.
HTTPS Options
Automatic (recommended) — Caddy + Let's Encrypt
This is what the Quick Start sets up. Caddy handles everything. Requirements:
- Domain pointed at your server
- Ports 80 + 443 open
- No other web server (Apache, nginx) running on those ports
Bring your own certificate
If you already have a certificate (from Cloudflare, your organization, etc.), tell Caddy to use it instead of Let's Encrypt:
analyzer.example.com {
tls /path/to/cert.pem /path/to/key.pem
reverse_proxy localhost:3000
}
Mount the cert files into the container:
docker run ... \
-v /path/to/cert.pem:/certs/cert.pem:ro \
-v /path/to/key.pem:/certs/key.pem:ro \
...
And update the Caddyfile paths to /certs/cert.pem and /certs/key.pem.
Cloudflare Tunnel (no open ports needed)
If you can't open ports 80/443 (residential ISP, restrictive firewall), use a Cloudflare Tunnel. It creates an outbound connection from your server to Cloudflare — no inbound ports needed. Your Caddyfile becomes:
:80 {
reverse_proxy localhost:3000
}
And Cloudflare handles HTTPS at the edge.
Behind an existing reverse proxy (nginx, Traefik, etc.)
If you already run a reverse proxy, skip Caddy entirely and proxy directly to the Node.js port:
docker run -d \
--name meshcore-analyzer \
--restart unless-stopped \
-p 3000:3000 \
-v $(pwd)/config.json:/app/config.json:ro \
-v meshcore-data:/app/data \
meshcore-analyzer
Then configure your existing proxy to forward traffic to localhost:3000.
HTTP only (development / local network)
For local testing or a LAN-only setup, use the default Caddyfile that ships in the image (serves on port 80, no HTTPS):
docker run -d \
--name meshcore-analyzer \
--restart unless-stopped \
-p 80:80 \
-v $(pwd)/config.json:/app/config.json:ro \
-v meshcore-data:/app/data \
meshcore-analyzer
MQTT Security
The container runs Mosquitto on port 1883 with anonymous access by default. This is safe as long as the port isn't exposed outside the container.
The Quick Start docker run command above does not expose port 1883. Only add -p 1883:1883 if you need remote observers to connect directly.
If you need to expose MQTT
Option 1: Firewall — Only allow specific IPs:
sudo ufw allow from 203.0.113.10 to any port 1883 # Your observer's IP
Option 2: Add authentication — Edit docker/mosquitto.conf before building:
allow_anonymous false
password_file /etc/mosquitto/passwd
After starting the container, create users:
docker exec -it meshcore-analyzer mosquitto_passwd -c /etc/mosquitto/passwd myuser
Option 3: Use TLS — For production, configure Mosquitto with TLS certificates. See the Mosquitto docs.
Recommended approach for remote observers
Don't expose 1883 at all. Instead, have your observers publish to a shared public MQTT broker (like lincomatic's), and configure your analyzer to subscribe to that broker in mqttSources. The analyzer makes an outbound connection — no inbound ports needed.
Database Backups
Packet data is stored in meshcore.db inside the meshcore-data Docker volume.
Find the database file
docker volume inspect meshcore-data --format '{{ .Mountpoint }}'
# Prints something like: /var/lib/docker/volumes/meshcore-data/_data
The database is at that path + /meshcore.db.
Manual backup
cp $(docker volume inspect meshcore-data --format '{{ .Mountpoint }}')/meshcore.db \
~/meshcore-backup-$(date +%Y%m%d).db
Automated daily backup (cron)
crontab -e
# Add this line:
0 3 * * * cp $(docker volume inspect meshcore-data --format '\{\{ .Mountpoint \}\}')/meshcore.db /home/youruser/backups/meshcore-$(date +\%Y\%m\%d).db
Easier alternative: use a local directory
Instead of a Docker volume, mount a local directory. Replace -v meshcore-data:/app/data with:
-v ./analyzer-data:/app/data
Now the database is just ./analyzer-data/meshcore.db — easy to find, back up, and restore.
Restoring from backup
Stop the container, replace meshcore.db with your backup file, start the container:
docker stop meshcore-analyzer
cp ~/meshcore-backup-20260324.db $(docker volume inspect meshcore-data --format '{{ .Mountpoint }}')/meshcore.db
docker start meshcore-analyzer
Updating
cd meshcore-analyzer
git pull
docker build -t meshcore-analyzer .
docker stop meshcore-analyzer
docker rm meshcore-analyzer
# Re-run the same docker run command from Quick Start step 4
Data is preserved in the Docker volumes.
Tip: Save your docker run command in a script (run.sh) so you don't have to remember all the flags.
Customization
Branding
In config.json:
{
"branding": {
"siteName": "Bay Area Mesh",
"tagline": "Community LoRa network for the Bay Area",
"logoUrl": "https://example.com/logo.png",
"faviconUrl": "https://example.com/favicon.ico"
}
}
Themes
Create a theme.json in your data directory to customize colors. See CUSTOMIZATION.md for all options.
Map defaults
Center the map on your area in config.json:
{
"mapDefaults": {
"center": [37.45, -122.0],
"zoom": 9
}
}
Troubleshooting
| Problem | Likely cause | Fix |
|---|---|---|
| Site shows "connection refused" | Container not running | docker ps to check, docker logs meshcore-analyzer for errors |
| HTTPS not working | Port 80 blocked | Open port 80 — Caddy needs it for ACME challenges |
| "too many certificates" error | Let's Encrypt rate limit (5/domain/week) | Use a different subdomain, bring your own cert, or wait a week |
| Certificate won't provision | DNS not pointed at server | dig your-domain must show your server IP before starting |
| No packets appearing | No observer connected | docker exec meshcore-analyzer mosquitto_sub -t 'meshcore/#' -C 1 -W 10 — if silent, no data is coming in |
| Container crashes on startup | Bad JSON in config | python3 -c "import json; json.load(open('config.json'))" to validate |
| "address already in use" | Another web server on 80/443 | Stop it: sudo systemctl stop nginx apache2 |
| Slow on Raspberry Pi | First build is slow | Normal — subsequent builds use cache. Runtime performance is fine. |
Architecture Overview
Traffic flow
flowchart LR
subgraph Internet
U[Browser] -->|HTTPS :443| C
O1[Observer 1] -->|MQTT :1883| M
O2[Observer 2] -->|MQTT :1883| M
LE[Let's Encrypt] -->|HTTP :80| C
end
subgraph Docker Container
C[Caddy] -->|proxy :3000| N[Node.js]
M[Mosquitto] --> N
N --> DB[(SQLite)]
N -->|WebSocket| U
end
style C fill:#22c55e,color:#000
style M fill:#3b82f6,color:#fff
style N fill:#f59e0b,color:#000
style DB fill:#8b5cf6,color:#fff
Container internals
flowchart TD
S[supervisord] --> C[Caddy]
S --> M[Mosquitto]
S --> N[Node.js server]
C -->|reverse proxy + auto HTTPS| N
M -->|MQTT messages| N
N --> API[REST API]
N --> WS[WebSocket — live feed]
N --> MQTT[MQTT client — ingests packets]
N --> DB[(SQLite — data/meshcore.db)]
style S fill:#475569,color:#fff
style C fill:#22c55e,color:#000
style M fill:#3b82f6,color:#fff
style N fill:#f59e0b,color:#000
Data flow
sequenceDiagram
participant R as LoRa Repeater
participant O as Observer
participant M as Mosquitto
participant N as Node.js
participant B as Browser
R->>O: Radio packet (915 MHz)
O->>M: MQTT publish (raw hex + SNR + RSSI)
M->>N: Subscribe callback
N->>N: Decode, store in SQLite + memory
N->>B: WebSocket broadcast
B->>N: REST API requests
N->>B: JSON responses