FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim AS base # System dependencies: # ngspice -- native SPICE engine # wine -- runs LTspice (a Windows x86-64 binary) in batch mode # xvfb -- headless X display; LTspice opens an X connection even with -b # The LTspice binaries themselves are NOT in the image -- they are mounted from # the host at /opt/ltspice-src (see docker-compose.prod.yml). Without that mount # the LTspice engine degrades gracefully and ngspice still works. # Debian's Wine (8.0) is too old for LTspice v26 -- it opens a GUI event loop # instead of running -b headless. Use WineHQ 11.10 to match the dev host, where # LTspice runs fine. i386 multiarch is required (Wine pulls 32-bit components # even for the x86-64 LTspice.exe). All four packages are pinned to the same # version because the WineHQ repo sometimes carries mismatched amd64/i386 # versions, and the wine-devel metapackage demands an exact-version match. RUN dpkg --add-architecture i386 && \ apt-get update && \ apt-get install -y --no-install-recommends \ ngspice xvfb xauth wget gnupg ca-certificates && \ mkdir -pm755 /etc/apt/keyrings && \ wget -qO- https://dl.winehq.org/wine-builds/winehq.key | gpg --dearmor -o /etc/apt/keyrings/winehq-archive.key && \ wget -qNP /etc/apt/sources.list.d/ https://dl.winehq.org/wine-builds/debian/dists/bookworm/winehq-bookworm.sources && \ apt-get update && \ apt-get install -y --install-recommends \ winehq-devel=11.10~bookworm-1 wine-devel=11.10~bookworm-1 \ wine-devel-amd64=11.10~bookworm-1 wine-devel-i386=11.10~bookworm-1 && \ rm -rf /var/lib/apt/lists/* # WineHQ installs to /opt/wine-devel/bin; put it on PATH so `wine` resolves # (mcltspice's runner invokes the bare `wine` command). ENV PATH="/opt/wine-devel/bin:${PATH}" # LTspice v26 blocks on graphics init unless libGL/libEGL are loadable. The # slim image ships neither, so add Mesa software rendering (llvmpipe) -- Xvfb # has no GPU, so this is the fallback the dev host reaches via DRI3 anyway. # Separate layer to keep the heavy WineHQ layer cached. RUN apt-get update && \ apt-get install -y --no-install-recommends \ libgl1 libglx-mesa0 libgl1-mesa-dri libegl1 libegl-mesa0 \ libvulkan1 mesa-vulkan-drivers && \ rm -rf /var/lib/apt/lists/* WORKDIR /app # Copy dependency metadata first for layer caching COPY pyproject.toml ./ # --------------------------------------------------------------------------- # Development target # --------------------------------------------------------------------------- FROM base AS dev # Install in editable mode (source mounted via docker-compose volume) RUN --mount=type=cache,target=/root/.cache/uv \ --mount=type=bind,source=pyproject.toml,target=pyproject.toml \ uv pip install --system -e ".[dev,ltspice]" # Copy source for initial build (overridden by volume mount in dev) COPY src/ ./src/ EXPOSE 8000 CMD ["uv", "run", "uvicorn", "spicebook.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"] # --------------------------------------------------------------------------- # Production target # --------------------------------------------------------------------------- FROM base AS prod ENV UV_COMPILE_BYTECODE=1 # Install dependencies first (no source yet -- better caching) RUN --mount=type=cache,target=/root/.cache/uv \ --mount=type=bind,source=pyproject.toml,target=pyproject.toml \ uv pip install --system ".[ltspice]" COPY src/ ./src/ # Re-install with source to get the package registered RUN --mount=type=cache,target=/root/.cache/uv \ uv pip install --system ".[ltspice]" COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh RUN chmod +x /usr/local/bin/docker-entrypoint.sh # Run as non-root. /opt/ltspice is the writable LTSPICE_DIR (holds the Wine # prefix); /opt/ltspice-src is the read-only mountpoint for the host binaries. RUN useradd --create-home --shell /bin/bash spicebook && \ mkdir -p /app/notebooks/user /app/notebooks/examples /opt/ltspice /opt/ltspice-src && \ chown -R spicebook:spicebook /app/notebooks /opt/ltspice USER spicebook # Build a fresh Wine prefix with this image's own Wine version (avoids the # version-mismatch hazard of shipping a prefix built by a different Wine). # LTspice batch mode needs no Mono/Gecko, so disable them to skip the download. ENV WINEARCH=win64 \ WINEPREFIX=/opt/ltspice/.wine \ WINEDEBUG=-all \ WINEDLLOVERRIDES="mscoree=;mshtml=" # Keep one Xvfb up for the whole init -- xvfb-run tears the display down the # moment wineboot returns, leaving wineserver's lingering processes to spin on # a dead X server (fatal XIO errors, hung build). Explicit display + timeouts # make the step deterministic and unable to hang. RUN Xvfb :99 -screen 0 1024x768x16 -nolisten tcp >/tmp/xvfb-build.log 2>&1 & \ sleep 2 && \ DISPLAY=:99 timeout 90 wineboot --init && \ DISPLAY=:99 timeout 30 wineserver -w; \ wineserver -k 2>/dev/null; pkill Xvfb 2>/dev/null; \ rm -f /tmp/.X99-lock /tmp/.X11-unix/X99; true # Seed LTspice's settings file so batch (-b) runs skip the one-time first-run # consent dialog (which otherwise blocks even in batch mode and the sim hangs). # Captured from a broken-in install; LastRunVersion/CaptureAnalytics are the # lines that matter. Without this the LTspice engine times out on every run. COPY --chown=spicebook:spicebook ltspice-config/LTspice.ini \ /opt/ltspice/.wine/drive_c/users/spicebook/AppData/Roaming/LTspice.ini EXPOSE 8000 ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"] CMD ["python", "-m", "uvicorn", "spicebook.main:app", "--host", "0.0.0.0", "--port", "8000"]