A capture tool inside a Docker container opens the camera, saves a still, then dies when the preview window should appear. The reflex is to add device flags and privileged mode. The real culprit is usually a missing DISPLAY environment variable. Here you get a one-minute test that separates capture from rendering, the docker run flags that wire a container to the host's X server, worked numbers for the preview path, and a decision for each failure case.
Symptom: frames arrive, the window does not
A capture tool does two unrelated jobs. Capture reads frames from the device node — /dev/video0 for a USB camera — through V4L2. Rendering opens a window on the machine's X server. When stills save and frame counters climb while the window call fails, the camera, the cable and every passthrough flag are already proven good.
The error text rarely says so. Many Linux capture utilities report a rendering failure with wording that sounds like a camera fault, so an absent display reads like an absent device. Separate the two paths before touching a flag; the capture-versus-rendering isolation method works unchanged inside a container.
Run the test that settles it. Log in on the host itself — over SSH is fine, and the reason it is fine matters — and run the identical capture command. If it fails on the host the same way, the container setup was never the problem: an SSH shell has no DISPLAY variable, so a window-oriented tool has nowhere to draw on the host either.
Why a container has no display
X11 is client-server. The capture tool is an X client; it connects through a Unix socket in /tmp/.X11-unix — X0 serves display :0, X1 serves :1 — and it presents an authorization cookie. The login manager sets DISPLAY and stages that cookie for desktop sessions. Ordinary shells, and every container started from them, inherit none of it.
docker run forwards variables only when you pass -e, and forwards no sockets unless you mount them. A container started from an SSH session therefore begins with an empty DISPLAY, no X socket and no cookie: three independent ways to lose a window, none of them related to the camera. The device-side container failure has its own causes, covered step by step in the camera USB passthrough in Docker guide.
One extra socket shows up on some embedded boards, where the ISP camera stack serves clients through its own file (for example /tmp/argus_socket). Mount it and capture works — but it carries frames, not windows, so preview still needs DISPLAY and the X socket.
Likely causes in order, with a test for each
Work down the list; the first three account for nearly every preview failure that follows a successful still.
- DISPLAY unset or empty. The container was started over SSH, or with -e DISPLAY=$DISPLAY from a shell where the variable was empty. Test: echo $DISPLAY inside the container. Empty means this is the failure.
- X socket not mounted. DISPLAY is set but no socket came across. Test: ls /tmp/.X11-unix inside the container. Nothing there means mount it.
- Authorization refused. The error says 'Authorization required' or names a bad MIT-MAGIC-COOKIE. The socket is reachable but the server rejects this client. Test: run xhost on the host as the desktop user and compare users and cookies.
- No server behind the number. DISPLAY=:1 on a machine whose only socket is X0, a Wayland session without XWayland, or a headless unit with no display server at all. Test: list /tmp/.X11-unix on the host and set DISPLAY to match what exists.
- Capture side after all. Stills fail too. Test node, permissions and modes on the host first, then inside the container. A device held by a second process produces the same refusal described in the GigE camera exclusive control failure, container or not.
Fix it in the docker run line
- On the host, in a desktop terminal — not SSH — run echo $DISPLAY. Note the value: :0 on most single-seat bench units, :1 on some.
- Restart the container with the display wired in, substituting the value step 1 printed for :0:
docker run -it \
-e DISPLAY=:0 \
-v /tmp/.X11-unix:/tmp/.X11-unix:ro \
-v $HOME/.Xauthority:/tmp/.xauth:ro \
-e XAUTHORITY=/tmp/.xauth \
--device /dev/video0 \
--name camera-app your-image:tag
- Fix authorization one of two ways. On a bench unit you control, run xhost +local: on the host once before starting the container. On anything shared, rely on the cookie mount above: the shell expands $HOME before docker sees it, the file only has to be readable, and XAUTHORITY tells the client where it is. Some Wayland sessions keep the cookie under /run/user/1000/ instead of the home directory; echo $XAUTHORITY on the host shows the path to mount.
-
Prove the render path before the camera path: run xclock from the x11-apps package inside the container. A clock on screen certifies DISPLAY, socket and authorization together; whatever fails after that is capture.
-
Drop --privileged. --device /dev/video0 grants exactly the camera; privileged grants the whole host and buries real permission errors. Keep whatever GPU runtime flag your image needs — it was never the reason the window failed.
Worked example: the numbers behind the decision
Take a 1920 × 1080 RGBA preview at 60 frames/s. One frame: 1920 × 1080 × 4 = 8,294,400 bytes ≈ 8.3 MB. Per second: 8.3 MB × 60 ≈ 498 MB/s, about 4.0 Gbit/s. Through the local Unix socket, with shared memory, those bytes never leave RAM, which is why a correctly wired container previews without strain.
Forward the same window over SSH X on a 1 Gbit/s link and the ceiling is 1000 ÷ 8 = 125 MB/s, so the paper maximum is 125 ÷ 8.3 ≈ 15 frames/s, and protocol overhead drags the real rate well under it. That failure presents as a dead preview while the camera and the container are innocent.
The capture side has its own budget, worth one check: 1280 × 720 uncompressed YUYV at 30 frames/s is 1280 × 720 × 2 = 1,843,200 bytes ≈ 1.8 MB per frame, about 55 MB/s. USB 2.0 carries at most 3072 bytes per 125 µs microframe — 24.6 MB/s — so this mode already requires a USB3 link; a cable or hub stuck at USB2 speed pushes the camera into a compressed or slower mode, a capture-side symptom rather than a window problem.
The rule the numbers give: render on the machine's own display through the local socket, and never carry raw preview frames through forwarded X. When a preview must be watched from a desk, record a file on the machine and copy it — capture needs no display anywhere in that flow.
The same fix fits any capture device
Nothing in the display wiring depends on the camera on the other end. A compact module and a full-size camera differ in sensor and housing, but both deliver finished frames to your process, and neither draws the window. A board-level camera such as the LS-BCM-0003 (3.1 MP, global shutter, 120 fps over USB3) and a full-size area scan camera such as the MV-CS200-10UM (20 MP, C-mount, rolling shutter) preview identically once DISPLAY, socket and cookie are in place.
- Sensor format
- 1/1.8"
- Lens mount
- C-mount
- Interface
- USB3
- Resolution
- 5 MP
- Sensor format
- 1"
- Lens mount
- C-mount
- Interface
- USB3
- Resolution
- 16-25 MP
Hardware for a container build is chosen at the camera configurator, where interface, shutter and mount narrow the field before the display half of the pipeline is wired.
Wayland, headless units and :1 versus :0
Current desktops often run Wayland with XWayland layered on top, and X clients keep working: XWayland provides the socket, and DISPLAY is usually :0 or :1. Do not guess. List /tmp/.X11-unix on the host and set DISPLAY to what exists — socket X1 means DISPLAY=:1. A mismatch prints the same 'cannot open display' error an empty variable does.
On a headless industrial unit with no display server, no DISPLAY wiring helps, because there is nothing to connect to. Choose deliberately: attach a monitor for commissioning — the second-monitor live display guide covers the options without a frame grabber — run an in-memory X server for automated tests, or record files and preview nothing. Production lines record; benches preview.
The pattern generalizes: wrap any GUI tool in a VM or a container and it loses the desktop session's display wiring. The VM and container fixes for industrial software installs work the equivalent case for a lab-software package, with the same environment-variable logic.
When it really is the capture side
If stills fail on the host with DISPLAY properly set, stop blaming the window. Work the capture list: the node exists (v4l2-ctl --list-devices, from the v4l-utils package), permissions allow access (add the user to the video group or pass the device explicitly), no second process holds the camera, and both sides agree on a mode. Inside a container the same list applies plus one item: the node must be passed with --device, and a camera plugged in after container start needs a restart to appear.
For the plumbing inside your own application — buffers, callbacks, the difference between a frame you hold and a frame you show — the GUI preview guide and the preview restart pattern pick up where the flags end.
Prevent it next time
Wrap the display wiring once, in a start script on the host. It exports DISPLAY from the desktop session, stages the cookie or scopes xhost, and always passes the variable, the socket mount and the device together, so the flags stop varying with who starts the container or from which shell.
Build the tool headless-first: a switch that saves stills or records instead of opening a window. Continuous-integration runs then exercise capture with no display at all, and a rendering regression shows up as rendering instead of posing as a camera fault. Log frames captured and frames rendered; the first divergence between the two counters names the failing line.
Decision, case by case
- Same error over SSH on the host and inside the container: display, not camera. Pass -e DISPLAY with the value from a desktop terminal, mount /tmp/.X11-unix read-only, fix authorization with the cookie mount, and drop --privileged.
- Stills save inside the container, the window fails: capture is proven; wire the display only.
- xclock draws inside the container but the camera tool still fails: capture side — node, permissions, busy device or mode mismatch.
- No socket under /tmp/.X11-unix on the host: no display server is running; attach a display for commissioning or record files instead.
- Preview must be watched remotely: record on the machine and copy the file. A 1080p60 RGBA window moves about 498 MB/s against a 125 MB/s gigabit ceiling — forwarded X cannot carry it.
If the host test passes, the flags above are in place and frames still refuse to move, send the run line, the v4l2-ctl --list-devices output and the exact error text through the contact page — the counters will usually already have named the failing side.
Questions
Still images save inside the container but the preview window fails. Is the camera faulty?
No. A saved still proves the whole capture path: device node, driver, link bandwidth and format negotiation all work. The failing call is the window, which needs DISPLAY, the X socket and an authorization cookie. Wire those three and the same image that reaches disk will reach the screen. If stills also fail, treat it as capture and check the node, permissions and modes instead.
Should the variable be DISPLAY=:0 or DISPLAY=:0.0?
Both name display 0. The :0.0 form adds the screen number, which is 0 on every single-output setup, so the two behave identically there. What matters is matching a socket that exists: X0 in /tmp/.X11-unix means :0 works, X1 means you need :1. Older guides write :0.0; current sessions usually set :0. List the sockets and copy what you find.
Why does the identical command work at the bench but fail over SSH?
The desktop login sets DISPLAY and stages the X cookie for its session. An SSH shell inherits neither, so any window-opening tool fails there even with no container involved. Because docker run copies only the variables you pass with -e, a container started from that SSH shell inherits the same emptiness. Read the value with echo $DISPLAY in a desktop terminal and pass it explicitly.
Is the X socket needed when the container only records video?
No. Capture consumes the device node — pass it with --device /dev/video0 — and never touches DISPLAY. The socket and the variable exist for the code that opens a window. Keep the split deliberate in your own tool: a record-without-preview switch lets the same image run headless in CI and on units with no monitor attached.
Is mounting the X socket into a container safe?
It hands the container a working channel to your desktop session, so scope it. Prefer the cookie mount: the server still rejects any client that presents no valid cookie. xhost +local: is faster to type but accepts every local connection — acceptable on a single-operator bench unit, poor practice on shared machines. Skip --privileged entirely; --device plus the two display mounts is all the access this task needs.
DISPLAY is set and the socket is mounted, yet the tool still prints 'cannot open display'. What is left?
Run xclock inside the container. If the clock draws, the display path is complete and the failure sits in the camera path: confirm the node exists inside the container, that permissions allow it, and that no host process holds the device. If even the clock fails, the remaining suspects are authorization — mount the cookie and point XAUTHORITY at it — or a display number with no matching socket behind it.

