16 KiB
Docker — quick reference
Full reference: docs/20_docker_image.md.
This file is just the copy-paste commands.
| File | What it is |
|---|---|
Dockerfile |
Multi-stage image. Mock-only by default; hardware variant via --build-arg INCLUDE_MELEXIS=1 + a BuildKit secret carrying the Melexis Python packages. |
compose.hw.yml |
docker-compose service for the hardware variant — host networking, USB device passthrough, reports volume, bench-config bind mount. |
../.dockerignore |
Excludes .venv/, reports/*, the deprecated BabyLIN SDK, generated caches, etc. |
All commands below assume you're running them from the repo root
inside a WSL2 distro (Ubuntu / Debian / …) with docker already on
the $PATH. If docker --version doesn't work yet, install it first
— see § Prerequisites.
Prerequisites — install Docker on WSL
If docker --version already prints a version, skip this section.
Two install paths. Option A (Docker Desktop) is the easy one that most teams use. Option B (Docker Engine directly inside WSL2) is for environments where Docker Desktop's licensing or policies are blocked.
Option A — Docker Desktop on Windows (WSL2 backend, recommended)
The Windows host runs Docker Desktop; your WSL2 distro talks to it. You install the daemon in exactly one place (Windows) and it appears seamlessly inside every enabled WSL distro.
-
Make sure WSL2 is current (Windows PowerShell, admin):
wsl --install # no-op if WSL is already there wsl --set-default-version 2 wsl --updateReboot if Windows prompts.
-
Verify your distro is on WSL 2 (not WSL 1):
wsl -l -vThe
VERSIONcolumn should read2for your distro. If it shows1, convert:wsl --set-version <DistroName> 2. -
Install Docker Desktop:
- Download https://www.docker.com/products/docker-desktop/.
- During install, leave "Use WSL 2 instead of Hyper-V" ticked.
- Launch Docker Desktop after install completes.
-
Enable WSL integration (one-time):
- Docker Desktop → Settings → Resources → WSL Integration.
- Toggle on integration for every WSL distro you'll run
dockerfrom (Ubuntu, Debian, …). - Click Apply & Restart.
-
Verify from inside WSL:
docker --version docker run --rm hello-worldhello-worldshould print "Hello from Docker!" and exit 0.
Option B — Docker Engine inside WSL2 (no Docker Desktop)
Use this when Docker Desktop isn't allowed (corporate / license policy) or when you want a single isolated Linux install.
-
Enable
systemdin WSL2 (Docker's daemon expects it). In your WSL distro edit/etc/wsl.conf:[boot] systemd=trueThen from Windows PowerShell:
wsl --shutdownReopen the WSL terminal; check
systemctl --versionruns. -
Install Docker Engine (Ubuntu / Debian example — Docker's official apt repo):
# Remove anything old that might shadow the new install sudo apt-get remove -y docker docker-engine docker.io containerd runc 2>/dev/null || true # Add Docker's apt key + repo sudo apt-get update sudo apt-get install -y ca-certificates curl gnupg lsb-release sudo install -m 0755 -d /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/ubuntu/gpg \ | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \ https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo $VERSION_CODENAME) stable" \ | sudo tee /etc/apt/sources.list.d/docker.list >/dev/null # Install sudo apt-get update sudo apt-get install -y docker-ce docker-ce-cli containerd.io \ docker-buildx-plugin docker-compose-plugin -
Run without
sudo:sudo usermod -aG docker $USERLog out and back into WSL (or
exec su -l $USER) so the new group membership takes effect. -
Start the daemon:
sudo systemctl enable --now docker -
Verify:
docker --version docker run --rm hello-world
Hardware-only — pass the Owon PSU USB device into WSL with usbipd-win
The mock image needs nothing beyond Docker itself. The hardware
image needs the Owon PSU's USB-serial adapter exposed inside WSL2.
Windows doesn't share USB devices with WSL2 out of the box; the
de-facto bridge is usbipd-win.
-
Install
usbipd-winon Windows (PowerShell, admin):winget install --interactive --exact dorssel.usbipd-winReboot.
-
List USB devices to find the BUSID of the serial adapter:
usbipd listLook for a row that describes your adapter — "USB Serial", "CH340", "FT232", "Owon" — and note its
BUSID(e.g.2-3). -
Bind the device (one-time per device, admin):
usbipd bind --busid 2-3 -
Attach the device to WSL (every time you plug it in, normal user):
usbipd attach --wsl --busid 2-3 -
Confirm it appeared inside WSL:
ls /dev/ttyUSB*You should see
/dev/ttyUSB0(or similar). That's the path you pass todocker run --device /dev/ttyUSB0:/dev/ttyUSB0.
If you want the device to re-attach automatically every time you
plug it in, use usbipd attach --auto-attach --wsl --busid 2-3
(consult usbipd --help for the full set of options).
MUM network access (192.168.7.2)
The MUM presents itself as a USB-RNDIS Ethernet adapter on
Windows. With Docker Desktop's WSL2 backend, --network host in
the container reaches the MUM automatically — no extra setup beyond
plugging the MUM in and seeing it appear in ipconfig (it should
add an interface with a 192.168.7.x address on the Windows side).
If you went with Option B (Engine in WSL2), the MUM still works because the WSL2 distro shares the Windows network stack for host-mode containers.
Sanity check before the hardware run
# Docker reachable from WSL?
docker version
# USB-serial visible in WSL?
ls -la /dev/ttyUSB*
# MUM reachable?
ping -c 2 192.168.7.2
If all three succeed you're ready for the hardware run below.
Mock-only image (CI-ready, no hardware needed)
Build
docker build -f docker/Dockerfile -t ecu-tests:mock .
Run the mock suite
mkdir -p reports
docker run --rm \
-v "$PWD/reports:/reports" \
ecu-tests:mock \
pytest -m "not hardware" -v \
--junitxml=/reports/junit.xml \
--html=/reports/report.html --self-contained-html
When the container exits, reports/report.html and
reports/junit.xml are on the host. Open the HTML report:
xdg-open reports/report.html # Linux
open reports/report.html # macOS
start reports\report.html # Windows
Interactive shell
docker run --rm -it -v "$PWD:/workspace" ecu-tests:mock bash
Edit files on the host, run pytest inside the container — code
changes show up immediately.
Hardware image (real bench)
One-time setup — Melexis packages
pylin / pymumclient / pylinframe ship inside the Melexis IDE,
not on PyPI. Bundle them into a tarball that you'll pass as a
BuildKit secret:
# Adjust the path to where Melexis IDE is installed
MELEXIS_SITE="/mnt/c/Program Files/Melexis/Melexis IDE/plugins/com.melexis.mlxide.python_1.2.0.202408130945/python/Lib/site-packages"
tar -czf melexis-pkgs.tar.gz \
-C "$MELEXIS_SITE" \
pylin pymumclient pylinframe
The tarball is gitignored (see .dockerignore) and never enters
any image layer — BuildKit's --mount=type=secret only exposes it
to the single RUN step that copies the packages into
/opt/venv/lib/python3.x/site-packages/.
License: the resulting image contains proprietary Melexis code. Treat it like the Melexis IDE itself — keep it on a private registry, not Docker Hub.
Build
DOCKER_BUILDKIT=1 docker build \
-f docker/Dockerfile -t ecu-tests:hw \
--build-arg INCLUDE_MELEXIS=1 \
--secret id=melexis_tarball,src=./melexis-pkgs.tar.gz \
.
Verify the Melexis packages landed inside the image:
docker run --rm ecu-tests:hw \
python -c "import pylin, pymumclient, pylinframe; print('OK')"
Run the hardware suite — direct docker run
docker run --rm \
--network host \
--device /dev/ttyUSB0:/dev/ttyUSB0 \
--group-add dialout \
-v "$PWD/reports:/reports" \
-v "$PWD/config/test_config.yaml:/workspace/config/test_config.yaml:ro" \
-e ECU_TESTS_CONFIG=/workspace/config/test_config.yaml \
ecu-tests:hw \
pytest -m "hardware and mum and not slow" -v \
--junitxml=/reports/junit.xml \
--html=/reports/report.html --self-contained-html
The flags, in plain English:
| Flag | Reason |
|---|---|
--network host |
MUM is at 192.168.7.2 via USB-RNDIS on the host; bridge networking would hide it. |
--device /dev/ttyUSB0:/dev/ttyUSB0 |
Pass the Owon PSU's USB-serial device into the container. Adjust to whatever ls /dev/ttyUSB* shows on the host. |
--group-add dialout |
Without it, the tester user can't open the serial device. |
-v config/test_config.yaml:…:ro |
Tweak bench config without rebuilding the image. |
Run via docker-compose
docker compose -f docker/compose.hw.yml build
docker compose -f docker/compose.hw.yml up --abort-on-container-exit
Same effect as the docker run above, but the parameters are
checked into compose.hw.yml so all you remember is the file path.
Iteration — edit-on-host, run-in-container
docker run --rm -it \
--network host \
--device /dev/ttyUSB0:/dev/ttyUSB0 \
--group-add dialout \
-v "$PWD:/workspace" \
-v "$PWD/reports:/reports" \
ecu-tests:hw \
bash
Inside the container:
# Run a specific file
pytest tests/hardware/test_overvolt.py -v -s
# Or one parametrized case
pytest "tests/hardware/test_overvolt.py::test_template_voltage_status_parametrized[overvoltage]" -v -s
# Or the settle characterization
pytest -m psu_settling -v -s
Where everything lives
After docker build and docker run, three different stores hold
three different things. Knowing the difference saves time when you
want to find a report, free disk space, or confirm "did my build
actually succeed?"
| Thing | Lives where | How you access it |
|---|---|---|
| The image | Docker daemon's content-addressed layer store. Not a single file. | docker images, docker inspect, docker history |
| A running / stopped container | Daemon's runtime state. Ephemeral when --rm is used. |
docker ps, docker ps -a, docker logs, docker exec |
| The test reports | Host filesystem at ./reports/, via the -v bind-mount in every run command. Survives container deletion. |
ls reports/, open reports/report.html |
The image
You don't navigate to it as files — query it through docker:
docker images # all images on this daemon
docker images ecu-tests # just the ones tagged ecu-tests
docker inspect ecu-tests:mock # full metadata (JSON)
docker history ecu-tests:mock # layer-by-layer breakdown
The on-disk location is daemon-internal:
| Host setup | Backing store |
|---|---|
| Native Docker Engine on Linux (Option B in the install section) | /var/lib/docker/overlay2/… |
| Docker Desktop + WSL2 (Option A) | Inside a hidden WSL2 distro docker-desktop-data. Windows side: %LOCALAPPDATA%\Docker\wsl\disk\docker_data.vhdx. Don't poke directly — always use the docker CLI. |
Images persist across reboots until you delete them:
docker rmi ecu-tests:mock # one image
docker system prune -a # everything unused (careful)
docker system df # what's eating disk
A running container
docker run … creates a container from the image. The container has
its own writable filesystem layer on top of the image's read-only
layers. The image is unchanged when the container exits.
docker ps # running right now
docker ps -a # all, including exited
docker logs <id> # captured stdout / stderr
docker exec -it <id> bash # shell into a still-running container
Every run command in this README uses --rm, so the container is
deleted the moment it exits. The image stays. The reports
(see below) stay too because they're on the host filesystem, not
inside the container.
Inside the container — what the Dockerfile lays out
/ (container root)
├── opt/
│ └── venv/ ← Python venv with all pip-installed deps
├── workspace/ ← the repo, copied in at build time
│ ├── ecu_framework/
│ ├── tests/
│ ├── config/
│ └── …
├── reports/ ← mount point for the host's ./reports/
└── home/tester/ ← unprivileged user home (uid 1000)
Peek at the layout from a throwaway container:
docker run --rm -it ecu-tests:mock bash
# inside:
ls /workspace
ls /opt/venv/bin
which pytest
/workspace is a frozen snapshot of the repo from the moment you
ran docker build. Edits to files on the host afterwards do NOT
show up inside the image — unless you bind-mount the repo at run
time:
docker run --rm -it -v "$PWD:/workspace" ecu-tests:mock bash
(That's exactly what the "Iteration" example does.)
Reports on the host — what you actually look at
Every docker run command in this README includes a bind-mount:
-v "$PWD/reports:/reports"
The container writes its outputs to /reports/; the daemon's
bind-mount makes those writes show up on the host at ./reports/
in your repo. After the container exits, the files are still there:
<repo root>/
└── reports/
├── report.html ← open this in a browser
├── junit.xml ← machine-readable for CI
├── summary.md
└── requirements_coverage.json
--rm deletes the container; it does not touch the bind-mounted
host directory.
Three commands cover 95% of "where is it?"
docker images ecu-tests # is the image there?
docker run --rm -v "$PWD/reports:/reports" \
ecu-tests:mock pytest -m "not hardware" -q
ls reports/ # outputs landed where?
Platform notes
- Linux: works as shown above.
- WSL2 (Windows): USB devices need
usbipd-winto bind them into the WSL2 distro; from there they appear as/dev/ttyUSB0exactly like on native Linux. Docker Desktop bridges WSL2 to the host network, so--network hostreaches the MUM normally. - macOS Docker Desktop: USB passthrough is not supported.
Workaround is to run a TCP-to-serial bridge on the host
(
socat) and have the container connect to that — fiddly, documented indocs/20_docker_image.md§4.3 as a non-default path.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
ModuleNotFoundError: No module named 'pylin' |
Image built without INCLUDE_MELEXIS=1 |
Rebuild with the build-arg + secret |
Permission denied: '/dev/ttyUSB0' |
Missing --group-add dialout |
Add it (or the group that owns the device on the host) |
| MUM unreachable at 192.168.7.2 | Bridge network instead of host network | Add --network host (Linux); on macOS see §4.3 |
Empty reports/ after run |
/reports not bind-mounted |
Add -v "$PWD/reports:/reports" |
| HTML report missing styling | Forgot --self-contained-html |
Pytest renders the report without inlined CSS otherwise |
See docs/20_docker_image.md §8 for
the full table.