Database β
MeshMonitor supports three database backends:
- SQLite (default) - Simple file-based database, no additional setup required
- PostgreSQL - Scalable relational database for larger deployments
- MySQL / MariaDB - Alternative scalable database option
Choosing a Database β
SQLite (Recommended for Most Users) β
SQLite is the default and recommended choice for most deployments:
- Zero configuration - Works out of the box
- Single file - Easy to backup and migrate
- Low resource usage - Ideal for home servers and Raspberry Pi
- Sufficient for most deployments - Handles thousands of nodes without issue
SQLite is automatically used when no DATABASE_URL environment variable is set.
PostgreSQL (For Advanced Deployments) β
Consider PostgreSQL when:
- You have 1000+ active nodes with high message volume
- You need concurrent access from multiple services
- You require advanced queries or reporting
- You want enterprise-grade reliability and backups
- You're running multiple MeshMonitor instances accessing the same data
MySQL / MariaDB (Alternative Scalable Option) β
Consider MySQL/MariaDB when:
- You already have existing MySQL/MariaDB infrastructure
- Your team has MySQL expertise
- You need compatibility with MySQL-based tools
- You're running MariaDB as your preferred database
Configuration β
SQLite Configuration β
SQLite requires no configuration. The database file is stored at:
/data/meshmonitor.dbYou can customize the path with the DATABASE_PATH environment variable:
environment:
- DATABASE_PATH=/data/meshmonitor.dbPostgreSQL Configuration β
To use PostgreSQL, set the DATABASE_URL environment variable:
environment:
- DATABASE_URL=postgres://user:password@hostname:5432/meshmonitorConnection String Format β
postgres://[user]:[password]@[host]:[port]/[database]| Component | Description | Example |
|---|---|---|
user | PostgreSQL username | meshmonitor |
password | PostgreSQL password | secretpassword |
host | Server hostname or IP | localhost, postgres, db.example.com |
port | PostgreSQL port | 5432 (default) |
database | Database name | meshmonitor |
Docker Compose with PostgreSQL β
Use the Docker Compose Configurator to generate a complete configuration, or use this example:
services:
postgres:
image: postgres:16-alpine
container_name: meshmonitor-postgres
restart: unless-stopped
volumes:
- postgres-data:/var/lib/postgresql/data
environment:
- POSTGRES_DB=meshmonitor
- POSTGRES_USER=meshmonitor
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
healthcheck:
test: ["CMD-SHELL", "pg_isready -U meshmonitor -d meshmonitor"]
interval: 10s
timeout: 5s
retries: 5
meshmonitor:
image: ghcr.io/yeraze/meshmonitor:latest
container_name: meshmonitor
ports:
- "8080:3001"
restart: unless-stopped
volumes:
- meshmonitor-data:/data
env_file: .env
environment:
- DATABASE_URL=postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/meshmonitor
depends_on:
postgres:
condition: service_healthy
volumes:
meshmonitor-data:
driver: local
postgres-data:
driver: localCreate a .env file:
# PostgreSQL credentials
POSTGRES_USER=meshmonitor
POSTGRES_PASSWORD=your_secure_password_herePostgreSQL Memory and Maintenance β
The postgres image starts with shared_buffers=128MB. That is too small once a busy install holds a few GB, and queries start reading from disk. For a host with 4 GB or more of RAM, pass larger settings to the server:
postgres:
image: postgres:16-alpine
command:
- postgres
- -c
- shared_buffers=1GB # about 25% of the RAM you give PostgreSQL
- -c
- effective_cache_size=3GB # about 75% of that RAM
- -c
- work_mem=16MB
- -c
- maintenance_work_mem=256MB
shm_size: 1gb # must be at least shared_buffersTo check, run SELECT sum(blks_hit) * 100.0 / nullif(sum(blks_hit + blks_read), 0) FROM pg_stat_database; in psql. A healthy cache hit ratio is above 99%.
Turn on Settings β Database Maintenance too. It deletes messages, traceroutes, route segments, neighbor info and audit log entries past their retention periods, then runs VACUUM. It is off by default. Older releases hid the section on PostgreSQL and MySQL, so an install that started on one of those has probably never been cleaned up.
After the first large cleanup, rebuild the bloated indexes once. These commands do not block the app:
REINDEX TABLE CONCURRENTLY route_segments;
REINDEX TABLE CONCURRENTLY telemetry;
REINDEX TABLE CONCURRENTLY nodes;
REINDEX TABLE CONCURRENTLY audit_log;
VACUUM (ANALYZE) route_segments, telemetry, nodes, audit_log;MySQL / MariaDB Configuration β
To use MySQL or MariaDB, set the DATABASE_URL environment variable:
environment:
- DATABASE_URL=mysql://user:password@hostname:3306/meshmonitorConnection String Format β
mysql://[user]:[password]@[host]:[port]/[database]| Component | Description | Example |
|---|---|---|
user | MySQL username | meshmonitor |
password | MySQL password | secretpassword |
host | Server hostname or IP | localhost, mysql, db.example.com |
port | MySQL port | 3306 (default) |
database | Database name | meshmonitor |
MariaDB Support
MariaDB URLs use the same format: mariadb://user:password@host:3306/database
Docker Compose with MySQL β
Use the Docker Compose Configurator to generate a complete configuration, or use this example:
services:
mysql:
image: mysql:8.0
container_name: meshmonitor-mysql
restart: unless-stopped
volumes:
- mysql-data:/var/lib/mysql
environment:
- MYSQL_DATABASE=meshmonitor
- MYSQL_USER=meshmonitor
- MYSQL_PASSWORD=${MYSQL_PASSWORD}
- MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 10s
timeout: 5s
retries: 5
meshmonitor:
image: ghcr.io/yeraze/meshmonitor:latest
container_name: meshmonitor
ports:
- "8080:3001"
restart: unless-stopped
volumes:
- meshmonitor-data:/data
env_file: .env
environment:
- DATABASE_URL=mysql://${MYSQL_USER}:${MYSQL_PASSWORD}@mysql:3306/meshmonitor
depends_on:
mysql:
condition: service_healthy
volumes:
meshmonitor-data:
driver: local
mysql-data:
driver: localCreate a .env file:
# MySQL credentials
MYSQL_USER=meshmonitor
MYSQL_PASSWORD=your_secure_password_here
MYSQL_ROOT_PASSWORD=your_root_password_hereMigrating from SQLite to PostgreSQL β
If you have an existing SQLite installation and want to migrate to PostgreSQL, MeshMonitor includes a migration tool.
Prerequisites β
- A running PostgreSQL server (local or remote)
- An empty PostgreSQL database created for MeshMonitor
- Access to your existing SQLite database file
Migration Steps β
1. Stop MeshMonitor β
docker compose stop meshmonitor2. Create the PostgreSQL Database β
If using the included PostgreSQL container:
docker compose up -d postgresIf using an external PostgreSQL server:
CREATE DATABASE meshmonitor;
CREATE USER meshmonitor WITH PASSWORD 'your_secure_password';
GRANT ALL PRIVILEGES ON DATABASE meshmonitor TO meshmonitor;3. Copy the SQLite Database β
Extract the SQLite database from the Docker volume:
docker cp meshmonitor:/data/meshmonitor.db ./meshmonitor.db4. Run the Migration β
Using the MeshMonitor CLI migration tool:
# From the MeshMonitor source directory
npx tsx src/cli/migrate-db.ts \
--from sqlite:./meshmonitor.db \
--to postgres://meshmonitor:password@localhost:5432/meshmonitorOr with Docker:
docker run --rm -it \
-v $(pwd)/meshmonitor.db:/data/meshmonitor.db:ro \
--network host \
ghcr.io/yeraze/meshmonitor:4.9.2 \
npm run migrate-db -- \
--from sqlite:/data/meshmonitor.db \
--to postgres://meshmonitor:password@localhost:5432/meshmonitorMatch the image tag to your current version
Run the migration tool from the Docker image tag that matches the MeshMonitor version that produced your SQLite database (e.g. ghcr.io/yeraze/meshmonitor:3.12.0 if you're migrating a v3.12.0 database) β not latest. A newer tool may reference tables or columns that don't exist in your older schema and fail mid-migration. Migrate first, then upgrade MeshMonitor β the regular startup migrations will bring the new database up to the current schema.
Migration Options β
| Option | Description |
|---|---|
--from | Source database URL (e.g., sqlite:./meshmonitor.db) |
--to | Target database URL (e.g., postgres://user:pass@host/db) |
--dry-run | Show what would be migrated without making changes |
--verbose | Enable detailed logging |
5. Verify the Migration β
Check that data was migrated correctly:
# Connect to PostgreSQL
docker compose exec postgres psql -U meshmonitor -d meshmonitor
# Check row counts
SELECT 'nodes' as table_name, COUNT(*) as count FROM nodes
UNION ALL SELECT 'messages', COUNT(*) FROM messages
UNION ALL SELECT 'telemetry', COUNT(*) FROM telemetry;6. Update Configuration β
Update your docker-compose.yml to use PostgreSQL:
environment:
- DATABASE_URL=postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/meshmonitor7. Start MeshMonitor β
docker compose up -d meshmonitor8. Verify Operation β
Check the logs to confirm PostgreSQL is being used:
docker compose logs meshmonitor | grep -i "database\|postgres"You should see:
[INFO] Database: PostgreSQL (configured via DATABASE_URL)
[INFO] [DatabaseService] Using PostgreSQL driver for Drizzle repositories
[INFO] [PostgreSQL Driver] Database initialized successfullyMigration Notes β
- Data integrity: The migration tool validates data during transfer
- Large databases: Migration of 100,000+ rows may take several minutes
- Rollback: Keep your SQLite database as a backup until you've verified the migration
- Downtime: Plan for brief downtime during migration
Troubleshooting Migration β
Connection refused β
Ensure PostgreSQL is running and accessible:
docker compose exec postgres pg_isready -U meshmonitorPermission denied β
Verify database user has proper permissions:
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO meshmonitor;
GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO meshmonitor;Type conversion errors β
Some SQLite data types are loosely typed. The migration tool handles most cases, but you may see warnings for:
- Float values in integer columns (automatically truncated)
- Boolean values stored as 0/1 (automatically converted)
Migrating from SQLite to MySQL β
The migration process for MySQL is similar to PostgreSQL.
Migration Steps β
1. Stop MeshMonitor β
docker compose stop meshmonitor2. Create the MySQL Database β
If using the included MySQL container:
docker compose up -d mysqlIf using an external MySQL server:
CREATE DATABASE meshmonitor CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'meshmonitor'@'%' IDENTIFIED BY 'your_secure_password';
GRANT ALL PRIVILEGES ON meshmonitor.* TO 'meshmonitor'@'%';
FLUSH PRIVILEGES;3. Copy the SQLite Database β
Extract the SQLite database from the Docker volume:
docker cp meshmonitor:/data/meshmonitor.db ./meshmonitor.db4. Run the Migration β
npx tsx src/cli/migrate-db.ts \
--from sqlite:./meshmonitor.db \
--to mysql://meshmonitor:password@localhost:3306/meshmonitor5. Update Configuration β
Update your docker-compose.yml to use MySQL:
environment:
- DATABASE_URL=mysql://${MYSQL_USER}:${MYSQL_PASSWORD}@mysql:3306/meshmonitor6. Start MeshMonitor β
docker compose up -d meshmonitor7. Verify Operation β
Check the logs to confirm MySQL is being used:
docker compose logs meshmonitor | grep -i "database\|mysql"You should see:
[INFO] Database: MySQL (configured via DATABASE_URL)
[INFO] [DatabaseService] Using MySQL driver for Drizzle repositories
[INFO] [MySQL Driver] Database initialized successfullyDatabase Schema β
MeshMonitor uses Drizzle ORM for type-safe database operations. The schema supports SQLite, PostgreSQL, and MySQL/MariaDB with automatic dialect handling.
Core Tables β
| Table | Description |
|---|---|
nodes | Meshtastic node information |
messages | Chat messages and packets |
channels | Channel configuration |
telemetry | Device telemetry data |
settings | Application settings |
traceroutes | Route tracing results |
neighbor_info | Neighbor node information |
Authentication Tables β
| Table | Description |
|---|---|
users | User accounts |
sessions | Active sessions |
permissions | User permissions |
api_tokens | API authentication tokens |
audit_log | Security audit trail |
Additional Tables β
| Table | Description |
|---|---|
push_subscriptions | Web push notification subscriptions |
user_notification_preferences | Per-user notification settings |
backup_history | Backup operation history |
custom_themes | User-created themes |
For the complete schema definition, see src/db/schema/.
Database Maintenance β
SQLite β
SQLite maintenance is largely automatic. MeshMonitor enables:
- WAL mode - Better concurrency and crash recovery
- Automatic checkpointing - Keeps the WAL file size manageable
Manual vacuum (optional, for reclaiming disk space):
docker compose exec meshmonitor sqlite3 /data/meshmonitor.db "VACUUM;"PostgreSQL β
PostgreSQL has built-in maintenance, but consider:
-- Reclaim disk space
VACUUM ANALYZE;
-- Check database size
SELECT pg_size_pretty(pg_database_size('meshmonitor'));
-- Check table sizes
SELECT relname, pg_size_pretty(pg_total_relation_size(relid))
FROM pg_catalog.pg_statio_user_tables
ORDER BY pg_total_relation_size(relid) DESC;MySQL / MariaDB β
MySQL/MariaDB have built-in maintenance, but consider:
-- Optimize tables (reclaim space and defragment)
OPTIMIZE TABLE nodes, messages, telemetry;
-- Check database size
SELECT
table_schema AS 'Database',
ROUND(SUM(data_length + index_length) / 1024 / 1024, 2) AS 'Size (MB)'
FROM information_schema.tables
WHERE table_schema = 'meshmonitor'
GROUP BY table_schema;
-- Check table sizes
SELECT
table_name AS 'Table',
ROUND((data_length + index_length) / 1024 / 1024, 2) AS 'Size (MB)'
FROM information_schema.tables
WHERE table_schema = 'meshmonitor'
ORDER BY (data_length + index_length) DESC;Backups β
SQLite Backup β
# Stop MeshMonitor for consistent backup
docker compose stop meshmonitor
# Copy database file
docker cp meshmonitor:/data/meshmonitor.db ./backup-$(date +%Y%m%d).db
# Restart MeshMonitor
docker compose start meshmonitorOr use the built-in System Backup feature.
PostgreSQL Backup β
# Using pg_dump
docker compose exec postgres pg_dump -U meshmonitor meshmonitor > backup-$(date +%Y%m%d).sql
# Compressed backup
docker compose exec postgres pg_dump -U meshmonitor meshmonitor | gzip > backup-$(date +%Y%m%d).sql.gzRestore PostgreSQL β
# Drop and recreate database
docker compose exec postgres psql -U meshmonitor -c "DROP DATABASE meshmonitor; CREATE DATABASE meshmonitor;"
# Restore from backup
cat backup.sql | docker compose exec -T postgres psql -U meshmonitor meshmonitorMySQL / MariaDB Backup β
# Using mysqldump
docker compose exec mysql mysqldump -u meshmonitor -p meshmonitor > backup-$(date +%Y%m%d).sql
# Compressed backup
docker compose exec mysql mysqldump -u meshmonitor -p meshmonitor | gzip > backup-$(date +%Y%m%d).sql.gzRestore MySQL / MariaDB β
# Drop and recreate database
docker compose exec mysql mysql -u root -p -e "DROP DATABASE meshmonitor; CREATE DATABASE meshmonitor CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
# Restore from backup
cat backup.sql | docker compose exec -T mysql mysql -u meshmonitor -p meshmonitorPerformance Tuning β
SQLite β
SQLite generally performs well with default settings. For high-traffic deployments:
-- Increase cache size (default 2000 pages)
PRAGMA cache_size = 10000;
-- Set synchronous mode (trade durability for speed)
PRAGMA synchronous = NORMAL;PostgreSQL β
For production PostgreSQL deployments:
-- Increase shared buffers (25% of RAM)
ALTER SYSTEM SET shared_buffers = '1GB';
-- Increase work memory for complex queries
ALTER SYSTEM SET work_mem = '64MB';
-- Enable parallel queries
ALTER SYSTEM SET max_parallel_workers_per_gather = 2;MySQL / MariaDB β
For production MySQL/MariaDB deployments, add to your my.cnf:
[mysqld]
# Buffer pool size (50-70% of RAM for dedicated server)
innodb_buffer_pool_size = 1G
# Log file size (larger = better performance, longer recovery)
innodb_log_file_size = 256M
# Flush logs once per second instead of each transaction
innodb_flush_log_at_trx_commit = 2
# Query cache (MariaDB only, MySQL 8.0+ removed this)
# query_cache_size = 64M
# query_cache_type = 1Development Guidelines β
When contributing to MeshMonitor's database code, follow these patterns:
Adding New Database Methods β
- Create repository method in
src/db/repositories/:
// src/db/repositories/nodes.ts
async getNodeByIdAsync(nodeId: string): Promise<DbNode | undefined> {
const result = await this.db.select().from(nodes).where(eq(nodes.nodeId, nodeId));
return result[0];
}- Expose through DatabaseService with
Asyncsuffix:
// src/services/database.ts
async getNodeByIdAsync(nodeId: string): Promise<DbNode | undefined> {
return this.nodesRepository.getNodeByIdAsync(nodeId);
}- Use await at all call sites:
const node = await databaseService.getNodeByIdAsync(nodeId);Type Coercion for BIGINT β
PostgreSQL returns BIGINT as strings, MySQL as BigInt objects. Always coerce:
// β Wrong - fails on PostgreSQL
if (row.nodeNum === searchNodeNum)
// β
Correct - works on all databases
if (Number(row.nodeNum) === Number(searchNodeNum))Test Mocking β
When testing routes that use auth middleware, mock async database methods:
vi.mock('../../services/database.js', () => ({
default: {
drizzleDbType: 'sqlite',
findUserByIdAsync: vi.fn(),
findUserByUsernameAsync: vi.fn(),
checkPermissionAsync: vi.fn(),
getUserPermissionSetAsync: vi.fn(),
// ... your route-specific mocks
}
}));Database-Specific Code β
When you need database-specific behavior:
if (this.drizzleDbType === 'sqlite') {
// SQLite-specific: PRAGMA, VACUUM
} else if (this.drizzleDbType === 'postgres') {
// PostgreSQL-specific: sequences, BIGINT casts
} else if (this.drizzleDbType === 'mysql') {
// MySQL-specific: AUTO_INCREMENT
}Reference Documentation β
- Architecture Lessons - Detailed patterns and lessons learned
- Drizzle ORM Docs - Query building and schema definition
Need Help? β
- Check Frequently Asked Questions
- Review Deployment Guide
- Visit GitHub Issues