Deploy RouteMQ using Docker containers for consistent, scalable environments.
# Clone the repository
git clone <your-repo-url>
cd RouteMQ
# Build the Docker image
docker build -t routemq:latest .
# Run the image's default command: /opt/venv/bin/routemq --run
docker run -d \
--name routemq \
-e MQTT_BROKER=test.mosquitto.org \
-e MQTT_PORT=1883 \
routemq:latestThe image enables the health server on 127.0.0.1:8080 inside the container, and its HEALTHCHECK probes it
there, so no port needs publishing. docker ps reports healthy only after the app's /ready passes, which
requires a completed startup and a connected MQTT broker.
Use the repository docker-compose.yml for Redis, MySQL, ClickHouse, the app, and queue workers. Add Mosquitto when you want a local broker instead of test.mosquitto.org.
# Start all services
docker compose up -d
# View logs
docker compose logs -f routemq
# Stop all services
docker compose downThe repository root Dockerfile is the source of truth. This section summarizes it instead of
repeating it; read the file for exact commands.
| Stage | What it does |
|---|---|
uv |
Supplies the pinned uv binary to builder. Nothing from this stage reaches the final image. |
builder |
Installs build packages and runs uv sync --locked --no-dev --no-editable --extra redis --extra clickhouse --extra prometheus into /opt/venv. |
runtime |
The final stage. Copies /opt/venv to the same path, copies app/, and runs as the non-root app user. |
The root Dockerfile has no development or production target. docker build . produces the runtime stage.
- Both
FROMimages, the Python 3.12 slim Debian base and the uv image, are literal references pinned by version tag andsha256digest. Neither is selected throughARGor a floatinglatesttag. - The install is locked. The build copies
uv.lockand runsuv sync --locked, so a missing lockfile, or one that no longer matchespyproject.toml, fails the build. - The image holds RouteMQ and its base dependencies, which include the MySQL driver, plus the
redis,clickhouse, andprometheusextras used bydocker-compose.yml. RouteMQ is installed as a regular package, not an editable one. Development dependencies and thecliandpostgresextras are not installed. - The
runtimestage contains nouv. Containers start/opt/venv/bin/routemqdirectly and download or install nothing at startup.PATHstarts with/opt/venv/bin. /opt/venvsits outside/app, so bind mounts at/app/app,/app/bootstrap, or all of/appleave the environment in place.PYTHONPATH=/appputs mounted source ahead of the installed packages.- The copied source and
/opt/venvstay root-owned. Theappuser owns only the/appdirectory itself, where a generated.envcan be written, and/app/logs. - The default command is
/opt/venv/bin/routemq --run. The queue workers indocker-compose.ymlreplace it with/opt/venv/bin/routemq --queue-work --queue <name> --sleep <seconds>. HEALTHCHECKruns/opt/venv/bin/python -m routemq.healthcheckevery 30 seconds (10-second timeout, 40-second start period, 3 retries). The image setsHEALTH_HTTP_ENABLED=true,HEALTH_HTTP_HOST=127.0.0.1, andHEALTH_HTTP_PORT=8080, so the app and each worker container serve their own/ready. See Health Checks.- The
TIMEZONEbuild argument (defaultAsia/Jakarta) setsTZin both stages.
The required CI build job builds this image and then runs:
python scripts/smoke_docker.py --image routemq:ci-smokeThe script never builds or pulls the RouteMQ image; it tests a tag that already exists locally. It runs the image's
default command and a queue worker against Mosquitto, Redis, and MySQL containers on a private --internal Docker
network, with no published ports, no host .env, and no internet route, so any install or download at startup
fails. First in Redis queue mode and then in database queue mode, it publishes MQTT messages that a fixture route
turns into queued jobs and checks the MySQL rows the worker writes. It also checks that both containers stay ready
after idling past the 30-second Redis worker metadata expiry, that the worker turns unhealthy and recovers when
Redis or MySQL stops and restarts, that the app does the same for Mosquitto, and that both exit 0 on SIGTERM.
Before that, it inspects the image for a non-root user, a virtualenv with the three extras, no development
packages, and no uv.
A missing prerequisite, a failed assertion, or a timeout fails the job; nothing is skipped. The script removes only the containers and network it created. To run it locally, build once and pass the tag:
docker build -t routemq:local .
python scripts/smoke_docker.py --image routemq:localversion: '3.8'
services:
routemq:
build: .
container_name: routemq-app
environment:
# MQTT Configuration
MQTT_BROKER: ${MQTT_BROKER:-mosquitto}
MQTT_PORT: ${MQTT_PORT:-1883}
MQTT_USERNAME: ${MQTT_USERNAME:-}
MQTT_PASSWORD: ${MQTT_PASSWORD:-}
MQTT_GROUP_NAME: ${MQTT_GROUP_NAME:-production_group}
# Database Configuration
ENABLE_MYSQL: ${ENABLE_MYSQL:-true}
DB_HOST: ${DB_HOST:-mysql}
DB_PORT: ${DB_PORT:-3306}
DB_NAME: ${DB_NAME:-routemq_production}
DB_USER: ${DB_USER:-routemq_user}
DB_PASS: ${DB_PASS:-secure_password}
# Redis Configuration
ENABLE_REDIS: ${ENABLE_REDIS:-true}
REDIS_HOST: ${REDIS_HOST:-redis}
REDIS_PORT: ${REDIS_PORT:-6379}
# Application Configuration
LOG_LEVEL: ${LOG_LEVEL:-INFO}
WORKER_COUNT: ${WORKER_COUNT:-3}
depends_on:
mosquitto:
condition: service_healthy
mysql:
condition: service_healthy
redis:
condition: service_healthy
networks:
- routemq-network
restart: unless-stopped
volumes:
- ./app:/app/app:ro # Mount application code (read-only)
- ./logs:/app/logs # Mount logs directory
- ./.env:/app/.env:ro # Mount environment file
deploy:
resources:
limits:
cpus: '2.0'
memory: 1G
reservations:
cpus: '0.5'
memory: 256M
# Same readiness probe as the image HEALTHCHECK; exits 0 only when /ready reports ready
healthcheck:
test: ["CMD", "/opt/venv/bin/python", "-m", "routemq.healthcheck"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
mosquitto:
image: eclipse-mosquitto:2.0
container_name: routemq-mosquitto
ports:
- "1883:1883"
- "9001:9001"
volumes:
- ./docker/mosquitto/mosquitto.conf:/mosquitto/config/mosquitto.conf:ro
- ./docker/mosquitto/passwd:/mosquitto/config/passwd:ro
- mosquitto_data:/mosquitto/data
- mosquitto_logs:/mosquitto/log
networks:
- routemq-network
restart: unless-stopped
healthcheck:
test: ["CMD", "mosquitto_pub", "-h", "localhost", "-t", "health", "-m", "check", "-u", "health", "-P", "check"]
interval: 30s
timeout: 5s
retries: 3
mysql:
image: mysql:8.0
container_name: routemq-mysql
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-root_password}
MYSQL_DATABASE: ${DB_NAME:-routemq_production}
MYSQL_USER: ${DB_USER:-routemq_user}
MYSQL_PASSWORD: ${DB_PASS:-secure_password}
ports:
- "3306:3306"
volumes:
- mysql_data:/var/lib/mysql
- ./docker/mysql/init:/docker-entrypoint-initdb.d:ro
networks:
- routemq-network
restart: unless-stopped
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p${MYSQL_ROOT_PASSWORD:-root_password}"]
interval: 30s
timeout: 10s
retries: 3
redis:
image: redis:7-alpine
container_name: routemq-redis
ports:
- "6379:6379"
volumes:
- redis_data:/data
- ./docker/redis/redis.conf:/usr/local/etc/redis/redis.conf:ro
networks:
- routemq-network
restart: unless-stopped
command: redis-server /usr/local/etc/redis/redis.conf
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 30s
timeout: 5s
retries: 3
volumes:
mysql_data:
redis_data:
mosquitto_data:
mosquitto_logs:
networks:
routemq-network:
driver: bridgeCreate a .env file for configuration:
# MQTT Broker Settings
MQTT_BROKER=mosquitto
MQTT_PORT=1883
MQTT_USERNAME=routemq_user
MQTT_PASSWORD=mqtt_secure_password
MQTT_GROUP_NAME=production_group
# Database Settings
ENABLE_MYSQL=true
DB_HOST=mysql
DB_PORT=3306
DB_NAME=routemq_production
DB_USER=routemq_user
DB_PASS=db_secure_password
# Redis Settings
ENABLE_REDIS=true
REDIS_HOST=redis
REDIS_PORT=6379
REDIS_PASSWORD=redis_secure_password
# Application Settings
LOG_LEVEL=INFO
WORKER_COUNT=3
ENVIRONMENT=production
# Security Settings
SECRET_KEY=your-secret-key-here
JWT_SECRET=jwt-secret-key-here
# MySQL Root Password (for container)
MYSQL_ROOT_PASSWORD=mysql_root_password# .env.docker - Docker-specific overrides
MQTT_BROKER=mosquitto
DB_HOST=mysql
REDIS_HOST=redisCreate docker/mosquitto/mosquitto.conf:
# docker/mosquitto/mosquitto.conf
listener 1883
protocol mqtt
listener 9001
protocol websockets
# Persistence
persistence true
persistence_location /mosquitto/data/
# Logging
log_dest file /mosquitto/log/mosquitto.log
log_type error
log_type warning
log_type notice
log_type information
# Security
allow_anonymous false
password_file /mosquitto/config/passwd
# Shared subscriptions (for scaling)
max_queued_messages 1000
message_size_limit 268435456Create docker/mosquitto/passwd:
# Generate password file
docker run -it --rm -v $(pwd)/docker/mosquitto:/mosquitto/config eclipse-mosquitto:2.0 \
mosquitto_passwd -c /mosquitto/config/passwd routemq_user
# Or manually create (replace with actual hash)
echo "routemq_user:$6$hash_here" > docker/mosquitto/passwdCreate docker/mysql/init/01-init.sql:
-- docker/mysql/init/01-init.sql
-- Database initialization script
CREATE DATABASE IF NOT EXISTS routemq_production
CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;
-- Create application user
CREATE USER IF NOT EXISTS 'routemq_user'@'%' IDENTIFIED BY 'secure_password';
GRANT ALL PRIVILEGES ON routemq_production.* TO 'routemq_user'@'%';
-- Create read-only user for monitoring
CREATE USER IF NOT EXISTS 'routemq_readonly'@'%' IDENTIFIED BY 'readonly_password';
GRANT SELECT ON routemq_production.* TO 'routemq_readonly'@'%';
FLUSH PRIVILEGES;
USE routemq_production;
-- Create RouteMQ-managed tables here, or set DB_AUTO_CREATE_TABLES=true
-- for controlled development environments.Create docker/redis/redis.conf:
# docker/redis/redis.conf
# Redis configuration for production
# Network
bind 0.0.0.0
port 6379
timeout 300
# Persistence
save 900 1
save 300 10
save 60 10000
appendonly yes
appendfsync everysec
# Memory
maxmemory 256mb
maxmemory-policy allkeys-lru
# Security
requirepass redis_secure_password
# Logging
loglevel noticeThe root Dockerfile has no development target and installs no development dependencies. Run Redis, MySQL,
and ClickHouse from docker-compose.dev.yml and run the app on the host:
docker compose -f docker-compose.dev.yml up -d
uv run routemq runTo run the app in a container too, start the optional full profile. It builds the same runtime image and
bind-mounts ./app and ./bootstrap into /app. PYTHONPATH=/app loads the mounted source ahead of the
installed packages, and the environment in /opt/venv is unaffected.
docker compose -f docker-compose.dev.yml --profile full up -dProduction uses the same runtime image. Build and tag it, then reference the tag:
docker build -t routemq:production .# docker-compose.prod.yml
version: '3.8'
services:
routemq:
image: routemq:production
environment:
- LOG_LEVEL=INFO
- ENABLE_MYSQL=true
volumes:
- ./app:/app/app:ro # Read-only application code
- ./logs:/app/logs # Logs volume
deploy:
replicas: 3 # Multiple instances for HA
resources:
limits:
cpus: '1.0'
memory: 512MThe root Dockerfile is already multi-stage: uv and builder produce /opt/venv, and runtime is the image you
run. See Dockerfile Explanation. There are no separate development and production
stages to select with --target.
# Build the runtime image
docker build -t routemq:latest .
# Build with specific version
docker build -t routemq:v1.0.0 .# Run with the local checkout mounted over /app; the environment stays in /opt/venv
docker run -it --rm \
-v $(pwd):/app \
-e LOG_LEVEL=DEBUG \
routemq:latest
# Run production container
docker run -d \
--name routemq-prod \
-e MQTT_BROKER=mqtt.example.com \
--restart unless-stopped \
routemq:v1.0.0
# Run a queue worker instead of the default app command
docker run -d --name routemq-worker routemq:v1.0.0 \
/opt/venv/bin/routemq queue-work --queue default --sleep 3
# Run with custom command
docker run -it --rm routemq:latest /opt/venv/bin/python -c "print('Hello')"# View logs
docker logs -f routemq
# Execute commands in running container
docker exec -it routemq bash
# Inspect container
docker inspect routemq
# View resource usage
docker stats routemqContainer Won't Start
# Check logs
docker logs routemq
# Check configuration
docker run --rm routemq:latest env
# Verify the runtime dependencies and extras import
docker run --rm routemq:latest /opt/venv/bin/python -c "import paho.mqtt.client, redis, clickhouse_connect, prometheus_client"Database Connection Issues
# Test MySQL connection
docker run --rm --network routemq_routemq-network mysql:8.0 \
mysql -h mysql -u routemq_user -p -e "SHOW DATABASES;"
# Check network connectivity
docker run --rm --network routemq_routemq-network routemq:latest \
/opt/venv/bin/python -c "import socket; socket.create_connection(('mysql', 3306), timeout=5); print('mysql:3306 reachable')"Memory Issues
# Monitor memory usage
docker stats
# Increase memory limits
docker run -m 1g routemq:latest# Run the image's readiness probe; exit code 0 means /ready passed
docker exec routemq /opt/venv/bin/python -m routemq.healthcheck; echo $?
# Read the readiness payload on the default health port
docker exec routemq curl -s http://127.0.0.1:8080/ready
# Show the results Docker recorded for the image HEALTHCHECK
docker inspect --format '{{json .State.Health}}' routemqApp readiness covers startup and the MQTT connection; it does not check MySQL or Redis. A queue worker
container's /ready checks the worker loop and its queue backend instead: Redis PING, or SELECT 1 plus the
queue tables for the database queue. To test MySQL directly, use the MySQL client shown under Database Connection
Issues. Docker marks a failing container unhealthy but does not restart it; see
Health Checks.
The root image already runs as the non-root app system user without a login shell, and its source and
/opt/venv are root-owned. Further restrictions are run-time options:
# Read-only file system (where possible)
# docker run --read-only routemq:latest
# Drop capabilities
# docker run --cap-drop ALL routemq:latest# Isolate networks
networks:
frontend:
driver: bridge
backend:
driver: bridge
internal: true # No external access
services:
routemq:
networks:
- frontend
- backend
mysql:
networks:
- backend # Only internal access# Use Docker secrets
secrets:
db_password:
file: ./secrets/db_password.txt
mqtt_password:
file: ./secrets/mqtt_password.txt
services:
routemq:
secrets:
- db_password
- mqtt_password
environment:
DB_PASS_FILE: /run/secrets/db_password
MQTT_PASSWORD_FILE: /run/secrets/mqtt_passwordservices:
routemq:
deploy:
resources:
limits:
cpus: '2.0'
memory: 1G
reservations:
cpus: '0.5'
memory: 256Mvolumes:
# Use named volumes for better performance
- mysql_data:/var/lib/mysql
# Mount application code read-only
- ./app:/app/app:ro
# Use tmpfs for temporary files
- type: tmpfs
target: /tmp
tmpfs:
size: 100M- Configuration Reference - Configure production settings
- Scaling - Scale your Docker deployment
- Load Balancing - Distribute traffic across containers
- Security - Secure your containerized deployment