Multi-Camera Setup with Docker

Open in ClaudeOpen in ChatGPT

If several ZED cameras are connected to the same host, Docker can help you deploy and manage all of them, either from a single container or from one container per camera. This page walks through both architectures so you can pick the one that fits your application, then covers the additional setup needed to run several cameras in separate containers.

This page assumes you are already familiar with running a single ZED camera in Docker. If not, start with Install on Linux or Install on NVIDIA® Jetson™, and check the hardware recommendations for multi-camera rigs.

One container per camera, or one container for all?

Running each camera in its own container isn’t the only valid architecture, and container-level isolation isn’t free.

Multiple containers (one per camera)

Each camera is opened by its own instance of your application, running in its own container; there are as many containers as cameras, each started and managed independently.

Advantages

  • Fault isolation: a crash or a stuck camera in one container doesn’t affect the others.
  • Independent lifecycle: restart, update, or roll back one camera’s application without touching the rest, and apply per-camera resource limits (--cpus, --memory, see the Docker run reference) individually, or through an orchestrator.
  • ROS 2 offers the same choice: composable nodes can be launched as separate processes instead of being composed together, for the same isolation and easier per-node debugging (see ROS 2 Composition). If you want that isolation for your ROS 2 nodes too, launch each camera’s node in its own process, in its own container.

Disadvantages

Single container (all cameras in one process)

A single instance of your application, running in one container, opens and manages every camera itself, with one process handling all of them at once.

Advantages

  • Shared GPU resources: one CUDA context and one set of loaded models serve every camera in that process, instead of one per camera.
  • ROS 2 zero-copy IPC: required for camera nodes to communicate with each other, or with a separate consumer node, without copying data (see the ZED IPC tutorial). This is in fact the default: the ROS 2 multi-camera launch file already loads every camera node as a component in the same ROS 2 container, specifically so that IPC remains available if you add a consumer node later.
  • ZED SDK Fusion’s INTRA_PROCESS shared-memory path: needs every camera in the same process too; it’s the lowest-latency of Fusion’s two workflows, documented in the configuration file reference.
  • Fewer moving parts: fewer containers and processes to start, monitor, and network together.

Disadvantages

  • No fault isolation: a crash or a stuck camera can take down every camera hosted in that process.
  • Coupled restarts: restarting or updating one camera’s logic means restarting the whole container, interrupting every other camera it hosts.
  • Shared limits and image: all cameras share the same container-level resource limits and the same base image and library versions; you cannot give one camera more CPU, or run it against a different ZED SDK version, without affecting the rest.

These aren’t all-or-nothing: if only some of your cameras need to share data (for Fusion or ROS 2 IPC), you can group just those cameras into one container/process and keep the rest isolated in their own containers.

The rest of this page walks through the practical setup for both: starting with a single container that opens every camera itself, then the additional steps needed to split cameras across several containers instead.

Setting up a multi-container configuration

Getting several containers to each own one camera correctly means sharing the same volumes across all of them, avoiding duplicate AI-model optimization, and assigning a specific camera to each container.

Share the same volumes with every container

All the ZED cameras connected to a host, whether USB or GMSL2, are exposed through the same host devices and services. Docker cannot restrict a container to a single camera’s device node, so every container needs the same runtime options and volumes as a single-camera setup:

  • USB cameras: -v /dev/:/dev/
  • GMSL2 cameras (ZED X, ZED X Mini, ZED X Nano, ZED X One): -v /dev/:/dev/, -v /tmp/:/tmp/, -v /var/nvidia/nvcam/settings/:/var/nvidia/nvcam/settings/, and -v /etc/systemd/system/zed_x_daemon.service:/etc/systemd/system/zed_x_daemon.service

The ZED Link driver (for GMSL2 cameras) must only be installed on the host, never inside a container.

Cache AI models and calibration files across containers

By default, every time a fresh container opens a camera it may need to:

  • Optimize the NEURAL depth model for the current GPU. This optimized engine is tied to the GPU and resolution, not to an individual camera, but if it isn’t persisted it will be rebuilt on every container restart, which can take a few minutes, especially on embedded platforms.
  • Download the camera’s calibration file from StereoLabs servers, if it isn’t already cached locally.

Bind-mount the following host folders into every container so this work happens only once and is shared by all of them, instead of being repeated per container:

-v /usr/local/zed/resources/:/usr/local/zed/resources/ \
-v /usr/local/zed/settings/:/usr/local/zed/settings/ \
  • /usr/local/zed/resources stores the optimized AI models (see the Volumes section for other uses of this folder).
  • /usr/local/zed/settings stores each camera’s calibration file (SN<serial>.conf).

Mounting the host’s own /usr/local/zed/resources and /usr/local/zed/settings directories (rather than an empty folder) lets every container reuse whatever has already been optimized or downloaded on the host.

