Back to Blog
Cloud & DevOps

Containerizing IoT Backend Services with Docker: From Dev to Production

Running your MQTT bridge directly on a server is fine until the server gets a kernel update and your process silently changes behaviour. Docker gives IoT backends the reproducibility, isolation, and deployment predictability they need at every scale.

October 5, 2024
12 min read
DockerIoT BackendContainerizationMQTT

Containerizing IoT Backend Services with Docker: From Dev to Production

IoT backends are not single services. A typical production system includes an MQTT bridge, a REST API, a WebSocket server, a time-series database, a visualisation layer, and often a rules engine. Running all of these as bare processes on a single server creates a brittle, undocumented environment that is impossible to replicate and painful to scale.

Docker solves this by making each service a self-contained, reproducible unit. The same image that runs on a developer's MacBook runs identically on an AWS ECS Fargate task in production. No "works on my machine" failures. No undocumented system dependencies. No manual installation steps that differ between environments.

Why IoT Backends Benefit Especially from Containers

IoT services have characteristics that make containerisation particularly valuable:

Long-lived processes with strict resource contracts: An MQTT bridge holding 5,000 persistent connections needs a predictable memory ceiling. Docker's --memory flag enforces this; a bare process can leak into available RAM and crash the server.

Version-pinned protocol dependencies: MQTT broker versions matter. Mosquitto 2.x has a different default ACL model than Mosquitto 1.6. Pin the version in your Docker image and every environment gets exactly that behaviour.

Multi-service coordination: Your MQTT bridge needs to start after the broker but before the API. Docker Compose depends_on with health checks enforces this ordering reliably.

Environment parity: The gap between development and production is where most IoT bugs hide. If a developer runs InfluxDB 2.6 locally and production runs 2.4, query syntax differences cause failures that are hard to diagnose. Containers pin the version everywhere.

The Full Local Stack with Docker Compose

docker-compose.yml

version: '3.9'

services: mosquitto: image: eclipse-mosquitto:2.0.18 restart: unless-stopped ports: - "1883:1883" - "8883:8883" # TLS - "9001:9001" # WebSocket volumes: - ./config/mosquitto:/mosquitto/config:ro - mosquitto-data:/mosquitto/data - mosquitto-log:/mosquitto/log healthcheck: test: ["CMD", "mosquitto_sub", "-t", "$$SYS/#", "-C", "1", "-i", "healthcheck", "-W", "3"] interval: 15s timeout: 10s retries: 3

mqtt-bridge: build: context: ./services/mqtt-bridge dockerfile: Dockerfile target: production restart: unless-stopped depends_on: mosquitto: condition: service_healthy influxdb: condition: service_healthy environment: - NODE_ENV=production - MQTT_URL=mqtt://mosquitto:1883 - INFLUX_URL=http://influxdb:8086 - INFLUX_TOKEN_FILE=/run/secrets/influx_token secrets: - influx_token ports: - "3001:3001" healthcheck: test: ["CMD", "wget", "-qO-", "http://localhost:3001/health"] interval: 30s timeout: 5s retries: 3

api: build: context: ./services/api dockerfile: Dockerfile target: production restart: unless-stopped depends_on: mqtt-bridge: condition: service_healthy environment: - NODE_ENV=production - DATABASE_URL=postgresql://postgres:5432/iotdb - JWT_SECRET_FILE=/run/secrets/jwt_secret secrets: - jwt_secret ports: - "3000:3000"

influxdb: image: influxdb:2.7-alpine restart: unless-stopped environment: - DOCKER_INFLUXDB_INIT_MODE=setup - DOCKER_INFLUXDB_INIT_USERNAME=admin - DOCKER_INFLUXDB_INIT_PASSWORD_FILE=/run/secrets/influx_admin_password - DOCKER_INFLUXDB_INIT_ORG=codecaracal - DOCKER_INFLUXDB_INIT_BUCKET=telemetry - DOCKER_INFLUXDB_INIT_RETENTION=90d secrets: - influx_admin_password volumes: - influxdb-data:/var/lib/influxdb2 healthcheck: test: ["CMD", "influx", "ping"] interval: 20s timeout: 5s retries: 5

grafana: image: grafana/grafana:10.2.3 restart: unless-stopped depends_on: influxdb: condition: service_healthy environment: - GF_SECURITY_ADMIN_PASSWORD__FILE=/run/secrets/grafana_admin_password - GF_INSTALL_PLUGINS=grafana-worldmap-panel secrets: - grafana_admin_password volumes: - grafana-data:/var/lib/grafana - ./config/grafana/dashboards:/etc/grafana/provisioning/dashboards:ro - ./config/grafana/datasources:/etc/grafana/provisioning/datasources:ro ports: - "3100:3000"

secrets: influx_token: file: ./secrets/influx_token.txt influx_admin_password: file: ./secrets/influx_admin_password.txt jwt_secret: file: ./secrets/jwt_secret.txt grafana_admin_password: file: ./secrets/grafana_admin_password.txt

