Latent bug: the post-loop check used `command -v` to verify govulncheck and staticcheck installed. `command -v` only walks PATH, but at this layer PATH does NOT include $GOPATH/bin (/home/crafter/go/bin) — that's only added in the canonical final PATH at the bottom of the Dockerfile (line 314). At runtime the binaries work fine via the bottom PATH; only the build-time verify was broken. The bug was masked by stale Docker layer caching from earlier Dockerfile shapes. Adding the new Nix layer above this step invalidated the cache and surfaced it. Switch to direct binary path checks (test -x \"\$GOPATH/bin/...\") which work regardless of PATH state at the layer.
344 lines
17 KiB
Docker
344 lines
17 KiB
Docker
# crafting-table v0.1 — polyglot dev/build/audit container
|
|
#
|
|
# Step 1 of 10: monolith image with every toolchain in the spec.
|
|
# Spec: Sulkta-Coop/openclaw-workspace/memory/spec-crafting-table.md
|
|
#
|
|
# Toolchain version pins (bump these to upgrade):
|
|
# NODE_VERSION 22.11.0 (LTS)
|
|
# GO_VERSION 1.22.10
|
|
# RUST_CHANNEL stable
|
|
# BUN_VERSION latest (rolling — pinned at install only)
|
|
# DOTNET_VERSION 8.0
|
|
# SWIFT_VERSION 5.9.2
|
|
# KOTLIN_VERSION 1.9.25
|
|
# GRADLE_VERSION 8.10
|
|
# JDK 17 (default) + 21 (alongside, via JAVA_HOME_21)
|
|
#
|
|
# Image runs as non-root user `crafter` (uid 1000) with passwordless sudo.
|
|
# Persistent caches mounted at /caches/{cargo,maven,gradle,npm,pip}.
|
|
# Workspace at /workspace. Per-job state at /data.
|
|
|
|
FROM debian:bookworm-slim
|
|
|
|
ENV DEBIAN_FRONTEND=noninteractive \
|
|
LANG=C.UTF-8 \
|
|
LC_ALL=C.UTF-8
|
|
|
|
# ---------- Toolchain version pins ----------
|
|
ENV NODE_VERSION=22.11.0 \
|
|
GO_VERSION=1.25.9 \
|
|
DOTNET_CHANNEL=8.0 \
|
|
SWIFT_VERSION=5.9.2 \
|
|
SWIFT_PLATFORM=ubuntu22.04 \
|
|
KOTLIN_VERSION=1.9.25 \
|
|
GRADLE_VERSION=8.10
|
|
|
|
# ============================================================
|
|
# 1. System base — apt packages
|
|
# ============================================================
|
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
curl wget git ca-certificates gnupg lsb-release apt-transport-https \
|
|
build-essential pkg-config make cmake ninja-build \
|
|
jq ripgrep fd-find \
|
|
valgrind clang lld llvm \
|
|
python3 python3-pip python3-venv python3-dev \
|
|
php-cli php-curl php-mbstring php-xml php-zip composer \
|
|
ruby ruby-dev ruby-bundler \
|
|
bash shellcheck bats \
|
|
openjdk-17-jdk-headless \
|
|
sudo unzip xz-utils zstd \
|
|
libcurl4 libxml2 libedit2 libsqlite3-0 libpython3-dev \
|
|
libncurses6 libtinfo6 libgcc-s1 libstdc++6 \
|
|
libssl-dev libffi-dev \
|
|
zlib1g-dev liblzma-dev libbz2-dev \
|
|
locales tzdata \
|
|
&& ln -sf /usr/bin/fdfind /usr/local/bin/fd \
|
|
&& rm -rf /var/lib/apt/lists/*
|
|
|
|
# ============================================================
|
|
# 2. Node 22 LTS via NodeSource
|
|
# ============================================================
|
|
RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \
|
|
&& apt-get install -y --no-install-recommends nodejs \
|
|
&& rm -rf /var/lib/apt/lists/* \
|
|
&& npm install -g pnpm tsx eslint typescript
|
|
|
|
# ============================================================
|
|
# 3. Go (download from go.dev tarball)
|
|
# ============================================================
|
|
RUN ARCH="$(dpkg --print-architecture)" \
|
|
&& case "$ARCH" in \
|
|
amd64) GOARCH=amd64 ;; \
|
|
arm64) GOARCH=arm64 ;; \
|
|
*) echo "Unsupported arch $ARCH" && exit 1 ;; \
|
|
esac \
|
|
&& curl -fsSL "https://go.dev/dl/go${GO_VERSION}.linux-${GOARCH}.tar.gz" -o /tmp/go.tgz \
|
|
&& tar -C /usr/local -xzf /tmp/go.tgz \
|
|
&& rm /tmp/go.tgz
|
|
ENV PATH=/usr/local/go/bin:/root/go/bin:$PATH \
|
|
GOPATH=/root/go
|
|
|
|
# ============================================================
|
|
# 4. Microsoft .NET 8 SDK (Microsoft apt repo for bookworm/12)
|
|
# ============================================================
|
|
RUN curl -fsSL https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor -o /usr/share/keyrings/microsoft.gpg \
|
|
&& echo "deb [signed-by=/usr/share/keyrings/microsoft.gpg] https://packages.microsoft.com/debian/12/prod bookworm main" > /etc/apt/sources.list.d/microsoft.list \
|
|
&& apt-get update \
|
|
&& apt-get install -y --no-install-recommends dotnet-sdk-8.0 \
|
|
&& rm -rf /var/lib/apt/lists/*
|
|
ENV DOTNET_CLI_TELEMETRY_OPTOUT=1 \
|
|
DOTNET_NOLOGO=1
|
|
|
|
# ============================================================
|
|
# Switch to bash for the bash-ism layers below (${var//pattern} etc).
|
|
# Layers above (apt base / Node / Go / .NET) only use POSIX so they cached fine
|
|
# under dash; SHELL is set here to invalidate the cache for layer 6+ only.
|
|
SHELL ["/bin/bash", "-c"]
|
|
|
|
# 5. Swift (Ubuntu 22.04 tarball — works on Debian bookworm
|
|
# because bookworm ships the right libicu/libstdc++ baseline)
|
|
# ============================================================
|
|
RUN ARCH="$(dpkg --print-architecture)" \
|
|
&& SWIFT_PLAT="${SWIFT_PLATFORM}" \
|
|
&& case "$ARCH" in \
|
|
amd64) SWIFT_TARBALL_NAME="swift-${SWIFT_VERSION}-RELEASE-${SWIFT_PLAT}" ;; \
|
|
arm64) SWIFT_TARBALL_NAME="swift-${SWIFT_VERSION}-RELEASE-${SWIFT_PLAT}-aarch64" ;; \
|
|
*) echo "Unsupported arch $ARCH for Swift" && exit 1 ;; \
|
|
esac \
|
|
&& apt-get update \
|
|
&& apt-get install -y --no-install-recommends libpython3-dev libxml2-dev \
|
|
libcurl4-openssl-dev libedit-dev libsqlite3-dev libtinfo-dev libncurses-dev \
|
|
&& rm -rf /var/lib/apt/lists/* \
|
|
&& curl -fsSL "https://download.swift.org/swift-${SWIFT_VERSION}-release/${SWIFT_PLAT//./}/swift-${SWIFT_VERSION}-RELEASE/${SWIFT_TARBALL_NAME}.tar.gz" -o /tmp/swift.tgz \
|
|
&& mkdir -p /opt/swift \
|
|
&& tar -xzf /tmp/swift.tgz -C /opt/swift --strip-components=1 \
|
|
&& rm /tmp/swift.tgz
|
|
ENV PATH=/opt/swift/usr/bin:$PATH
|
|
|
|
# ============================================================
|
|
# 6. Kotlin compiler (direct download from GitHub release)
|
|
# ============================================================
|
|
RUN curl -fsSL "https://github.com/JetBrains/kotlin/releases/download/v${KOTLIN_VERSION}/kotlin-compiler-${KOTLIN_VERSION}.zip" -o /tmp/kotlin.zip \
|
|
&& unzip -q /tmp/kotlin.zip -d /opt \
|
|
&& mv /opt/kotlinc /opt/kotlin \
|
|
&& rm /tmp/kotlin.zip
|
|
ENV PATH=/opt/kotlin/bin:$PATH
|
|
|
|
# ============================================================
|
|
# 7. JDK 21 alongside JDK 17 (Eclipse Temurin via apt)
|
|
# ============================================================
|
|
RUN mkdir -p /etc/apt/keyrings \
|
|
&& curl -fsSL https://packages.adoptium.net/artifactory/api/gpg/key/public | gpg --dearmor -o /etc/apt/keyrings/adoptium.gpg \
|
|
&& echo "deb [signed-by=/etc/apt/keyrings/adoptium.gpg] https://packages.adoptium.net/artifactory/deb bookworm main" > /etc/apt/sources.list.d/adoptium.list \
|
|
&& apt-get update \
|
|
&& apt-get install -y --no-install-recommends temurin-21-jdk \
|
|
&& rm -rf /var/lib/apt/lists/*
|
|
ENV JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64 \
|
|
JAVA_HOME_17=/usr/lib/jvm/java-17-openjdk-amd64 \
|
|
JAVA_HOME_21=/usr/lib/jvm/temurin-21-jdk-amd64
|
|
RUN update-alternatives --set java ${JAVA_HOME_17}/bin/java || true \
|
|
&& update-alternatives --set javac ${JAVA_HOME_17}/bin/javac || true
|
|
|
|
# ============================================================
|
|
# 8. Maven (apt) + Gradle (direct download — apt's gradle is ancient)
|
|
# ============================================================
|
|
RUN apt-get update \
|
|
&& apt-get install -y --no-install-recommends maven \
|
|
&& rm -rf /var/lib/apt/lists/* \
|
|
&& curl -fsSL "https://services.gradle.org/distributions/gradle-${GRADLE_VERSION}-bin.zip" -o /tmp/gradle.zip \
|
|
&& unzip -q /tmp/gradle.zip -d /opt \
|
|
&& mv /opt/gradle-${GRADLE_VERSION} /opt/gradle \
|
|
&& rm /tmp/gradle.zip
|
|
ENV PATH=/opt/gradle/bin:$PATH
|
|
|
|
# ============================================================
|
|
# 9. GitHub CLI (gh)
|
|
# ============================================================
|
|
RUN curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg \
|
|
&& chmod go+r /usr/share/keyrings/githubcli-archive-keyring.gpg \
|
|
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" > /etc/apt/sources.list.d/github-cli.list \
|
|
&& apt-get update \
|
|
&& apt-get install -y --no-install-recommends gh \
|
|
&& rm -rf /var/lib/apt/lists/*
|
|
|
|
# ============================================================
|
|
# 10. yq (Mike Farah, Go binary)
|
|
# ============================================================
|
|
RUN ARCH="$(dpkg --print-architecture)" \
|
|
&& curl -fsSL "https://github.com/mikefarah/yq/releases/latest/download/yq_linux_${ARCH}" -o /usr/local/bin/yq \
|
|
&& chmod +x /usr/local/bin/yq
|
|
|
|
# ============================================================
|
|
# 11. shfmt (Go binary)
|
|
# ============================================================
|
|
RUN ARCH="$(dpkg --print-architecture)" \
|
|
&& curl -fsSL "https://github.com/mvdan/sh/releases/download/v3.10.0/shfmt_v3.10.0_linux_${ARCH}" -o /usr/local/bin/shfmt \
|
|
&& chmod +x /usr/local/bin/shfmt
|
|
|
|
# ============================================================
|
|
# 12. Create non-root user `crafter` (uid 1000) with passwordless sudo
|
|
# and prepare workspace / cache / data dirs
|
|
# ============================================================
|
|
RUN useradd -m -u 1000 -s /bin/bash crafter \
|
|
&& echo "crafter ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/crafter \
|
|
&& chmod 0440 /etc/sudoers.d/crafter \
|
|
&& mkdir -p /workspace /caches/cargo /caches/maven /caches/gradle /caches/npm /caches/pip /caches/bun /data \
|
|
&& chown -R crafter:crafter /workspace /caches /data
|
|
|
|
# ============================================================
|
|
# 13. Switch to crafter for user-scoped installs
|
|
# ============================================================
|
|
USER crafter
|
|
WORKDIR /home/crafter
|
|
|
|
# Cache env (point tools at the persisted cache dirs)
|
|
ENV CARGO_HOME=/caches/cargo \
|
|
RUSTUP_HOME=/home/crafter/.rustup \
|
|
GRADLE_USER_HOME=/caches/gradle \
|
|
MAVEN_OPTS="-Dmaven.repo.local=/caches/maven" \
|
|
NPM_CONFIG_CACHE=/caches/npm \
|
|
PIP_CACHE_DIR=/caches/pip \
|
|
BUN_INSTALL=/home/crafter/.bun \
|
|
PIPX_HOME=/home/crafter/.local/pipx \
|
|
PIPX_BIN_DIR=/home/crafter/.local/bin
|
|
|
|
ENV PATH=/home/crafter/.local/bin:/caches/cargo/bin:/home/crafter/.bun/bin:$PATH
|
|
|
|
# ============================================================
|
|
# 14. Rust (rustup, stable) + cargo-audit + cargo-deny
|
|
# ============================================================
|
|
RUN curl -fsSL https://sh.rustup.rs | sh -s -- -y --default-toolchain stable --profile minimal --no-modify-path \
|
|
&& /caches/cargo/bin/rustup component add clippy rustfmt
|
|
# Install cargo-audit + cargo-deny to /usr/local (root-owned, NOT volume-shadowed
|
|
# at runtime). The /caches/cargo/bin/ dir IS volume-shadowed by the host bind
|
|
# mount, so cargo install artifacts there disappear inside the live container.
|
|
USER root
|
|
# cargo-audit + cargo-deny intentionally NOT installed in image — both
|
|
# `cargo install` and prebuilt-binary-download approaches flaked
|
|
# (libgit2-sys C bindings, quote build script under /caches/cargo
|
|
# contention, and DNS flakes on github.com release downloads). Operators
|
|
# who need rust audit can install at runtime once the container is up.
|
|
# Future v0.2 work: bake into a precompiled stage or use the prebuilt
|
|
# binary pattern with retries.
|
|
|
|
# ============================================================
|
|
# 15. Bun (curl install)
|
|
# ============================================================
|
|
RUN curl -fsSL https://bun.sh/install | bash
|
|
|
|
# ============================================================
|
|
# 16. Python user tooling: pipx-managed CLI tools
|
|
# ============================================================
|
|
RUN python3 -m pip install --user --break-system-packages --no-cache-dir pipx \
|
|
&& python3 -m pipx ensurepath \
|
|
&& python3 -m pipx install uv \
|
|
&& python3 -m pipx install ruff \
|
|
&& python3 -m pipx install mypy \
|
|
&& python3 -m pipx install pytest \
|
|
&& python3 -m pipx install pip-audit \
|
|
&& python3 -m pipx install semgrep \
|
|
# mypy needs the third-party stub packages injected into its own pipx
|
|
# venv (mypy-isolated, not the system site-packages). Without these,
|
|
# `mypy --strict` against any project that imports requests/PyYAML/etc.
|
|
# fails with "Library stubs not installed for X" exit 1.
|
|
&& python3 -m pipx inject mypy types-requests types-PyYAML types-setuptools
|
|
|
|
# ============================================================
|
|
# 17. Go user tooling — govulncheck + staticcheck
|
|
#
|
|
# Override GOPATH from the root default (/root/go set at line 79) to a
|
|
# crafter-writable path before running `go install` as USER crafter.
|
|
# Don't redeclare PATH here — the final clean PATH at the bottom of
|
|
# this Dockerfile is the single source of truth and includes
|
|
# /home/crafter/go/bin.
|
|
# ============================================================
|
|
ENV GOPATH=/home/crafter/go
|
|
# ============================================================
|
|
RUN for i in 1 2 3 4 5; do \
|
|
go install golang.org/x/vuln/cmd/govulncheck@latest \
|
|
&& go install honnef.co/go/tools/cmd/staticcheck@latest \
|
|
&& break || { echo "go install attempt $i failed, sleeping $((i*10))s"; sleep $((i*10)); }; \
|
|
done; \
|
|
test -x "$GOPATH/bin/govulncheck" && test -x "$GOPATH/bin/staticcheck" || { echo "go install failed after 5 attempts"; exit 1; }
|
|
|
|
# GOPATH already set above; PATH handled by the final clean ENV at the
|
|
# bottom (which includes /home/crafter/go/bin). No per-layer PATH ENV
|
|
# needed here — the layered approach drifted from accumulator-style PATH
|
|
# into hand-rolled lists earlier and broke (see git blame for the
|
|
# resulting band-aid commits).
|
|
|
|
# ============================================================
|
|
# 18. Ruby user tooling: bundler-audit, rubocop
|
|
# ============================================================
|
|
RUN gem install --user-install --no-document bundler-audit rubocop || \
|
|
sudo gem install --no-document bundler-audit rubocop
|
|
ENV PATH=/home/crafter/.local/share/gem/ruby/3.1.0/bin:$PATH
|
|
|
|
# ============================================================
|
|
# 19. PHP user tooling: phpstan, phpunit (composer global)
|
|
# ============================================================
|
|
ENV COMPOSER_HOME=/home/crafter/.composer
|
|
RUN composer global require --no-interaction phpstan/phpstan phpunit/phpunit
|
|
ENV PATH=/home/crafter/.composer/vendor/bin:$PATH
|
|
|
|
# ============================================================
|
|
# 19.5. Nix (single-user) — for Plutarch / Plutus / IOG flakes
|
|
# ============================================================
|
|
# Why: Cardano smart-contract toolchains (Plutarch, plutus-core, the
|
|
# Liqwid Agora `agora-scripts` exporter) ship as Nix flakes with
|
|
# haskell-nix-pinned GHC. Building them with system cabal alone is
|
|
# a manual-version-pinning fight against the IOG snapshot.
|
|
#
|
|
# Single-user install at /nix, no daemon. Sandbox disabled — nested
|
|
# sandboxes don't work cleanly under Docker. Flakes + nix-command
|
|
# enabled by default.
|
|
#
|
|
# Cache: bind-mount `/nix` at runtime (compose.yml ships this) so the
|
|
# multi-GB haskell-nix downloads persist across container rebuilds.
|
|
USER root
|
|
RUN mkdir -m 0755 /nix && chown crafter:crafter /nix
|
|
USER crafter
|
|
WORKDIR /home/crafter
|
|
RUN curl -fsSL https://nixos.org/nix/install -o /tmp/install-nix.sh \
|
|
&& sh /tmp/install-nix.sh --no-daemon --no-channel-add --no-modify-profile \
|
|
&& rm /tmp/install-nix.sh \
|
|
&& mkdir -p /home/crafter/.config/nix \
|
|
&& printf 'experimental-features = nix-command flakes\nsandbox = false\n' \
|
|
> /home/crafter/.config/nix/nix.conf
|
|
ENV PATH=/home/crafter/.nix-profile/bin:$PATH
|
|
|
|
# ============================================================
|
|
# 20. Smoke script — bake in
|
|
# ============================================================
|
|
COPY --chown=crafter:crafter smoke.sh /usr/local/bin/smoke.sh
|
|
USER root
|
|
RUN chmod +x /usr/local/bin/smoke.sh
|
|
|
|
# ============================================================
|
|
# 21. Application — FastAPI + async runner (wave 1: steps 2+3+4)
|
|
# ============================================================
|
|
COPY pyproject.toml requirements.txt /app/
|
|
# --force-reinstall is the critical flag: without it, pip skips packages
|
|
# already in /root/.local/ (left there by the earlier pipx bootstrap as
|
|
# USER root). uvicorn under /usr/local/bin needs them in
|
|
# /usr/local/lib/python3.11/dist-packages, so force the system install.
|
|
RUN pip install --break-system-packages --no-cache-dir --force-reinstall -r /app/requirements.txt
|
|
COPY --chown=crafter:crafter crafting_table /app/crafting_table
|
|
RUN chown -R crafter:crafter /app
|
|
|
|
# ============================================================
|
|
# 22. Final ENV / WORKDIR / CMD
|
|
# ============================================================
|
|
USER crafter
|
|
WORKDIR /workspace
|
|
|
|
# Final clean PATH — single source of truth that overrides any earlier
|
|
# accumulator drift in the layered ENV PATH= statements above. Lists
|
|
# every toolchain bin so cargo/rustc, swift, kotlinc, gradle, bun, go +
|
|
# govulncheck/staticcheck, ruff/mypy/pytest/uv, phpstan, bundler-audit
|
|
# are all reachable from the crafter user shell with no per-recipe prefix.
|
|
ENV PATH=/home/crafter/.nix-profile/bin:/home/crafter/.local/bin:/home/crafter/.composer/vendor/bin:/home/crafter/.local/share/gem/ruby/3.1.0/bin:/home/crafter/.bun/bin:/home/crafter/go/bin:/home/crafter/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/bin:/caches/cargo/bin:/opt/swift/usr/bin:/opt/kotlin/bin:/opt/gradle/bin:/usr/local/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
|
|
|
ENV PYTHONPATH=/app \
|
|
PYTHONUNBUFFERED=1
|
|
CMD ["uvicorn", "crafting_table.server:app", "--host", "0.0.0.0", "--port", "8810"]
|