You built a vision acquisition service the usual way: a host service launches a Docker container, and an image acquisition program runs inside it. Started by hand, the service works. At boot it fails every time — the container comes up, the acquisition program cannot find the camera, and the service ends in the failed state. Almost always the image is innocent; the fault is a race between your unit's start and the readiness of the container engine, the camera device, or the network link. Here you can rank the causes from the boot log, remove the race with two unit-file directives plus a device-readiness gate, size the retry timeout with the arithmetic shown, and prove the fix with three cold boots.
The symptom, and what it proves
The pattern always has the same shape. The unit is enabled to start at boot. During boot the container launches, the acquisition program runs inside it, reports that no camera is available, and exits; the unit lands in the failed state. Later the same day you start the same unit by hand and it works, first image included. That signature — fails at boot, works by hand — is the diagnosis by itself: nothing inside the container image is broken, the service is starting before something it assumes already exists.
One reboot's log names the culprit. Capture the boot log and compare three timestamps: the moment your unit started, the moment the container engine (docker.service) became active, and the moment the camera's device node appeared under /dev. Whichever dependency carries the later timestamp is the one to order and gate on. Do not guess between them; the log already knows.
The four causes, in order of probability
These four cover essentially every case of a vision container that fails only at boot, ranked by how often each turns out to be the cause.
| Rank | Cause | Signature in the boot log | Where it is fixed |
|---|---|---|---|
| 1 | Camera device not yet enumerated | Camera node appears after your unit started | Bounded gate on the node |
| 2 | Unit starts before the container engine | Your start step precedes docker.service active | After= plus Requires= |
| 3 | GigE link or camera power not up | Adapter link or camera reply lands later | Gate on camera reachability |
| 4 | Peripheral link late | Serial port to the light controller absent | Same bounded gate on that node |
Row 1 is the most common because ordering alone cannot see devices. udev creates nodes under /dev as devices enumerate, and a USB camera, a driver binding to a frame grabber, or a Camera Link card can all appear seconds after every service on the host reports ready. For USB cameras there is a second, separate trap: the right device nodes must also be passed into the container, as camera in Docker: pass V4L2 media nodes, not just /dev/video0 explains.
Row 3 is the GigE variant: the adapter's link and the camera's own power-up over PoE must complete before discovery can succeed; the GigE camera boot-order fix deals with discovery once ordering is correct. Row 2 hits hosts where the unit was first written to run from a logged-in session. Row 4 appears when the same container also drives a strobe controller over serial.
The fix: order first, with After= and Requires=
A systemd unit file is a plain ini-style text file, and its dependency directives carry the fix. Two lines order your service behind the container engine:
[Unit]
After=docker.service
Requires=docker.service
After= does the ordering: your unit does not start until docker.service has started. Requires= declares the engine as a requirement. Keep both, because they are not substitutes: a failing requirement stops your unit only when the ordering is also declared, so Requires= on its own gives you neither the order you want nor the failure behaviour you expect. Swap in Wants= where the attempt should be made anyway — a bench host on which the camera is often unplugged, for example.
Then point the unit's start command at a wrapper script instead of straight at the container launch. Ordering fixes the engine race; the wrapper fixes the device race.
Gate on the device, not on a delay
The wrapper below polls for the camera's device node with a bounded timeout, then hands over to your unchanged container command. Every value is declared at the top; the three marked as yours to replace come from your own host.
#!/bin/sh
# Wait for the camera node, then start the container.
# Call it with your container start command as its arguments.
NODE="/dev/video0" # yours: your camera's device node
TIMEOUT_S=75 # yours: 3 x your worst measured enumeration delay
POLL_S=3 # yours: poll interval
elapsed_s=0
while [ ! -e "$NODE" ] && [ "$elapsed_s" -lt "$TIMEOUT_S" ]; do
sleep "$POLL_S"
elapsed_s=$((elapsed_s + POLL_S))
done
if [ ! -e "$NODE" ]; then
echo "no camera at ${NODE} after ${TIMEOUT_S}s" >&2
exit 1
fi
exec "$@"
Two runs prove it. With NODE set to a path that exists, the wrapper hands over immediately — zero added latency. With NODE set to a path that never appears, TIMEOUT_S=6 and POLL_S=2, the loop runs 6 ÷ 2 = 3 polls and exits with status 1 after 6 s, so the unit lands in failed instead of running a blind container.
If you prefer an event-driven start, a udev rule can trigger a systemd unit the moment the device attaches — udev is the manager that creates those nodes under /dev, including USB camera nodes. The poll is easier to test and reason about; the udev trigger adds no latency at all. Either is correct; a sleep is not.
For a GigE camera there is no device node. Gate on the camera answering at its known IP, or on the adapter's link, and let discovery run after that; the wrapper logic is identical, only the probe changes.
Worked example: two GigE cameras on a dual-port PoE NIC
Inputs, each with its source:
- Host adapter: an MV-GE2002P dual-port PoE NIC, one camera per port.
- Cabling: an LS-CBL-0117 locking-stud RJ45 camera data cable, 15 m, per camera.
- Worst measured enumeration: 25 s — the slowest of ten cold reboots between the unit's would-be start and both cameras answering discovery. This input is an example; replace it with your own worst figure.
- Safety factor: 3, covering PoE power sequencing, switch port bring-up and a driver reload after an update.
- Poll interval: 3 s.
The rule in words: the retry timeout is three times the worst enumeration you have measured, and the poll count is that timeout divided by the interval.
The arithmetic: retry_timeout_s = 3 × 25 s = 75 s, and polls = 75 s ÷ 3 s = 25 attempts. When both cameras are already up, the wrapper adds one existence check and hands over — effectively 0 s.
Decision: run this host with TIMEOUT_S=75 and POLL_S=3, and let the unit fail loudly at the bound rather than retry forever. Never size the timeout below twice your own worst measurement — 50 s here — or the first cold boot after a firmware update fails again. A camera that misses a correctly sized timeout on three consecutive boots is telling you the fault is power, cabling or the driver, not ordering.
The adapter the example host uses:
- Interface
- GigE
- Ports
- 2 ports
The light controller is a dependency too
Acquisition rarely runs alone: the same container often commands the strobe, over RS-232 to something like an LS-CTL-0487 2-channel 24 V digital controller. That serial device node and the controller's own boot time belong on the same dependency list — a controller needs a moment after power-up before it accepts commands, and that moment belongs inside your worst-case measurement, not after it. Order and gate the serial node exactly as the camera node. The controller side of the system is in light controllers; the cable side, including frame grabber I/O cables, is in cables.
- Type
- Camera data cables
- Length
- 15 m
- Output
- Constant current, Constant voltage 24 V
- Channels
- 2 channels
Why the restart policy is not the fix
A container restart policy reacts after your process exits. It cannot watch a device node, an adapter link or the engine's own startup, and it cannot express "start only once the camera exists". Used alone it hides the race: the service looks healthy while the first frames of every boot go missing. Keep the ordering and the gate in the host service manager, where dependencies are declared once and written to the log; leave the restart policy enabled purely as a backstop for crashes at run time.
Prove it: the three-boot test
- Enable the unit for boot start, with the wrapper in place.
- Cold boot — a full power cycle, not a restart — three times.
- In each boot log, confirm the sequence: engine active, camera node present, your unit started.
- Confirm the first saved image is stamped after the camera-ready timestamp, on all three boots.
Any boot that fails hands you the racing dependency by timestamp. For the wider method of building and repeating dependency tests so a fix stays fixed, read how to test systemd service startup dependencies.
When ordering is not the answer
Ordering and gating fix races. Three neighbouring faults look similar and need different fixes:
- The container itself fails at run time — SDK, driver or device passthrough inside the container. See running 3D camera control software in Docker and passing a USB camera into containers with a CDI spec.
- Cameras come up on swapped channels after boot: multiple frame grabber device order.
- The camera has no IP after an adapter change: camera IP assignment failure.
On a Windows host the same race exists but the levers differ — service dependencies and driver load order there, not unit files.
Pre-ship checklist
- After= and Requires= both present, on docker.service.
- The start command runs the wrapper, not the bare container command.
- The gate is bounded at 3 × your measured worst, and the unit fails loudly at the bound.
- The worst case is re-measured after any driver, firmware, cable or switch change.
- Three consecutive cold boots pass, first image stamped after device readiness.
- Camera identities are pinned, so a late enumeration cannot swap channels — see assigning cameras to ports reliably.
When the next host goes together, frame grabbers and the system configurator cover the parts chain, and the systems topic page collects the neighbouring integration fixes. Free expert support with every order covers host bring-up questions too — reach us through the contact page.
Questions
Do I need Requires= if I already have After=?
Use both, and know which one does what. After= only orders the start; it does not make the container engine a requirement. Requires= declares the requirement, but on its own it does not block you either: a failing requirement stops your unit only when the ordering is declared too. The pair gives you the boot order and a clean failure if the engine fails. Swap Requires= for Wants= on a bench host where the camera is often unplugged.
How long should the wrapper wait for the camera?
Size the timeout from measurement, not from a round number: three times the worst delay between your unit's start and the camera's first answer, taken across about ten cold reboots. The example host in this guide measures 25 s at worst, so it runs 75 s. Poll every 2-5 s. A camera that misses a correctly sized timeout on three consecutive boots has a power, cabling or driver fault — ordering is no longer the suspect.
Isn't the container's restart policy enough?
No. A restart policy acts after your process exits, so it reacts to the race instead of removing it. It cannot watch a device node, an adapter link or the engine's own startup, and it gives you no predictable first-image time. Declare the dependencies once in the host service manager, where they are logged, and keep the restart policy purely as a backstop for crashes at run time.
How do I find the device node to gate on?
Watch the /dev directory through one boot: udev creates the camera's node as the device enumerates, and the node that appears exactly when your camera powers up is the one to wait for. A GigE camera has no node at all — gate on the camera answering at its known IP, or on the adapter's link. Passing the node into the container afterwards is a separate step from boot ordering.
Why does the service work when I start it by hand?
Because by the time you log in, the container engine is active, drivers are bound, the camera node exists and the network link is up — every dependency the service assumes already holds. The failure only exists in the boot window where those things become ready in an order nobody declared. That signature, fails at boot but works by hand, is itself the diagnosis: an ordering fault, not a broken image or camera.