volumes: mosquitto-data: mosquitto-log: influxdb-data: grafana-data:

Production Dockerfile for the MQTT Bridge Service

The multi-stage build pattern is essential: the build stage installs dev dependencies and compiles TypeScript; the production stage copies only the compiled output and production dependencies.

services/mqtt-bridge/Dockerfile

── Build stage ──────────────────────────────────────────────────────────────

FROM node:20-alpine AS builder

WORKDIR /app

COPY package*.json ./ RUN npm ci --include=dev

COPY tsconfig.json ./ COPY src/ ./src/ RUN npm run build # tsc → dist/

── Production stage ─────────────────────────────────────────────────────────

FROM node:20-alpine AS production

Non-root user for security

RUN addgroup -g 1001 -S nodejs && adduser -S iotservice -u 1001 -G nodejs

WORKDIR /app

Only production dependencies

COPY package*.json ./ RUN npm ci --omit=dev && npm cache clean --force

Copy compiled output from builder

COPY --from=builder --chown=iotservice:nodejs /app/dist ./dist

USER iotservice

Health check endpoint

EXPOSE 3001 HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 CMD wget -qO- http://localhost:3001/health || exit 1

CMD ["node", "dist/index.js"]

Secrets Management: Docker Secrets vs Environment Variables

Never pass secrets as plain environment variables in production. They appear in docker inspect output, process listings, and container logs.

| Method | Dev | Production | Secret exposure risk | |---|---|---|---| | Plain env var | OK | Avoid | High (visible in docker inspect) | | .env file | OK | Avoid | High (often committed to git) | | Docker secrets | Good | Recommended | Low (mounted as tmpfs file) | | AWS Secrets Manager | Good | Best | Very low (fetched at runtime) |

In your Node.js service, read secrets from files:

const fs = require('fs');