If several containers start at the same time and the AI models aren’t already optimized in the shared volume, every one of them will try to optimize the models simultaneously, which can overload the host. Optimize the models on the host beforehand with ZED Diagnostics, or headlessly with ZED_Diagnostic -c -nrlo_all, before starting any containers.

Assign a specific camera to each container

The ZED SDK opens the first available camera in the device list when no camera is specified. If several containers all start without specifying one, they race for the same camera: only one succeeds, and the others fail to open it (for example with CAMERA STREAM FAILED TO START or INVALID RESOLUTION errors) or keep retrying indefinitely.

Each container must be told which camera to open, by serial number:

$ZED_Studio --camera <serial_number> [resolution] [frame_rate]

Some ZED tools, such as ZED Explorer or ZED Depth Viewer, don’t accept a serial number for live view (ZED Explorer’s --sn option only applies to recording or streaming). If you use one of these in a container, select the target camera manually from the dropdown at the top of its window after it starts.

Example: four cameras, four containers

This example was tested on a Jetson AGX Orin with a ZED Link Quad capture card and 4 GMSL2 cameras connected (2x ZED X Mini, 2x ZED X). Each container runs its own instance of ZED Studio, pinned to one camera, so you get 4 independent live views, each with its own Depth Map toggle to visualize that camera’s 3D depth data.

First, enable GUI containers to reach the host’s X server (see Run ZED Explorer Tool for details):

$xhost +si:localuser:root

List the connected cameras to get their serial numbers:

1ZED_Explorer -a
2## Cam 0 ##
3 Model : "ZED X Mini"
4 S/N : 58550233
5 ...
6## Cam 1 ##
7 Model : "ZED X Mini"
8 S/N : 53672776
9 ...
10## Cam 2 ##
11 Model : "ZED X"
12 S/N : 45977921
13 ...
14## Cam 3 ##
15 Model : "ZED X"
16 S/N : 46132109
17 ...

Then start one named container per camera, all sharing the same volumes described above:

1for serial in 58550233 53672776 45977921 46132109; do
2 docker run -d --name zed_cam_$serial --runtime nvidia --privileged \
3 -e DISPLAY=$DISPLAY \
4 -v /tmp/.X11-unix:/tmp/.X11-unix \
5 -v /dev/:/dev/ \
6 -v /tmp/:/tmp/ \
7 -v /var/nvidia/nvcam/settings/:/var/nvidia/nvcam/settings/ \
8 -v /etc/systemd/system/zed_x_daemon.service:/etc/systemd/system/zed_x_daemon.service \
9 -v /usr/local/zed/resources/:/usr/local/zed/resources/ \
10 -v /usr/local/zed/settings/:/usr/local/zed/settings/ \
11 stereolabs/zed:<container_tag> /usr/local/zed/tools/ZED_Studio -c $serial
12 sleep 2
13done

The short sleep between each docker run is deliberate: starting several GMSL2 cameras on the same link at the exact same moment can occasionally make one of them fail to start on its first attempt. This is a transient bus-arbitration issue, not a configuration problem; simply restart (docker restart <container>) any container whose camera failed to open.

The same setup works with an orchestrator (see Orchestrate Containers); the only per-service difference is the -c <serial_number> argument:

