MeshInfo uses PostgreSQL as its storage backend. This document covers configuration, schema, and operational guidance.
Add the following to your config.toml:
[storage.postgres]
enabled = true
host = "postgres"
port = 5432
database = "meshinfo"
username = "postgres"
password = "your_password"
min_pool_size = 1
max_pool_size = 5postgres.enabled: Must betruepostgres.host: Database server hostname (use"localhost"if not using Docker)postgres.port: Database server port (default: 5432)postgres.database: Database namepostgres.username: Database userpostgres.password: Database passwordpostgres.min_pool_size: Minimum connection pool size (default: 1)postgres.max_pool_size: Maximum connection pool size (default: 5)
The schema is automatically created on first run. Key tables:
nodes: Core node information (ID, name, hardware, status)node_positions: Historical position datanode_neighborinfo: Neighbor relationships (JSONB)node_telemetry_current: Most recent telemetry per nodetelemetry: Complete telemetry history (JSONB payloads)chat_channels: Chat channel metadatachat_messages: All chat messagestraceroutes: Complete traceroute history (JSONB payloads)
- MQTT message received
- Handler reads existing node via
pg_storage.get_node_cached(LRU + DB) - Handler merges incoming fields into the node
data.update_nodewrites the merged node to PostgreSQL and refreshes the cache
- Application starts
- PostgreSQL connection established
- API queries PostgreSQL directly for each request (node lookups hit the LRU cache first)
- PostgreSQL write failures are logged but never block application execution
- Connection pool handles transient network issues
- Failed writes are logged for manual investigation
The postgres service in both compose files sets a few core memory
parameters via its command:. The defaults are deliberately conservative so
the stack runs on a small host, but they are already well above the stock
postgres image defaults (shared_buffers 128 MB, work_mem 4 MB).
Size them to your host: copy .env.sample to .env next to the compose file
(Docker Compose reads it automatically) and set any of:
| Variable | Default | What it does |
|---|---|---|
PG_SHARED_BUFFERS |
256MB |
Dedicated PG page cache (real RAM allocation). |
PG_WORK_MEM |
8MB |
Per-sort/hash memory; multiplied by concurrency. |
PG_MAINTENANCE_WORK_MEM |
64MB |
VACUUM / CREATE INDEX working memory. |
PG_EFFECTIVE_CACHE_SIZE |
1GB |
Planner hint for OS+PG cache (not an allocation). |
Suggested starting points by total host RAM (assuming Postgres shares the box with the rest of the MeshInfo stack — give it less than a dedicated DB host would get):
| Host RAM | PG_SHARED_BUFFERS |
PG_WORK_MEM |
PG_MAINTENANCE_WORK_MEM |
PG_EFFECTIVE_CACHE_SIZE |
|---|---|---|---|---|
| 4 GB | 512MB |
16MB |
128MB |
2GB |
| 8 GB | 1GB |
32MB |
256MB |
4GB |
| 16 GB | 2GB |
64MB |
512MB |
10GB |
Example .env:
PG_SHARED_BUFFERS=512MB
PG_WORK_MEM=16MB
PG_MAINTENANCE_WORK_MEM=128MB
PG_EFFECTIVE_CACHE_SIZE=2GB
Recreate the container to apply: docker compose up -d postgres.
All writes happen in real-time as data arrives from MQTT, ensuring minimal data loss on crash.
asyncpg connection pooling handles concurrent writes efficiently. Tune min_pool_size and max_pool_size based on your load.
Key indexes are maintained on node IDs, timestamps, and foreign key relationships for fast queries even with large datasets.
PostgreSQL operations are logged at INFO and ERROR levels:
INFO: PostgreSQL mode: Data will be queried directly from database
ERROR: Failed to write node abc123 to Postgres: connection timeout
Monitor these logs to ensure healthy operation.
If PostgreSQL connection fails on startup, MeshInfo will log the error and exit. Check:
- PostgreSQL container/service is running
- Connection settings in
config.tomlare correct (host,port,username,password) - Network connectivity between MeshInfo and the database
To verify record counts:
SELECT COUNT(*) FROM nodes;
SELECT COUNT(*) FROM chat_messages;
SELECT COUNT(*) FROM telemetry;
SELECT COUNT(*) FROM traceroutes;If you are upgrading from an older version that used JSON file storage, a one-time migration script is available. This script must be run against your legacy deployment before upgrading — it requires:
- The JSON data files still present on disk (
nodes.json,chat.json,telemetry.json,traceroutes.json) - The old
[paths]section in your config (withpaths.datapointing to those files)
From that legacy environment, run:
docker exec -it meshinfo-meshinfo-1 python3 scripts/migrate_json_to_postgres.pyThis reads your existing JSON data files and imports them into PostgreSQL. Once complete, update your config to the new PostgreSQL-only format (see config.toml.sample) and restart.
PostgreSQL major versions (16 → 18, etc.) use incompatible on-disk storage
formats. A newer postgres container will refuse to start against a data
volume initialised by an older one, failing with:
FATAL: database files are incompatible with server
DETAIL: The data directory was initialized by PostgreSQL version 16,
which is not compatible with this version 18.x.
When a MeshInfo release bumps the postgres image major version, run the
included migration script once. It dumps the database with a temporary
container of the old version, recreates the volume, and restores into the
new version — using only official postgres images.
git pull
docker compose pull # fetch the new postgres image
bash scripts/migrate-postgres.sh # one-time: dump old → restore new
docker compose up -dFor a development stack, point the script at the dev compose file:
COMPOSE_FILE=docker-compose-dev.yml bash scripts/migrate-postgres.shNotes:
- The script writes a compressed SQL dump to
backups/and verifies it before recreating the volume — that file is your recovery artifact. - It is a no-op on fresh installs and when the volume is already current, so it is safe to run unconditionally as part of an update.
- On Windows, run it from Git Bash (bundled with Git) —
bash scripts/migrate-postgres.sh. The script is MSYS-path-safe, so the single bash version covers every platform. - Set
KEEP_VOLUME_BACKUP=1to also snapshot the old data volume to<volume>_oldpg_backupfor an instant rollback (uses extra disk; delete it once the upgrade is verified). - To roll back from the SQL dump: set the
postgresimage (and volume mount) in your compose file back to the old version, remove the data volume so it re-initialises empty,docker compose up -d postgres, then restore —gzip -dc backups/<dump>.sql.gz | docker compose exec -T postgres psql -U postgres -d meshinfo.
mqtt_messages is the raw packet firehose and dominates the database — it
typically holds 98%+ of all rows. Fresh installs since this change get a
partitioned table (RANGE partitioned by month on created_at), so the
table stays operationally manageable as it grows: each date-range query
prunes to the relevant month(s) instead of scanning everything, and
maintenance runs at partition scale, not table scale. The app
(ensure_mqtt_partitions) keeps next month's partition created ahead of
the rollover.
Databases created before partitioning landed need a one-time conversion:
bash scripts/migrate-mqtt-partitioning.sh
# dev stack: COMPOSE_FILE=docker-compose-dev.yml bash scripts/migrate-mqtt-partitioning.shWhat it does, inside a single atomic transaction:
- Renames the existing table to
mqtt_messages_old. - Creates a new month-partitioned
mqtt_messages(with thepayloadcolumnlz4-compressed). - Creates one partition per month spanned by the data, copies every row, and verifies the row count matches before committing.
- Rebuilds indexes, re-homes the
idsequence, reinstalls the trigger.
Any failure rolls the whole thing back — mqtt_messages is untouched.
The script stops meshinfo for the duration and restarts it on success.
Notes:
- Requires lz4: the partitioned
mqtt_messagespayloadcolumn usesCOMPRESSION lz4, so Postgres must be built with lz4 support (PG 14+ — the officialpostgres:18image this stack ships with includes it). A self-hosted Postgres compiled without lz4 will reject both the fresh-install schema and the migration withcompression method lz4 not supported. - Disk: the migration needs ~3× the current
mqtt_messagessize free on the data volume transiently (new copy + WAL + headroom). The pre-flight check aborts with a clear message if there's not enough. Expand the data volume first if you're short. - Rollback safety: the original data is kept as
mqtt_messages_olduntil you drop it. Disk is not reclaimed until then. Once you've confirmed the app + Logs page look right, run:docker compose exec postgres psql -U postgres -d meshinfo \ -c 'DROP TABLE mqtt_messages_old;'
- Idempotent: re-running the script when the table is already partitioned (or doesn't exist yet) is a no-op.
- On Windows, run from Git Bash, like
migrate-postgres.sh. - Partitioning does not shrink disk — it makes a huge table manageable. Complete-history retention still implies the data volume must be allowed to grow.
- Use strong passwords for PostgreSQL
- Consider SSL/TLS for database connections in production
- Restrict database access via network policies
- Set up regular PostgreSQL backups (e.g.,
pg_dump) - Keep PostgreSQL updated with security patches
For issues or questions:
- Open an issue on GitHub
- Join #meshinfo on SacValleyMesh Discord
- Review application logs for detailed error messages