function readSecret(name) { const secretPath = /run/secrets/${name}; if (fs.existsSync(secretPath)) { return fs.readFileSync(secretPath, 'utf8').trim(); } // Fallback to environment variable for local dev without Docker secrets return process.env[name.toUpperCase()]; }

const INFLUX_TOKEN = readSecret('influx_token'); const JWT_SECRET = readSecret('jwt_secret');

ECS Task Definition for Production

When you move from local Docker Compose to AWS ECS Fargate in production, the task definition mirrors your Compose service configuration — with secrets fetched from AWS Secrets Manager instead of local files:

{
  "containerDefinitions": [{
    "name": "mqtt-bridge",
    "image": "ACCOUNT_ID.dkr.ecr.us-east-1.amazonaws.com/mqtt-bridge:v2.4.1",
    "cpu": 256,
    "memory": 512,
    "secrets": [
      {
        "name": "INFLUX_TOKEN",
        "valueFrom": "arn:aws:secretsmanager:us-east-1:ACCOUNT_ID:secret:iot/influx-token"
      }
    ],
    "healthCheck": {
      "command": ["CMD-SHELL", "wget -qO- http://localhost:3001/health || exit 1"],
      "interval": 30,
      "timeout": 5,
      "retries": 3,
      "startPeriod": 10
    },
    "logConfiguration": {
      "logDriver": "awslogs",
      "options": {
        "awslogs-group": "/ecs/mqtt-bridge",
        "awslogs-region": "us-east-1",
        "awslogs-stream-prefix": "ecs"
      }
    }
  }]
}

The discipline of containerising from day one means that when you need to scale from 1 ECS task to 10 — or migrate from ECS to EKS — no application code changes. Only infrastructure configuration changes.

---

Need your IoT backend containerised and deployed to production on AWS? [Contact Code Caracal](/contact) — we handle the full Docker and ECS architecture as part of our cloud backend engagements.

Written by CodeCaracal Engineering

We write from production experience — every technique in our articles has been deployed to real clients. No academic theory.

More Articles

Business · 12 min read

IoT Device Compliance: FCC, CE, and Product Certification Guide for Hardware Startups

Business · 11 min read

What to Look for When Hiring an IoT Development Partner: 8 Critical Criteria

Business · 11 min read

IoT MVP to Production: Realistic Timeline and Budget for Hardware Startups

Business · 11 min read

IoT Development Agency vs Building In-House: A Decision Framework for Founders

IoT Dashboard · 13 min read

Next.js IoT Analytics Dashboard: From Sensor Data to Production App

Business · 11 min read

How Much Does It Cost to Build an IoT Product in 2024? A Realistic Breakdown

IoT Dashboard · 11 min read

IoT Dashboard UX: Design Principles for Industrial Monitoring Interfaces

IoT Dashboard · 12 min read

Node.js WebSocket Server: The Real-Time Backend for IoT Dashboards

IoT Dashboard · 14 min read

Grafana + InfluxDB IoT Monitoring: Complete Production Setup Guide

IoT Dashboard · 12 min read

Building Real-Time IoT Dashboards with React and Recharts

Cloud & DevOps · 13 min read

CI/CD for Embedded Firmware: Automated Build, Test, and OTA Release Pipeline

Mobile Development · 12 min read

Flutter Offline-First IoT Apps: Hive + Sync Architecture That Works in the Field

Cloud & DevOps · 14 min read

Terraform for IoT Infrastructure: Provisioning AWS IoT Core, Lambda, and InfluxDB as Code

Mobile Development · 10 min read

Flutter IoT Alerts: Firebase Push Notifications for Device Events

Cloud & DevOps · 12 min read

Deploying IoT Backends on AWS: ECS Fargate vs Lambda vs EC2 Decision Guide

Mobile Development · 11 min read

Flutter + MQTT: Building Production IoT Mobile Apps That Scale

Mobile Development · 13 min read

Flutter BLE: Building a Bluetooth IoT Controller App from Scratch

Cloud & DevOps · 13 min read

AWS IoT Core vs Azure IoT Hub vs Google Cloud IoT: 2024 Honest Comparison

IoT Engineering · 13 min read

Kafka vs RabbitMQ for IoT: Choosing the Right Message Queue for High-Volume Telemetry

IoT Engineering · 14 min read

IoT System Testing: Unit, Integration, Hardware-in-the-Loop, and End-to-End

IoT Engineering · 14 min read

Predictive Maintenance with IoT Sensor Data: From Threshold to Machine Learning

Embedded Systems · 14 min read

IoT Bootloader Design: Secure Boot, A/B Partitions, and Reliable OTA Recovery

IoT Engineering · 14 min read

Multi-Tenant IoT Platform Architecture: Isolation, Scaling, and Data Partitioning

Embedded Systems · 14 min read

Memory Management in Embedded Firmware: Avoiding Heap Fragmentation and Stack Overflows

IoT Engineering · 13 min read

IoT Cost Optimization: How We Cut AWS IoT Bills by 60% Without Sacrificing Reliability

IoT Engineering · 12 min read

Edge Computing in IoT: When to Process On-Device vs In the Cloud

IoT Engineering · 13 min read

Digital Twins for IoT: Building a Virtual Mirror of Your Physical Devices

Embedded Systems · 14 min read

ESP32 Deep Sleep Mastery: Cutting Power Consumption from 240mA to 10µA

IoT Engineering · 10 min read

MQTT QoS 0, 1, and 2 Explained: Choosing the Right Level for IoT

IoT Engineering · 14 min read

IoT Monitoring and Observability: Metrics, Logs, and Distributed Tracing

Embedded Systems · 14 min read

Debugging Embedded Firmware: JTAG, GDB, Logic Analyzers, and Serial Tracing

IoT Engineering · 12 min read

WebSocket vs MQTT vs Server-Sent Events: Real-Time IoT Protocol Deep Dive

Embedded Systems · 13 min read

STM32 HAL vs Low-Level Drivers: When the Abstraction Costs You Too Much

IoT Engineering · 13 min read

IoT Data Pipeline: From Raw Sensor Reading to Live Dashboard in Under 100ms

IoT Engineering · 13 min read

Zero-Touch IoT Device Provisioning: Scaling from 10 to 100,000 Devices

Embedded Systems · 13 min read

UART vs SPI vs I2C: Choosing the Right Protocol for Sensor Integration

IoT Engineering · 12 min read

Real-Time IoT Alerting: From Simple Thresholds to ML Anomaly Detection

Embedded Systems · 12 min read

ESP32 Partition Table: Designing Flash Layout for Production Firmware

IoT Engineering · 12 min read

IoT Architecture Patterns: Hub-and-Spoke, Mesh, and Edge-Cloud Hybrid

Embedded Systems · 13 min read

IoT Battery Life Optimization: Engineering Devices That Last Years on a Single Charge

IoT Engineering · 13 min read

Time-Series Databases for IoT: InfluxDB vs TimescaleDB vs AWS Timestream

Security · 14 min read

Zero-Trust Security for Embedded IoT: Why Your Devices Are Probably Vulnerable

Embedded Systems · 14 min read

FreeRTOS on ESP32: Task Scheduling, Queues, and Resource Management for IoT

IoT Engineering · 12 min read

Building a Production IoT Gateway with Raspberry Pi and Node.js

Embedded Systems · 13 min read

ESP32 vs STM32: Choosing the Right Microcontroller for Your IoT Project

Mobile Development · 10 min read

Flutter + WebSocket: Building Real-Time IoT Dashboards That Don't Stutter

IoT Engineering · 13 min read

IoT Fleet Management at Scale: AWS IoT Core Device Registry and Provisioning

IoT Engineering · 11 min read

MQTT vs HTTP for IoT: Which Protocol Wins in Production?

IoT Engineering · 12 min read

ESP32 → MQTT → AWS IoT Core: The Production-Grade Architecture Guide

Let's Build Together

Got an IoT challenge?
We've shipped it.

Whether you need a fleet to track, a factory to monitor, or a farm to automate — our team has done it before and we'd love to build it with you. Typical response time: under 24 hours.

No upfront commitment99.9% uptime SLANDA on requestFixed-price options