docker-compose.yaml for 4 cameras
1version: '2.3'
2services:
3 zed_cam_58550233:
4 image: <your-image>
5 runtime: nvidia
6 privileged: true
7 environment:
8 - DISPLAY=${DISPLAY}
9 command: ["/usr/local/zed/tools/ZED_Studio", "-c", "58550233"]
10 volumes:
11 - /tmp/.X11-unix:/tmp/.X11-unix
12 - /dev:/dev
13 - /tmp/:/tmp/
14 - /var/nvidia/nvcam/settings/:/var/nvidia/nvcam/settings/
15 - /etc/systemd/system/zed_x_daemon.service:/etc/systemd/system/zed_x_daemon.service
16 - /usr/local/zed/resources/:/usr/local/zed/resources/
17 - /usr/local/zed/settings/:/usr/local/zed/settings/
18 zed_cam_53672776:
19 image: <your-image>
20 runtime: nvidia
21 privileged: true
22 environment:
23 - DISPLAY=${DISPLAY}
24 command: ["/usr/local/zed/tools/ZED_Studio", "-c", "53672776"]
25 volumes:
26 - /tmp/.X11-unix:/tmp/.X11-unix
27 - /dev:/dev
28 - /tmp/:/tmp/
29 - /var/nvidia/nvcam/settings/:/var/nvidia/nvcam/settings/
30 - /etc/systemd/system/zed_x_daemon.service:/etc/systemd/system/zed_x_daemon.service
31 - /usr/local/zed/resources/:/usr/local/zed/resources/
32 - /usr/local/zed/settings/:/usr/local/zed/settings/
33 zed_cam_45977921:
34 image: <your-image>
35 runtime: nvidia
36 privileged: true
37 environment:
38 - DISPLAY=${DISPLAY}
39 command: ["/usr/local/zed/tools/ZED_Studio", "-c", "45977921"]
40 volumes:
41 - /tmp/.X11-unix:/tmp/.X11-unix
42 - /dev:/dev
43 - /tmp/:/tmp/
44 - /var/nvidia/nvcam/settings/:/var/nvidia/nvcam/settings/
45 - /etc/systemd/system/zed_x_daemon.service:/etc/systemd/system/zed_x_daemon.service
46 - /usr/local/zed/resources/:/usr/local/zed/resources/
47 - /usr/local/zed/settings/:/usr/local/zed/settings/
48 zed_cam_46132109:
49 image: <your-image>
50 runtime: nvidia
51 privileged: true
52 environment:
53 - DISPLAY=${DISPLAY}
54 command: ["/usr/local/zed/tools/ZED_Studio", "-c", "46132109"]
55 volumes:
56 - /tmp/.X11-unix:/tmp/.X11-unix
57 - /dev:/dev
58 - /tmp/:/tmp/
59 - /var/nvidia/nvcam/settings/:/var/nvidia/nvcam/settings/
60 - /etc/systemd/system/zed_x_daemon.service:/etc/systemd/system/zed_x_daemon.service
61 - /usr/local/zed/resources/:/usr/local/zed/resources/
62 - /usr/local/zed/settings/:/usr/local/zed/settings/

Once the containers are running, each opens its own window with the live feed of its assigned camera. The logs confirm which serial number each container actually opened:

$docker logs zed_cam_58550233
$...
$[ZED][INFO] Using GMSL input... Switched to default resolution HD1200
$[ZED][INFO] [Init] Serial Number: S/N 58550233

Enable the Depth Map eye icon next to that camera in the left panel to also visualize its live depth data.

Scaling further

Running one depth pipeline per container multiplies GPU and memory usage by the number of cameras. Before scaling up a rig, check the GPU memory and, for GMSL2 rigs, the power budget recommendations for multi-camera setups.

Setting up a single-container configuration

Getting a single container to handle several cameras correctly means opening each camera from your own application code, and persisting the host’s AI model and calibration cache so the container doesn’t redo that work on every restart.

A single-container setup runs one process that opens every camera itself, setting each one’s serial number in InitParameters before opening it (see how to list connected cameras), exactly as a custom application would outside of Docker. Since grab() blocks until a new frame is available, a common pattern for N cameras is to give each one its own thread, so every camera grabs and processes frames independently instead of waiting on the others:

1// One entry per camera to open
2std::vector<int> serials = { <serial_1>, <serial_2>, /* ... */ <serial_N> };
3std::vector<sl::Camera> cameras(serials.size());
4std::vector<std::thread> threads;
5
6for (size_t i = 0; i != serials.size(); ++i) {
7 sl::InitParameters init_parameters;
8 init_parameters.input.setFromSerialNumber(serials[i]);
9
10 if (cameras[i].open(init_parameters) == sl::ERROR_CODE::SUCCESS) {
11 // One thread per camera: each grabs and processes independently
12 threads.emplace_back([&cameras, i]() {
13 while (true) {
14 if (cameras[i].grab() == sl::ERROR_CODE::SUCCESS) {
15 // retrieve and process images/depth for cameras[i]
16 }
17 }
18 });
19 }
20}
21
22for (auto& t : threads) t.join();

Running it in Docker needs nothing beyond a single container: one docker run, with the same volumes a single camera would need for your camera family, since it’s the application, not Docker, that’s responsible for opening each camera:

$docker run -d --name zed_multi_cam --runtime nvidia --privileged \
> -v /dev/:/dev/ \
> -v /tmp/:/tmp/ \
> -v /var/nvidia/nvcam/settings/:/var/nvidia/nvcam/settings/ \
> -v /etc/systemd/system/zed_x_daemon.service:/etc/systemd/system/zed_x_daemon.service \
> -v /usr/local/zed/resources/:/usr/local/zed/resources/ \
> -v /usr/local/zed/settings/:/usr/local/zed/settings/ \
> <your-image> ./your_multi_camera_app

Even with a single container, keep the last two volumes mounted to the host’s real /usr/local/zed/resources and /usr/local/zed/settings folders, as described above. A container’s filesystem is disposable: without these volumes, every NEURAL depth model would be re-optimized and every camera’s calibration file re-downloaded again each time the container restarts, rebuilds, or gets redeployed, adding minutes of startup time instead of reusing what’s already been optimized and downloaded on the host.