# Docker — quick reference Full reference: [`docs/20_docker_image.md`](../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). --- ## 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. 1. **Make sure WSL2 is current** (Windows PowerShell, admin): ```powershell wsl --install # no-op if WSL is already there wsl --set-default-version 2 wsl --update ``` Reboot if Windows prompts. 2. **Verify your distro is on WSL 2** (not WSL 1): ```powershell wsl -l -v ``` The `VERSION` column should read `2` for your distro. If it shows `1`, convert: `wsl --set-version 2`. 3. **Install Docker Desktop**: - Download . - During install, leave **"Use WSL 2 instead of Hyper-V"** ticked. - Launch Docker Desktop after install completes. 4. **Enable WSL integration** (one-time): - Docker Desktop → **Settings** → **Resources** → **WSL Integration**. - Toggle on integration for every WSL distro you'll run `docker` from (Ubuntu, Debian, …). - Click **Apply & Restart**. 5. **Verify from inside WSL**: ```bash docker --version docker run --rm hello-world ``` `hello-world` should 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. 1. **Enable `systemd` in WSL2** (Docker's daemon expects it). In your WSL distro edit `/etc/wsl.conf`: ```ini [boot] systemd=true ``` Then from Windows PowerShell: ```powershell wsl --shutdown ``` Reopen the WSL terminal; check `systemctl --version` runs. 2. **Install Docker Engine** (Ubuntu / Debian example — Docker's official apt repo): ```bash # 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 ``` 3. **Run without `sudo`**: ```bash sudo usermod -aG docker $USER ``` Log out and back into WSL (or `exec su -l $USER`) so the new group membership takes effect. 4. **Start the daemon**: ```bash sudo systemctl enable --now docker ``` 5. **Verify**: ```bash 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`](https://github.com/dorssel/usbipd-win). 1. **Install `usbipd-win` on Windows** (PowerShell, admin): ```powershell winget install --interactive --exact dorssel.usbipd-win ``` Reboot. 2. **List USB devices** to find the BUSID of the serial adapter: ```powershell usbipd list ``` Look for a row that describes your adapter — "USB Serial", "CH340", "FT232", "Owon" — and note its `BUSID` (e.g. `2-3`). 3. **Bind the device** (one-time per device, admin): ```powershell usbipd bind --busid 2-3 ``` 4. **Attach the device to WSL** (every time you plug it in, normal user): ```powershell usbipd attach --wsl --busid 2-3 ``` 5. **Confirm it appeared inside WSL**: ```bash ls /dev/ttyUSB* ``` You should see `/dev/ttyUSB0` (or similar). That's the path you pass to `docker 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 ```bash # 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 ```bash docker build -f docker/Dockerfile -t ecu-tests:mock . ``` ### Run the mock suite ```bash 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: ```bash xdg-open reports/report.html # Linux open reports/report.html # macOS start reports\report.html # Windows ``` ### Interactive shell ```bash 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: ```bash # 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 ```bash 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: ```bash docker run --rm ecu-tests:hw \ python -c "import pylin, pymumclient, pylinframe; print('OK')" ``` ### Run the hardware suite — direct `docker run` ```bash 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 ```bash 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 ```bash 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: ```bash # 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`: ```bash 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: ```bash 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. ```bash docker ps # running right now docker ps -a # all, including exited docker logs # captured stdout / stderr docker exec -it 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: ```bash 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: ```bash 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: ``` / └── 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?" ```bash 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-win` to bind them into the WSL2 distro; from there they appear as `/dev/ttyUSB0` exactly like on native Linux. Docker Desktop bridges WSL2 to the host network, so `--network host` reaches 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 in `docs/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`](../docs/20_docker_image.md) §8 for the full table.