> For clean Markdown of any page, append .md to the page URL. > For a complete documentation index, see https://docs.stereolabs.com/llms.txt. > For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.stereolabs.com/_mcp/server. # Multi-Camera Setup with Docker 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](/docs/integrations/docker/install-on-linux) or [Install on NVIDIA® Jetson™](/docs/integrations/docker/install-on-nvidia-jetson), and check the [hardware recommendations for multi-camera rigs](/docs/development/zed-sdk/modules/camera/multi-camera). ## 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](https://docs.docker.com/engine/reference/commandline/run/#options)) individually, or through an [orchestrator](/docs/integrations/docker/orchestrate-containers). * **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](/docs/integrations/ros-2/composition-and-ipc)). 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** * **Fixed overhead multiplies**: each container is its own CUDA context and loads its own copy of the optimized AI/depth models into GPU memory. See [Cache AI models and calibration files across containers](#cache-ai-models-and-calibration-files-across-containers) below and the [GPU memory guidance](/docs/development/zed-sdk/modules/camera/multi-camera#increase-gpu-memory) for multi-camera rigs. * **ROS 2 zero-copy IPC isn't possible across containers**: it only works between [composable nodes loaded into the same ROS 2 component container](/docs/integrations/ros-2/composition-and-ipc#compose-multiple-node-components-in-a-single-process-to-leverage-ipc), a single OS process, and a process can't span two Docker containers. Camera nodes split across several containers fall back to regular (serialized) ROS 2 topic transport instead. * **ZED SDK [Fusion](/docs/development/zed-sdk/modules/fusion) has the same constraint**: its low-latency [`INTRA_PROCESS` workflow](/docs/development/zed-sdk/modules/fusion#workflow) requires the camera publishers and the Fusion subscriber to run in the same process. Cameras split across containers must use its [`LOCAL_NETWORK` workflow](/docs/development/zed-sdk/modules/fusion#setting-up-a-network-workflow) instead, at the cost of a small (\<5ms) network hop; see the [configuration file reference](/docs/development/zed-sdk/modules/fusion#configuration-files) for both modes. ### 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](/docs/integrations/ros-2/composition-and-ipc#compose-multiple-node-components-in-a-single-process-to-leverage-ipc)). This is in fact the default: the [ROS 2 multi-camera launch file](/docs/integrations/ros-2/multi-camera-setup) 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](/docs/development/zed-sdk/modules/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](/docs/development/zed-sdk/modules/fusion#workflow), documented in the [configuration file reference](/docs/development/zed-sdk/modules/fusion#configuration-files). * **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. > **Note** > > These aren't all-or-nothing: if only some of your cameras need to share data (for [Fusion](/docs/development/zed-sdk/modules/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](/docs/products/embedded/zed-link-capture-card/gmsl2#what-is-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](/docs/integrations/docker/create-docker-images#volumes) as a single-camera setup: * **USB cameras**: `-v /dev/:/dev/` * **[GMSL2](/docs/products/embedded/zed-link-capture-card/gmsl2#what-is-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` > **Note** > > The ZED Link driver (for [GMSL2](/docs/products/embedded/zed-link-capture-card/gmsl2#what-is-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: ```txt -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](/docs/integrations/docker/create-docker-images#volumes) for other uses of this folder). * `/usr/local/zed/settings` stores each camera's calibration file (`SN.conf`). > **Note** > > 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. > **Warning** > > 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 Diagnostic](/docs/development/zed-tools/zed-diagnostics#optimize-the-ai-models), 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: * List the connected cameras and their serial numbers with `ZED_Explorer -a` or `ZED_Studio -l`. * In your own application, call [`InitParameters.input.setFromSerialNumber()`](/docs/development/zed-sdk/modules/camera/multi-camera#how-to-list-connected-cameras) before opening the camera. * With the **[ZED Studio](/docs/development/zed-tools/zed-studio)** tool, pass the serial number on the command line: ```bash ZED_Studio --camera [resolution] [frame_rate] ``` > **Note** > > 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](/docs/development/zed-tools/zed-studio#live-camera-playback) to visualize that camera's 3D depth data. First, enable GUI containers to reach the host's X server (see [Run the ZED Explorer Tool](/docs/integrations/docker/install-on-nvidia-jetson#run-the-zed-explorer-tool) for details): ```bash xhost +si:localuser:root ``` List the connected cameras to get their serial numbers: ```sh ZED_Explorer -a ## Cam 0 ## Model : "ZED X Mini" S/N : 58550233 ... ## Cam 1 ## Model : "ZED X Mini" S/N : 53672776 ... ## Cam 2 ## Model : "ZED X" S/N : 45977921 ... ## Cam 3 ## Model : "ZED X" S/N : 46132109 ... ``` Then start one named container per camera, all sharing the same volumes described above: ```sh for serial in 58550233 53672776 45977921 46132109; do docker run -d --name zed_cam_$serial --runtime nvidia --privileged \ -e DISPLAY=$DISPLAY \ -v /tmp/.X11-unix:/tmp/.X11-unix \ -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/ \ stereolabs/zed: /usr/local/zed/tools/ZED_Studio -c $serial sleep 2 done ``` > **Note** > > The short `sleep` between each `docker run` is deliberate: starting several [GMSL2](/docs/products/embedded/zed-link-capture-card/gmsl2#what-is-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 `) any container whose camera failed to open. The same setup works with an orchestrator (see [Orchestrate Containers](/docs/integrations/docker/orchestrate-containers)); the only per-service difference is the `-c ` argument:
docker-compose.yaml for 4 cameras ```yml version: '2.3' services: zed_cam_58550233: image: runtime: nvidia privileged: true environment: - DISPLAY=${DISPLAY} command: ["/usr/local/zed/tools/ZED_Studio", "-c", "58550233"] volumes: - /tmp/.X11-unix:/tmp/.X11-unix - /dev:/dev - /tmp/:/tmp/ - /var/nvidia/nvcam/settings/:/var/nvidia/nvcam/settings/ - /etc/systemd/system/zed_x_daemon.service:/etc/systemd/system/zed_x_daemon.service - /usr/local/zed/resources/:/usr/local/zed/resources/ - /usr/local/zed/settings/:/usr/local/zed/settings/ zed_cam_53672776: image: runtime: nvidia privileged: true environment: - DISPLAY=${DISPLAY} command: ["/usr/local/zed/tools/ZED_Studio", "-c", "53672776"] volumes: - /tmp/.X11-unix:/tmp/.X11-unix - /dev:/dev - /tmp/:/tmp/ - /var/nvidia/nvcam/settings/:/var/nvidia/nvcam/settings/ - /etc/systemd/system/zed_x_daemon.service:/etc/systemd/system/zed_x_daemon.service - /usr/local/zed/resources/:/usr/local/zed/resources/ - /usr/local/zed/settings/:/usr/local/zed/settings/ zed_cam_45977921: image: runtime: nvidia privileged: true environment: - DISPLAY=${DISPLAY} command: ["/usr/local/zed/tools/ZED_Studio", "-c", "45977921"] volumes: - /tmp/.X11-unix:/tmp/.X11-unix - /dev:/dev - /tmp/:/tmp/ - /var/nvidia/nvcam/settings/:/var/nvidia/nvcam/settings/ - /etc/systemd/system/zed_x_daemon.service:/etc/systemd/system/zed_x_daemon.service - /usr/local/zed/resources/:/usr/local/zed/resources/ - /usr/local/zed/settings/:/usr/local/zed/settings/ zed_cam_46132109: image: runtime: nvidia privileged: true environment: - DISPLAY=${DISPLAY} command: ["/usr/local/zed/tools/ZED_Studio", "-c", "46132109"] volumes: - /tmp/.X11-unix:/tmp/.X11-unix - /dev:/dev - /tmp/:/tmp/ - /var/nvidia/nvcam/settings/:/var/nvidia/nvcam/settings/ - /etc/systemd/system/zed_x_daemon.service:/etc/systemd/system/zed_x_daemon.service - /usr/local/zed/resources/:/usr/local/zed/resources/ - /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: ```bash 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](/docs/development/zed-sdk/modules/camera/multi-camera#increase-gpu-memory) and, for [GMSL2](/docs/products/embedded/zed-link-capture-card/gmsl2#what-is-gmsl2) rigs, the [power budget](/docs/products/embedded/zed-link-capture-card/gmsl2/gmsl-2-power-requirements) 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](/docs/development/zed-sdk/modules/camera/multi-camera#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: **`C++`** ```cpp C++ // One entry per camera to open std::vector serials = { , , /* ... */ }; std::vector cameras(serials.size()); std::vector threads; for (size_t i = 0; i != serials.size(); ++i) { sl::InitParameters init_parameters; init_parameters.input.setFromSerialNumber(serials[i]); if (cameras[i].open(init_parameters) == sl::ERROR_CODE::SUCCESS) { // One thread per camera: each grabs and processes independently threads.emplace_back([&cameras, i]() { while (true) { if (cameras[i].grab() == sl::ERROR_CODE::SUCCESS) { // retrieve and process images/depth for cameras[i] } } }); } } for (auto& t : threads) t.join(); ``` **`Python`** ```python Python import threading import pyzed.sl as sl # One entry per camera to open serials = [, , ..., ] cameras = [] threads = [] def grab_loop(zed): while True: if zed.grab() == sl.ERROR_CODE.SUCCESS: # retrieve and process images/depth for this camera pass for serial in serials: init_params = sl.InitParameters() init_params.set_from_serial_number(serial) zed = sl.Camera() if zed.open(init_params) == sl.ERROR_CODE.SUCCESS: cameras.append(zed) t = threading.Thread(target=grab_loop, args=(zed,)) t.start() threads.append(t) for t in 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](#share-the-same-volumes-with-every-container) for your camera family, since it's the application, not Docker, that's responsible for opening each camera: **`GMSL2`** ```bash GMSL2 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_multi_camera_app ``` **`USB`** ```bash USB docker run -d --name zed_multi_cam --gpus all --privileged \ -v /dev/:/dev/ \ -v /usr/local/zed/resources/:/usr/local/zed/resources/ \ -v /usr/local/zed/settings/:/usr/local/zed/settings/ \ ./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](#cache-ai-models-and-calibration-files-across-containers). 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.