mirror of
https://github.com/l5yth/potato-mesh.git
synced 2026-10-31 03:51:58 +01:00
199 lines
9.8 KiB
Markdown
199 lines
9.8 KiB
Markdown
<!-- Copyright © 2025-26 l5yth & contributors -->
|
|
<!-- Licensed under the Apache License, Version 2.0 (see LICENSE) -->
|
|
|
|
# PotatoMesh Docker Guide
|
|
|
|
PotatoMesh publishes ready-to-run container images to the GitHub Packages container
|
|
registry (GHCR). You do not need to clone the repository to deploy them - Compose
|
|
will pull the latest release images for you.
|
|
|
|
## Prerequisites
|
|
|
|
- Docker Engine 24+ or Docker Desktop with the Compose plugin
|
|
- Access to `/dev/ttyACM*` (or equivalent) if you plan to attach a Meshtastic
|
|
device to the ingestor container
|
|
- An API token that authorises the ingestor to post to your PotatoMesh instance
|
|
|
|
## Images on GHCR
|
|
|
|
| Service | Image |
|
|
|----------|----------------------------------------------------------------------------------------------------------------|
|
|
| Web UI | `ghcr.io/l5yth/potato-mesh-web-linux-amd64:<tag>` (e.g. `latest`, `0.6.0`, `v0.6.0`, or `0.7.0-rc1`) |
|
|
| Ingestor | `ghcr.io/l5yth/potato-mesh-ingestor-linux-amd64:<tag>` (e.g. `latest`, `0.6.0`, `v0.6.0`, or `0.7.0-rc1`) |
|
|
|
|
Images are published for every tagged release. Stable builds receive both
|
|
semantic version tags (for example `0.6.0`) and a matching `v`-prefixed tag (for
|
|
example `v0.6.0`), plus a `latest` tag that tracks the newest stable release.
|
|
Pre-release tags (for example `-rc`, `-beta`, `-alpha`, or `-dev` suffixes) are
|
|
published only with their explicit version strings (`0.7.0-rc1` and `v0.7.0-rc1`
|
|
in this example) and do not advance `latest`. Pin the versioned tags when
|
|
you need a specific build.
|
|
|
|
## Configure environment
|
|
|
|
Create a `.env` file alongside your Compose file and populate the variables you
|
|
need. At a minimum you must set `API_TOKEN` so the ingestor can authenticate
|
|
against the web API.
|
|
|
|
```env
|
|
API_TOKEN=replace-with-a-strong-token
|
|
SITE_NAME=PotatoMesh Demo
|
|
CONNECTION=/dev/ttyACM0
|
|
INSTANCE_DOMAIN=mesh.example.org
|
|
```
|
|
|
|
Additional environment variables are optional:
|
|
|
|
| Variable | Default | Purpose |
|
|
| --- | --- | --- |
|
|
| `API_TOKEN` | _required_ | Shared secret used by the ingestor and API clients for authenticated `POST` requests. |
|
|
| `INSTANCE_DOMAIN` | _auto-detected_ | Public hostname (optionally with port) advertised by the web UI, metadata, and API responses. |
|
|
| `SITE_NAME` | `"PotatoMesh Demo"` | Title and branding surfaced in the web UI. |
|
|
| `CHANNEL` | `"#LongFast"` | Default LoRa channel label displayed on the dashboard. |
|
|
| `FREQUENCY` | `"915MHz"` | Default LoRa frequency description shown in the UI. |
|
|
| `CONTACT_LINK` | `"#potatomesh:dod.ngo"` | Chat link or Matrix room alias rendered in UI footers and overlays. |
|
|
| `MAP_CENTER` | `38.761944,-27.090833` | Latitude and longitude that centre the map view. |
|
|
| `MAP_ZOOM` | _unset_ | Fixed Leaflet zoom (disables the auto-fit checkbox when set). |
|
|
| `MAX_DISTANCE` | `42` | Maximum relationship distance (km) before edges are hidden. |
|
|
| `DEBUG` | `0` | Enables verbose logging across services when set to `1`. |
|
|
| `ALLOWED_CHANNELS` | _unset_ | Comma-separated channel names the ingestor accepts; packets from other channels are dropped before `HIDDEN_CHANNELS` applies. The node list sent at connect can still carry a node's last position, metrics and signal details (last heard, SNR, hop limit) heard on a filtered channel. |
|
|
| `HIDDEN_CHANNELS` | _unset_ | Comma-separated channel names whose packets the ingestor drops. The node list sent at connect can still carry a node's last position, metrics and signal details (last heard, SNR, hop limit) heard on a filtered channel. |
|
|
| `FEDERATION` | `1` | Controls whether the instance announces itself and crawls peers (`1`) or stays isolated (`0`). |
|
|
| `PRIVATE` | `0` | Restricts public visibility and disables chat/message endpoints when set to `1`. |
|
|
| `CONNECTION` | `/dev/ttyACM0` | Serial device, TCP endpoint, or Bluetooth target used by the ingestor to reach the radio. |
|
|
| `MIN_THREADS` | `16` | Minimum Puma worker threads kept warm on the web service. |
|
|
| `MAX_THREADS` | `96` | Maximum Puma worker threads on the web service. Each active `/api/events` (SSE) stream pins one thread, so keep this above your peak concurrent SSE clients plus API/ingest headroom. |
|
|
|
|
The ingestor posts to the URL configured via `INSTANCE_DOMAIN` (defaulting to
|
|
`http://web:41447` in the provided compose file). `CHANNEL_INDEX` sets the
|
|
channel the activity announcement is sent on; it does not filter what is
|
|
ingested.
|
|
|
|
## Docker Compose file
|
|
|
|
Use the `docker-compose.yml` file provided in the repository (or download the
|
|
[raw file from GitHub](https://raw.githubusercontent.com/l5yth/potato-mesh/main/docker-compose.yml)).
|
|
It already references the published GHCR images, defines persistent volumes for
|
|
data, configuration, and logs, and includes optional bridge-profile services for
|
|
environments that require classic port mapping. Place this file in the same
|
|
directory as your `.env` file so Compose can pick up both.
|
|
|
|
The dedicated configuration volume binds to `/app/.config/potato-mesh` inside
|
|
the container, storing the instance private key and staged
|
|
`/.well-known/potato-mesh` documents. Credentials survive reboot and re-deploy
|
|
as long as this volume persists.
|
|
|
|
The `potatomesh_pages` volume mounts to `/app/pages` and holds operator-managed
|
|
Markdown files that are rendered as static content pages in the web UI. On first
|
|
start the default `1-about.md` page is copied from the image into the volume.
|
|
You can add, edit, or remove `.md` files in this volume to customise your
|
|
instance's navigation. To use a host directory instead of a named volume, replace
|
|
the volume entry with a bind mount:
|
|
|
|
```yaml
|
|
volumes:
|
|
- ./my-pages:/app/pages
|
|
```
|
|
|
|
## Start the stack
|
|
|
|
From the directory containing the Compose file:
|
|
|
|
```bash
|
|
docker compose up -d
|
|
```
|
|
|
|
Docker automatically pulls the GHCR images when they are not present locally.
|
|
The dashboard becomes available at `http://127.0.0.1:41447`. Use the bridge
|
|
profile when you need to map the port explicitly:
|
|
|
|
```bash
|
|
COMPOSE_PROFILES=bridge docker compose up -d
|
|
```
|
|
|
|
## Updating
|
|
|
|
```bash
|
|
docker compose pull
|
|
docker compose up -d
|
|
```
|
|
|
|
## Running behind a reverse proxy (TLS + static assets)
|
|
|
|
The web container serves plain HTTP on port `41447`. For any public deployment,
|
|
terminate TLS in a reverse proxy in front of it. A ready-to-adapt nginx example
|
|
lives at [`deploy/nginx.example.conf`](deploy/nginx.example.conf); the notes
|
|
below explain the parts that matter.
|
|
|
|
Forwarded headers (required). The app derives its public scheme and host -
|
|
used for `INSTANCE_DOMAIN`, page metadata, the sitemap, and federation links -
|
|
from `X-Forwarded-Proto` and the `Host` header. Forward both, or generated URLs
|
|
resolve to the wrong scheme/host:
|
|
|
|
```nginx
|
|
proxy_set_header Host $host;
|
|
proxy_set_header X-Forwarded-Proto $scheme;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
```
|
|
|
|
Static-asset caching. Every JS module and `base.css` is versioned
|
|
(`?v=<APP_VERSION>`) and safe to cache immutably for a year. Images,
|
|
icons, and fonts are not versioned - keep a short TTL with revalidation. Two
|
|
ways to serve the versioned assets with long-lived caching:
|
|
|
|
1. Any deployment (portable): have the app emit the headers itself. The
|
|
container bakes assets into the image at `/app/public` with no volume, so a
|
|
host proxy cannot read them from disk - this is the only option for the
|
|
Compose/GHCR stack. Tracked in
|
|
[#870](https://github.com/l5yth/potato-mesh/issues/870).
|
|
2. Bare-metal (repo checkout): serve `/assets/` straight from nginx off disk
|
|
(the `location /assets/` block in the example). This also keeps the ~50
|
|
per-page ES-module requests off the single Ruby process. For containers, only
|
|
do this if you bind-mount `web/public` into an nginx sidecar.
|
|
|
|
Three things that bite in practice - all handled in the example file:
|
|
|
|
- Filesystem permissions: the proxy worker user (`http`, `www-data`, …) must
|
|
be able to traverse to and read `web/public`. Check with
|
|
`namei -l <path>/web/public/assets/styles/base.css` - every parent directory
|
|
needs `o+x`, or disk-served assets return `403`.
|
|
- Upstream keepalive needs both `proxy_http_version 1.1` and
|
|
`proxy_set_header Connection ""`.
|
|
- TLS session resumption: don't add `ssl_session_tickets on;` alongside
|
|
Certbot's `options-ssl-nginx.conf` - it already sets `ssl_session_tickets
|
|
off` and ships its own `ssl_session_cache`; duplicating the directive is a
|
|
fatal config error. Leave it off.
|
|
|
|
Verify after `nginx -t && systemctl reload nginx`:
|
|
|
|
```bash
|
|
curl -sD- -H 'Accept-Encoding: gzip' https://<host>/assets/js/app/main.js?v=<ver> -o /dev/null \
|
|
| grep -i 'cache-control\|content-encoding'
|
|
# expect: cache-control: public, max-age=31536000, immutable and content-encoding: gzip
|
|
```
|
|
|
|
## Performance & scaling
|
|
|
|
The web app runs as a single Puma process with a bounded thread pool
|
|
(`MIN_THREADS:MAX_THREADS`, default `16:96`). Keep hot read paths cheap and
|
|
let the reverse proxy absorb static traffic.
|
|
|
|
- Thread pool. Each live-update SSE stream (`GET /api/events`) pins one
|
|
thread for its lifetime - size `MAX_THREADS` above your expected concurrent
|
|
SSE clients plus API/ingest headroom. Override with `MIN_THREADS` / `MAX_THREADS`.
|
|
- Static assets. Serve `/assets/` from the reverse proxy (or via app-level
|
|
immutable headers) so the module fan-out and revalidations never touch Ruby -
|
|
see the section above.
|
|
- Cluster (multi-process) mode is not supported. To scale on one host:
|
|
front it with the reverse proxy, serve assets from disk, and keep queries cheap.
|
|
|
|
## Troubleshooting
|
|
|
|
- Serial device permissions (Linux/macOS): grant access with `sudo chmod 666
|
|
/dev/ttyACM0` or add your user to the `dialout` group.
|
|
- Port already in use: identify the conflicting service with `sudo lsof -i
|
|
:41447`.
|
|
- Viewing logs: `docker compose logs -f` tails output from both services.
|
|
|
|
For general Docker support, consult the [Docker Compose documentation](https://docs.docker.com/compose/).
|