Docker lets you run multiple IoT services (MQTT broker, Node-RED, a dashboard) on one VPS as isolated containers, each with its own dependencies, instead of installing everything directly onto the same operating system where version conflicts and messy uninstalls eventually become a problem. Docker Compose defines the whole stack in one file, started and stopped together.
The problem this solves
Installing Mosquitto, Node-RED and a database directly onto a VPS works fine until you need to upgrade one of them, or remove one cleanly, or move the exact same setup to a new server. Each piece of software has its own dependencies, its own config file locations, and its own way of being installed, and over time a hand-built server accumulates cruft that’s hard to fully account for. Docker contains each service in its own isolated environment, with dependencies bundled in, so they don’t interfere with each other and the whole stack can be torn down and rebuilt from one definition file.
Installing Docker
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh
sudo usermod -aG docker
The last line lets you run Docker commands without typing sudo every time, useful but worth being aware of: anyone in the docker group effectively has root-equivalent access to the host, since Docker’s daemon runs as root. Fine for a single-admin VPS, worth thinking about if you’re sharing server access with others.
A real example: MQTT, Node-RED and a dashboard together
This is what a docker-compose.yml combining services already covered on this site looks like in practice:
version: "3.8"
services:
mosquitto:
image: eclipse-mosquitto:latest
ports:
- "8883:8883"
volumes:
- ./mosquitto/config:/mosquitto/config
- mosquitto-data:/mosquitto/data
restart: unless-stopped
nodered:
image: nodered/node-red:latest
ports:
- "1880:1880"
volumes:
- nodered-data:/data
depends_on:
- mosquitto
restart: unless-stopped
grafana:
image: grafana/grafana:latest
ports:
- "3000:3000"
volumes:
- grafana-data:/var/lib/grafana
restart: unless-stopped
volumes:
mosquitto-data:
nodered-data:
grafana-data:
Bring the whole stack up with one command:
docker compose up -d
All three services start, on their own network, able to reach each other by service name (Node-RED can reach Mosquitto at the hostname mosquitto, no IP address needed), with no manual installation of Node.js, Mosquitto, or Grafana’s dependencies required on the host system at all.
Named volumes: why they matter
The volumes: section above is doing important work. Without it, any data a container writes (Node-RED’s flows, Grafana’s dashboards, Mosquitto’s message store) disappears the moment that container is removed or rebuilt. Named volumes persist data outside the container’s own lifecycle, so updating an image (docker compose pull && docker compose up -d) doesn’t wipe your actual configuration and history.
Container security: what actually matters
- Never expose the Docker socket to a container (avoid mounting
/var/run/docker.sockinto any container unless you specifically understand why, since it effectively grants that container root access to the host). - Avoid
--privilegedmode unless a specific piece of hardware access genuinely requires it. Almost nothing on this site’s stack does. - Only publish the ports you actually need externally. The same firewall principle from the security checklist applies on top of Docker’s own port mapping, not instead of it.
- Pin image versions in production rather than always pulling
:latest, once a stack is working well, so an upstream update doesn’t change behaviour underneath you unexpectedly.
Docker vs installing directly: when each makes sense
| Approach | Best for | Trade-off |
|---|---|---|
| Direct install (apt) | Single service, simplest possible setup | Harder to cleanly remove or run multiple versions side by side |
| Docker / Docker Compose | Multiple services on one VPS, reproducible setups | Slight learning curve, small additional resource overhead |
Most guides on this site cover the direct-install approach because it’s the simplest starting point. Once you’re running three or more services on the same VPS, Docker Compose genuinely starts paying for itself in reduced complexity, not added complexity, which is the opposite of how it initially feels to newcomers.
Resource overhead: is it actually heavier?
Docker’s overhead is smaller than reputation suggests, typically a few percent of CPU and a modest amount of RAM per container for the container runtime itself. On the small VPS sizes covered throughout this site (2-4GB RAM), running three to four lightweight services in Docker is comfortable, not a meaningful resource penalty compared to running them directly.
Troubleshooting common problems
A container won’t start, exits immediately. Check its logs first, always: docker compose logs servicename almost always shows the actual error, whether it’s a config file syntax problem, a port conflict, or a missing environment variable. Resist the urge to guess before checking this.
“Port is already allocated” error. Another process, container or otherwise, is already bound to that port on the host. Check with sudo lsof -i :PORTNUMBER to see what’s using it, then either stop that process or change the port mapping in your Compose file.
Containers can’t reach each other by service name. Confirm they’re actually on the same Docker network; Compose creates one automatically for all services defined in the same file, but if you’re running separate docker run commands rather than Compose, services won’t share a network unless explicitly configured to.
Data disappeared after updating an image. Almost always means a named volume wasn’t actually in place for that service, or the volume name changed between Compose file versions. Confirm with docker volume ls that the expected volume exists and is genuinely being used, not silently replaced with an anonymous one.
Disk filling up unexpectedly. Old, unused images and stopped containers accumulate over time. docker system prune clears this safely (it won’t touch named volumes or running containers), worth running periodically rather than only when disk space becomes a visible problem.
Logging and monitoring a multi-container stack
As a stack grows past two or three services, checking logs one container at a time becomes tedious. docker compose logs -f (without specifying a service) tails every container’s logs together, interleaved, which is often enough for a small personal or business setup. For anything more involved, shipping container logs into the same Grafana/InfluxDB stack already covered elsewhere on this site, via a log-forwarding tool, keeps everything in one place rather than requiring separate tooling just for logs.
A note on Docker Compose versions
Modern Docker installations include Compose as a built-in subcommand (docker compose, no hyphen), which has replaced the older standalone docker-compose tool. Both work similarly for most purposes covered on this site, but the built-in version is the one Docker’s own installation script sets up by default now, and the one assumed throughout this guide.
Restart policies: getting them right
The restart: unless-stopped policy used in the example above means a container restarts automatically after a crash or server reboot, but stays stopped if you deliberately stop it yourself, generally the right default for IoT services that should be reliably running. The alternative restart: always can fight you if you’re deliberately trying to keep something stopped for maintenance, restarting it anyway, which is a common source of confusion the first time someone encounters it.
Frequently asked questions
Do I need to learn Kubernetes for this?
No. Kubernetes solves problems at a scale (many servers, automatic failover, complex orchestration) that almost nothing on this site needs. Docker Compose on a single VPS is the right tool for the projects covered here.
How do I update a service without losing its data?
With named volumes in place as shown above: docker compose pull to fetch the new image, then docker compose up -d to recreate the container using it. The volume persists across this, so configuration and history survive the update.
Can I run ThingsBoard in this same Docker Compose file?
Yes, ThingsBoard’s own official deployment is Docker-based, as covered in ThingsBoard Self-Hosted, and can sit alongside other services in one Compose file if you want everything managed together.
What VPS specs does a Docker-based stack like the example above need?
2 vCPU and 4GB RAM is comfortable for the three-service example shown. LumaDock‘s mid-tier plans fit this comfortably with headroom to add more services later.
Should I run a database (PostgreSQL, InfluxDB) in Docker too, or install it directly?
Docker is generally fine for databases on a small project, with the same named-volume discipline as everything else, though some people prefer installing a primary database directly on the host for slightly simpler backup tooling. Both approaches are common and reasonable; consistency with the rest of your stack matters more than which one you pick.
Handling secrets and configuration properly
The example Compose file earlier in this guide keeps things simple, but real deployments usually need to pass credentials (database passwords, API keys) into containers without hardcoding them directly into a file that might end up in version control. Docker Compose supports a separate .env file for exactly this, referenced automatically by Compose and easy to exclude from any Git repository:
# .env file, in the same directory as docker-compose.yml
POSTGRES_PASSWORD=your-actual-password
# referenced in docker-compose.yml
environment:
- POSTGRES_PASSWORD=
This keeps actual secrets out of the main Compose file, which is the part more likely to be shared, copied between servers, or accidentally committed to a public repository.
Health checks: catching problems before they cascade
Docker Compose supports a healthcheck block per service, letting Docker itself periodically verify a container is genuinely working, not just running:
healthcheck:
test: ["CMD", "mosquitto_sub", "-t", "$/#", "-C", "1"]
interval: 30s
timeout: 5s
retries: 3
This matters specifically for multi-service stacks where one service depends on another: a healthcheck lets dependent services wait for a real “ready” signal rather than just “the container started”, which is the difference between Node-RED starting cleanly and Node-RED starting before Mosquitto is actually accepting connections yet.
How do I back up Docker volumes specifically?
The simplest reliable method for most setups on this site: a full VPS snapshot via your provider, which captures volumes along with everything else. For volume-specific backups without a full snapshot, docker run --rm -v volumename:/data -v $(pwd):/backup busybox tar czf /backup/backup.tar.gz /data creates a portable archive of a single named volume.
Is it safe to run Docker on the same VPS as services installed directly (not in Docker)?
Yes, this is a common and entirely reasonable mixed setup, for example Mosquitto installed directly alongside Node-RED and Grafana running in Docker. They share the same host resources but don’t otherwise interfere with each other.
What’s the difference between a bind mount and a named volume?
A bind mount maps a specific host directory directly into a container, useful when you want to edit config files from the host filesystem easily. A named volume is managed entirely by Docker, generally the better default for application data since it’s more portable between hosts and less prone to host-specific path issues.
How Docker networking actually works between services
Beyond simply knowing services can reach each other by name, it’s worth understanding why: Compose creates a private network for the whole stack by default, with an internal DNS resolving each service’s name to its container’s internal IP address automatically. This is why Node-RED can connect to Mosquitto using the hostname mosquitto rather than a hardcoded IP address, and why that connection keeps working even after a container restart changes its underlying IP, something that would otherwise be a constant source of broken configuration in a non-containerised setup with services moved between machines.
Can I limit how much CPU or memory a single container uses?
Yes, via deploy.resources.limits in the Compose file, useful for preventing one misbehaving service from starving the others on a small VPS. Worth setting on anything experimental or third-party until you trust its resource behaviour under real load.
Do containers need their own separate IP addresses on the VPS?
No, by default they share the host’s single public IP, with Docker handling port mapping to route traffic to the right container. This is generally simpler to manage than separate IPs per service for the scale of project covered on this site.
What’s the simplest way to see what’s actually running right now?
docker compose ps lists every service defined in the current directory’s Compose file along with its status, while docker ps shows every container running on the host regardless of which Compose file (if any) created it.