← All modules

6.4 Docker Deployment

Draft — not verified

Theoretical Background: Docker Deployment and System Testing

Module 6 Theory: Docker Deployment and System Testing

Learning Objectives

  • Understand the multi-service Docker Compose architecture of ArbiterROS
  • Analyse the ROS 2 Dockerfile construction and image selection constraints
  • Understand the Raspberry Pi 4B deployment pipeline via deploy-pi.sh
  • Study the sim-to-real transition and its architectural requirements
  • Apply the system testing strategy across simulation, virtual ROS 2, and hardware
  • Understand the TLS proxy pattern used in cloud deployment

6.4.1 Docker for Robotics

Containers package the entire software stack — ROS 2 Humble, rosbridge, Python nodes, Nuxt 3 — into reproducible units that run identically in simulation and on physical hardware, with only port mappings and device paths changing between environments.

Dockerfile for the ROS 2 Backend:

# backend/Dockerfile
FROM ros:humble                  # IMPORTANT: NOT ros:humble-ros-base
                                 # ros2cli is broken in the base image

RUN apt-get update && apt-get install -y \
    ros-humble-rosbridge-server  \
    ros-humble-nav2-bringup      \
    ros-humble-slam-toolbox      \
    python3-pip                  \
    && rm -rf /var/lib/apt/lists/*

COPY . /opt/ros2-robot/
WORKDIR /opt/ros2-robot

ENTRYPOINT ["/bin/bash", "-c", \
  "source /opt/ros/humble/setup.bash && ./start_nodes.sh"]

Dockerfile for the Nuxt 3 Frontend:

# frontend/Dockerfile
FROM node:20-alpine
RUN npm install -g pnpm          # pnpm only — never npm or yarn
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
COPY . .
RUN pnpm build                   # outputs to .output/
EXPOSE 3000
CMD ["node", ".output/server/index.mjs"]

6.4.2 Docker Compose for Multi-Service Deployment

Local Full-Stack (docker-compose.yml):

services:
  backend:
    build: ./backend
    ports:
      - "9090:9090"     # rosbridge WebSocket
      - "8085:8085"     # latency API
    environment:
      - DEPLOY_MODE=docker

  frontend:
    build: ./frontend
    ports:
      - "3000:3000"
    environment:
      - ROSBRIDGE_URL=ws://backend:9090
    depends_on:
      - backend

Services communicate over the internal Docker bridge network using service names as hostnames. The frontend Nuxt server uses ROSBRIDGE_URL=ws://backend:9090 for its server-side rosbridge proxy (see Module 6.2), while the browser connects to ws://localhost:9090 directly for low-latency control.

Production Cloud (docker-compose.prod.yml):

services:
  backend:
    image: arbiterros-backend:latest
    expose:
      - "9090"
      - "8085"
    environment:
      - VIRTUAL_HOST=arbiter-backend.txio.live

  frontend:
    image: arbiterros-frontend:latest
    expose:
      - "3000"
    environment:
      - VIRTUAL_HOST=arbiter.txio.live
      - LETSENCRYPT_HOST=arbiter.txio.live
      - ROSBRIDGE_URL=ws://backend:9090

  nginx-proxy:
    image: jwilder/nginx-proxy
    ports: ["80:80", "443:443"]
    volumes:
      - /var/run/docker.sock:/tmp/docker.sock:ro
      - certs:/etc/nginx/certs

  letsencrypt:
    image: jrcs/letsencrypt-nginx-proxy-companion
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro

The jwilder/nginx-proxy pattern auto-generates nginx virtual host configurations from VIRTUAL_HOST environment variables and Let’s Encrypt certificates via the companion container. New services are automatically proxied when their container starts.

6.4.3 Raspberry Pi 4B Deployment

The deploy-pi.sh script handles the complete Pi deployment pipeline in a single command:

# deploy-pi.sh (key steps)
bash deploy-pi.sh install          # first-time: install ROS2 + deps
bash deploy-pi.sh                  # deploy + start (pigpio driver default)
bash deploy-pi.sh --driver l298n   # use L298N driver instead
bash deploy-pi.sh status           # check Pi health
bash deploy-pi.sh logs             # tail service logs

Pi-Specific Compose (docker-compose.pi.yml):

services:
  backend:
    build: ./backend
    privileged: true              # required for GPIO access
    devices:
      - /dev/ttyUSB0:/dev/ttyUSB0  # RPLidar serial port
      - /dev/gpiomem:/dev/gpiomem  # GPIO memory-mapped I/O
    environment:
      - DEPLOY_MODE=pi-docker
      - DRIVER_TYPE=pigpio
      - PIGPIO_HOST=localhost

The privileged: true flag grants the container access to the Pi’s GPIO hardware. In production environments, a more targeted devices list is preferred; privileged is used here for portability across Pi board revisions.

6.4.4 Sim-to-Real Transition

ArbiterROS is specifically designed so that algorithms developed in simulation require no code changes when deployed to hardware. The transition relies on three architectural properties:

  1. Identical topic names and types: The virtual robot publishes /scan as sensor_msgs/Laser­Scan with a full 360° field of view at 1° resolution. The RPLidar A1 publishes the same type with the same field of view. No algorithm code needs to distinguish between them.
  2. Connection mode abstraction: useRobotConnection hides the sim/hardware distinction behind the Connection­Mode type. The control loop, obstacle avoidance, and analytics composables call publish­Velocity(v, omega) regardless of the active mode.
  3. Driver abstraction: HardwareRobot loads a driver by parameter. Setting driver_type: mock in pi4b_params.yaml runs the full ROS 2 stack without GPIO hardware, enabling algorithm testing on any Linux machine.

Physical Parameters Requiring Adjustment:

ParameterVirtual robotPi 4B hardware
Wheel base0.30 m0.20 m (per pi4b_params.yaml)
Wheel radius0.05 m0.033 m (TT motor standard)
Max linear velocity2.0 m/s0.4 m/s (conservative indoor)
Encoder CPRN/A (simulated)360

These values are set in pi4b_params.yaml and loaded by hardware_robot.py at startup — no source code changes required.

6.4.5 System Testing and Validation

Frontend Unit Tests (vitest):

# Run all 169 tests (~3 seconds)
cd frontend && pnpm test:run

# Test file coverage:
tests/composables/useMobileRobotSimulation.test.ts
tests/composables/useControlAlgorithms.test.ts
tests/composables/useSLAMNavigation.test.ts
tests/composables/useObstacleAvoidance.test.ts
tests/composables/useROSBridge.test.ts
tests/composables/useRobotConnection.test.ts
tests/composables/useMobileRobotWithROS2.test.ts
tests/composables/useAnalyticsEngine.test.ts
tests/lib/MobileRobotPhysics.test.ts
tests/lib/LaserScan.test.ts
tests/lib/FigureEightTrajectory.test.ts
tests/lib/SLAMMap.test.ts

Backend Health Check:

./start.sh status     # prints: rosbridge up, latency API up, robot node running
curl http://localhost:8085/api/health
# {"status": "ok", "node": "latency_tracker", "uptime_s": 42.1}

Integration Test Sequence:

  1. Start backend: docker compose up --build
  2. Start frontend: pnpm dev
  3. Navigate to /ros2-test — verify all topics show “connected” status
  4. Navigate to /control — switch to ROS 2 Virtual mode, run Proportional controller to a waypoint
  5. Navigate to /latency — verify mean latency  ms for Docker mode
  6. Navigate to /slam-navigation — verify occupancy grid renders and updates as robot explores

6.4.6 Troubleshooting Common Issues

  • rosbridge not connecting: Ensure the backend container is fully started before the frontend attempts connection. Add depends_on: backend with a healthcheck if needed.
  • GPIO permission denied on Pi: Ensure pigpiod is running (sudo pigpiod) and the container has privileged: true.
  • SLAM map not updating: The SLAM toolbox is delayed 5 s after the robot node starts (see robot_bringup.launch.py). Wait for the “slam_toolbox: online” log message.
  • Build fails with npx nuxi build: Always use pnpm build — the npx nuxi build path bypasses the required symlink step for pnpm workspace resolution.

Integration: Theory to Practice

The Docker deployment is the integration layer that unifies all Chapter 6 modules.

docker-compose.yml runs the backend (which includes the autonomous behaviour node from 6.1 and the latency tracker from 6.3) alongside the Nuxt server (which implements the web control interface from 6.2) in a single command.

On the Pi 4B, docker-compose.pi.yml additionally maps the RPLidar and GPIO devices into the container for the hardware integration (6.5).

Theoretical Design Choices

Why ros:humble and not ros:humble-ros-base?ros:humble-ros-base omits ros2cli tools (ros2 topic, ros2 node, etc.). These tools are essential for debugging inside a running container (docker exec -it backend ros2 topic list). Using the full ros:humble image adds approximately 200 MB but makes the container self-diagnosable, significantly reducing debugging time.

Why pnpm and not npm or yarn?pnpm uses a content-addressable store and hard-links identical packages across projects, reducing install time and disk usage by approximately 40% compared to npm for a project of this dependency depth. The --frozen-lockfile flag in the Dockerfile ensures reproducible builds — npm’s equivalent (ci) does not respect the workspace protocol used by Nuxt 3.

Why jwilder/nginx-proxy over a hand-written nginx config? Hand-writing nginx virtual host configurations for TLS requires manual certificate renewal and configuration updates for each new service. The jwilder/nginx-proxy + letsencrypt-companion pattern auto-generates and renews Let’s Encrypt certificates, listens for container events via the Docker socket, and updates the nginx configuration dynamically. Adding a new publicly accessible service requires only adding two environment variables to its container.

Why privileged: true for the Pi backend container? GPIO access on Linux requires either root privileges or membership in the gpio group, neither of which is easily achieved inside a Docker container without privileged. The more secure alternative — a custom capability set + /dev/gpiomem device mapping — is board-revision-specific and breaks between Pi 3, Pi 4B, and Pi 5. For an educational deployment, privileged provides predictable cross-platform behaviour at the cost of container isolation.

Why ARM64/aarch64 images for Pi 4B? The Pi 4B uses a Cortex-A72 CPU (ARMv8 / 64-bit). Standard ros:humble images are built for amd64. The docker-compose.pi.yml specifies platform: linux/arm64 to pull the correct image. Building amd64 images for Pi via QEMU emulation is 10–20 slower and the resulting binaries run at approximately half the native speed.