spicebook/backend/Dockerfile
Ryan Malloy 1ec22c82dc Add gated LTspice engine via Wine 11.10
Wire mcltspice into the backend image: WineHQ 11.10 (matching the dev host),
i386 multiarch, Mesa software GL, a build-time Wine prefix seeded with the
LTspice.ini first-run config, and an entrypoint that starts Xvfb. The LTspice
install (exe/lib/examples) mounts from the host; the engine reads LTSPICE_DIR.

Gated for now: LTspice v26 stalls at graphics init under headless Wine in the
slim image (runs fine on a full desktop). The mount + LTSPICE_DIR are commented
in docker-compose.prod.yml so the engine fails fast as 'unavailable' rather than
hanging. ngspice is unaffected.
2026-06-20 07:43:08 -06:00

122 lines
5.6 KiB
Docker

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"]