# ---- Stage 0: QEMU .so + ROM binaries ---- # The Velxio runtime needs libqemu-xtensa.so + libqemu-riscv32.so (the # QEMU shared libraries that simulate ESP32 / ESP32-S3 / ESP32-C3) plus # the matching boot ROM blobs. Three sources, tried in order: # # 1. Local prebuilt files at prebuilt/qemu/. # Drop your own compiled .so files in there (see docs/BUILD-QEMU.md # for the full build-from-source guide) and they win — no network # access required. # # 2. velxio.dev gated download endpoint, when VELXIO_LICENSE_KEY is set. # Free personal-tier keys at https://velxio.dev/license/signup. # # 3. Build fails with a clear error telling you which of the two paths # to pick. # # Backward compatibility: the old QEMU_RELEASE_URL ARG is still accepted # so forks pointing at a private mirror of the binaries keep working. FROM ubuntu:22.04 AS qemu-provider RUN apt-get update && apt-get install -y --no-install-recommends curl ca-certificates \ && rm -rf /var/lib/apt/lists/* ARG TARGETARCH ARG VELXIO_LICENSE_KEY= ARG VELXIO_BINARY_BASE_URL=https://velxio.dev/api/pro/license/downloads # Legacy escape hatch — set this to keep using the old GitHub Release # mirror (or any other CDN you proxy from). When set, takes precedence # over the gated path. ARG QEMU_RELEASE_URL= # Copy the prebuilt directory (may contain .so+ROM files or just the .gitkeep) RUN mkdir -p /qemu COPY prebuilt/qemu/ /qemu/ # Resolve which fetch path the build will use and fail-fast with a # friendly message if neither prebuilt files nor a key were provided. RUN cd /qemu \ && have_libs=1 && for base in libqemu-xtensa libqemu-riscv32; do \ [ -f "${base}.so" ] || have_libs=0 ; \ done \ && if [ "$have_libs" = "1" ]; then \ echo "[qemu-provider] using local prebuilt/qemu/ files — no download" ; \ elif [ -n "${QEMU_RELEASE_URL}" ]; then \ echo "[qemu-provider] using legacy QEMU_RELEASE_URL: ${QEMU_RELEASE_URL}" ; \ elif [ -n "${VELXIO_LICENSE_KEY}" ]; then \ echo "[qemu-provider] using velxio.dev gated download with provided license key" ; \ else \ echo "" ; \ echo "ERROR: Velxio docker build needs the QEMU runtime libraries." ; \ echo "" ; \ echo "Pick one:" ; \ echo " a) Free personal key from https://velxio.dev/license/signup ," ; \ echo " then re-build with --build-arg VELXIO_LICENSE_KEY=vlx_personal_..." ; \ echo " b) Build QEMU yourself (see docs/BUILD-QEMU.md) and drop the .so" ; \ echo " files plus the three esp32*-rom.bin files into prebuilt/qemu/." ; \ echo "" ; \ exit 1 ; \ fi # Download arch-specific .so and arch-independent ROM files. # The gated endpoint serves the same byte-for-byte content as the legacy # GitHub Release path; asset paths just drop the .so extension since the # license module's manifest carries the real filename. RUN cd /qemu \ && for base in libqemu-xtensa libqemu-riscv32; do \ f="${base}.so" ; \ if [ -f "$f" ]; then \ echo "Using local $f ($(stat -c%s "$f") bytes)" ; \ elif [ -n "${QEMU_RELEASE_URL}" ]; then \ echo "Downloading ${base}-${TARGETARCH}.so → $f (legacy URL) ..." ; \ curl -fSL -o "$f" "${QEMU_RELEASE_URL}/${base}-${TARGETARCH}.so" ; \ else \ echo "Downloading ${base}-${TARGETARCH} → $f (velxio.dev) ..." ; \ curl -fSL -o "$f" "${VELXIO_BINARY_BASE_URL}/${base}-${TARGETARCH}?key=${VELXIO_LICENSE_KEY}" ; \ fi ; \ done \ && for f in esp32-v3-rom.bin esp32-v3-rom-app.bin esp32c3-rom.bin; do \ asset="${f%.bin}" ; \ if [ -f "$f" ]; then \ echo "Using local $f ($(stat -c%s "$f") bytes)" ; \ elif [ -n "${QEMU_RELEASE_URL}" ]; then \ echo "Downloading $f (legacy URL) ..." ; \ curl -fSL -o "$f" "${QEMU_RELEASE_URL}/$f" ; \ else \ echo "Downloading $asset → $f (velxio.dev) ..." ; \ curl -fSL -o "$f" "${VELXIO_BINARY_BASE_URL}/${asset}?key=${VELXIO_LICENSE_KEY}" ; \ fi ; \ done \ && ls -lh /qemu/ # ---- Stage 0.5: ESP-IDF toolchain for ESP32 compilation ---- FROM ubuntu:22.04 AS espidf-builder RUN apt-get update && apt-get install -y --no-install-recommends \ git wget flex bison gperf python3 python3-pip python3-venv \ cmake ninja-build ccache libffi-dev libssl-dev \ libusb-1.0-0 ca-certificates \ && rm -rf /var/lib/apt/lists/* # Install ESP-IDF 4.4.7 (matches Arduino ESP32 core 2.0.17 / lcgamboa QEMU ROM) RUN git clone -b v4.4.7 --recursive --depth=1 --shallow-submodules \ https://github.com/espressif/esp-idf.git /opt/esp-idf WORKDIR /opt/esp-idf # Install toolchains for esp32 (Xtensa) and esp32c3 (RISC-V) only RUN ./install.sh esp32,esp32c3 # Clean up large unnecessary files to reduce image size RUN rm -rf .git docs examples \ && find /root/.espressif -name '*.tar.*' -delete 2>/dev/null || true # Install Arduino-as-component for full Arduino API support in ESP-IDF builds RUN git clone --branch 2.0.17 --depth=1 --recursive --shallow-submodules \ https://github.com/espressif/arduino-esp32.git /opt/arduino-esp32 \ && rm -rf /opt/arduino-esp32/.git # ---- Stage 1: Build frontend and third-party ---- FROM node:20 AS frontend-builder WORKDIR /app # avr8js, rp2040js and @wokwi/elements are pulled directly from the npm # registry (see frontend/package.json) — no upstream git clones needed. # Board SVGs live in frontend/public/boards/, component SVGs in # frontend/public/component-svgs/, and components-metadata.json is committed. COPY frontend/ frontend/ COPY scripts/ scripts/ WORKDIR /app/frontend # Lock files aren't committed in this repo (they're gitignored) — see the # note in .gitignore. The `rm -f` below is defense-in-depth in case # someone runs `docker build .` from a tree where a local lock exists. RUN rm -f package-lock.json \ && npm install --include=optional \ && npm run build:docker # ---- Stage 2: Final Production Image ---- FROM python:3.12-slim # Install system dependencies, nginx, and QEMU runtime. # # qemu-system-arm + qemu-utils provide qemu-system-aarch64 and qemu-img, # both invoked by app/services/qemu_manager.py for Raspberry Pi 3 # simulation. They were missing for the entire 2024-2026 stretch when # Pi 3 simulation was advertised but broken — see docs/BOOT_IMAGES.md. # ~200 MB added to the image; trade-off is "Pi 3 actually works". # # Other libs (libglib2.0-0, libgcrypt20, libslirp0, libpixman-1-0, # libfdt1) used to be needed only as runtime deps for libqemu-xtensa.so; # they're still needed but now also pulled in transitively by # qemu-system-arm. RUN apt-get update && apt-get install -y --no-install-recommends \ curl \ ca-certificates \ nginx \ libglib2.0-0 \ libgcrypt20 \ libslirp0 \ libpixman-1-0 \ libfdt1 \ cmake \ ninja-build \ libusb-1.0-0 \ git \ ccache \ qemu-system-arm \ qemu-utils \ sdcc \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* \ && pip install --no-cache-dir packaging # Install arduino-cli into /usr/local/bin directly (avoids touching /bin) RUN curl -fsSL https://raw.githubusercontent.com/arduino/arduino-cli/master/install.sh \ | BINDIR=/usr/local/bin sh # Only install arduino-cli binary here. Core installation (arduino:avr, # rp2040:rp2040) is done at first boot by entrypoint.sh and persisted # in the mounted /root/.arduino15 volume. # ESP32 compilation uses ESP-IDF instead of arduino-cli. WORKDIR /app # Data directory for persistent SQLite database (mounted as a volume at runtime) RUN mkdir -p /app/data # Install Python backend dependencies COPY backend/requirements.txt . RUN pip install --no-cache-dir -r requirements.txt # Copy backend application code COPY backend/app/ ./app/ # One-off maintenance scripts (e.g. backfill_boards_2026_05). Pure stdlib — # run with: docker exec velxio-app python /app/scripts/