diff --git a/.changeset/fast-cores-split.md b/.changeset/fast-cores-split.md new file mode 100644 index 00000000..27bd78a8 --- /dev/null +++ b/.changeset/fast-cores-split.md @@ -0,0 +1,5 @@ +--- +"ftw": minor +--- + +Make the official core container a small static runtime and keep Python/CVXPY exclusively in the independently updatable optimizer image. diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3a96ee43..214b81a6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -69,7 +69,7 @@ jobs: drivers/*.lua) drivers=true ;; - docker-compose*.yml|scripts/enable-modular-stack.sh|scripts/migrate-legacy-compose.sh|scripts/test-modular-compose.sh|scripts/install-macos.sh) + Dockerfile|Dockerfile.optimizer|docker-compose*.yml|scripts/enable-modular-stack.sh|scripts/migrate-legacy-compose.sh|scripts/test-modular-compose.sh|scripts/test-container-boundaries.sh|scripts/install-macos.sh) compose=true ;; Makefile|.github/workflows/test.yml) @@ -195,13 +195,13 @@ jobs: -head "${{ needs.changes.outputs.head_sha }}" compose: - name: compose migration + name: module boundaries needs: changes if: needs.changes.outputs.compose == 'true' runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - - run: make compose-migration-test + - run: make compose-migration-test container-boundary-test e2e: name: full stack diff --git a/Dockerfile b/Dockerfile index ca6ea3d1..5c5057fc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,14 +1,13 @@ -# FTW container — static Go host + Python mathematical planner. -# -# Builder uses golang:alpine to keep the Go binary fully static. The runtime is -# Debian slim because the CVXPY and HiGHS ARM64 wheels target glibc. +# FTW core container — static Go host plus bundled Lua drivers and web assets. +# The optional Python/CVXPY optimizer ships as its own independently updatable +# image from Dockerfile.optimizer. Core falls back safely when it is absent. # # Multi-arch: linux/amd64 + linux/arm64 via docker buildx TARGETOS / # TARGETARCH when available. Plain `docker build` falls back to the # native Go arch inside the builder image. # --- Builder --------------------------------------------------------------- -FROM golang:1.26-alpine AS builder +FROM --platform=$BUILDPLATFORM golang:1.26-alpine AS builder # git is needed by `go build` to resolve VCS info baked into the binary # via -X main.Version. Everything else is in the base image. @@ -33,20 +32,12 @@ RUN cd go && \ CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${target_arch} \ go build -trimpath -ldflags="-s -w -X main.Version=${VERSION}" \ -o /out/ftw ./cmd/ftw -# --- Optimizer ------------------------------------------------------------- -FROM python:3.12-slim-bookworm AS optimizer - -COPY optimizer/ /src/optimizer/ -RUN python -m venv /opt/venv && \ - /opt/venv/bin/pip install --no-cache-dir /src/optimizer - # --- Runtime --------------------------------------------------------------- -FROM python:3.12-slim-bookworm +FROM alpine:3.22 -# ca-certificates: HTTPS to elprisetjustnu / met.no / ECB FX. -# tzdata: timezone-aware price + plan windows (Europe/Stockholm etc). -RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates tzdata && \ - rm -rf /var/lib/apt/lists/* +# HTTPS integrations and timezone-aware price/plan windows need these at +# runtime. BusyBox wget provides the health check without adding Python/curl. +RUN apk add --no-cache ca-certificates tzdata # Image layout: # /app/ftw binary (immutable, replaced on upgrade) @@ -62,19 +53,15 @@ RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates # is no path resolution against the config file's directory; the open # call is literally state.Open(cfg.State.Path). COPY --from=builder /out/ftw /app/ftw -COPY --from=optimizer /opt/venv /opt/venv -COPY optimizer/ /app/optimizer/ COPY drivers/ /app/drivers/ COPY web/ /app/web/ COPY LICENSE NOTICE /usr/share/doc/ftw/ RUN ln -s /app/ftw /app/forty-two-watts && \ mkdir -p /app/data /app/data/drivers /run/ftw-update /run/ftw-optimizer && \ - chown -R 100:101 /app /run/ftw-update /run/ftw-optimizer /opt/venv + chown -R 100:101 /app /run/ftw-update /run/ftw-optimizer -ENV HOME=/app/data \ - FTW_OPTIMIZER_PYTHON=/opt/venv/bin/python \ - FTW_OPTIMIZER_DIR=/app/optimizer +ENV HOME=/app/data USER 100:101 WORKDIR /app/data @@ -100,7 +87,7 @@ EXPOSE 8080 # open database file" because SQLite can't create state.db inside # a directory it doesn't own. HEALTHCHECK --interval=10s --timeout=5s --start-period=20s --retries=12 \ - CMD python -c "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8080/api/health', timeout=4)" || exit 1 + CMD wget -q -T 4 -O /dev/null http://127.0.0.1:8080/api/health || exit 1 ENTRYPOINT ["/app/ftw"] CMD ["-config", "/app/data/config.yaml", "-web", "/app/web", "-drivers", "/app/drivers", "-user-drivers", "/app/data/drivers"] diff --git a/Makefile b/Makefile index d8c8df75..2f567b11 100644 --- a/Makefile +++ b/Makefile @@ -11,7 +11,7 @@ # make dev — start sims + main app (hot-reload workflow) # make clean — remove all build artifacts -.PHONY: help test optimizer-install optimizer-test compose-migration-test build build-arm64 build-amd64 build-windows-amd64 release \ +.PHONY: help test optimizer-install optimizer-test compose-migration-test container-boundary-test build build-arm64 build-amd64 build-windows-amd64 release \ run-sim dev fmt vet clean e2e ci ci-ui ci-hw-pi docs \ verify verify-all install-hooks driver-repository-validate driver-versions @@ -68,6 +68,9 @@ compose-migration-test: bash -n scripts/enable-modular-stack.sh scripts/migrate-legacy-compose.sh scripts/install-macos.sh bash scripts/test-modular-compose.sh +container-boundary-test: + bash scripts/test-container-boundaries.sh + optimizer/.venv/.installed: optimizer/pyproject.toml $(MAKE) optimizer-install @@ -99,7 +102,7 @@ ci-hw-pi: # verify-all adds cross-compile checks for all release targets, catching # platform-specific syscall/import mistakes before push. -verify: test compose-migration-test +verify: test compose-migration-test container-boundary-test cd go && go vet ./... cd go && go build ./... @echo "verify: vet + test + build clean" diff --git a/config.example.yaml b/config.example.yaml index 753c8104..2bfaef5b 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -176,8 +176,9 @@ state: # trusted_keys: # ftw-drivers-2026-01: MX+j27UBkyM099hTyJlmMLK9qlTTDUJsaK/vH12fFKc= -# Mathematical MPC optimizer. The official container includes Python/CVXPY; -# native installs can set optimizer_command to their venv's Python executable. +# Mathematical MPC optimizer. Official Compose runs it as the independently +# updatable ftw-optimizer service; native installs can point optimizer_command +# at their venv's Python executable. # planner: # enabled: true # engine: python diff --git a/scripts/test-container-boundaries.sh b/scripts/test-container-boundaries.sh new file mode 100755 index 00000000..a5cd4655 --- /dev/null +++ b/scripts/test-container-boundaries.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) +cd "$ROOT" + +if grep -Eq 'COPY optimizer/|--from=optimizer|/opt/venv|FTW_OPTIMIZER_(PYTHON|DIR)' Dockerfile; then + echo "Dockerfile must contain only core, drivers and web assets; use Dockerfile.optimizer for Python/CVXPY" >&2 + exit 1 +fi + +grep -q '^FROM alpine:' Dockerfile +grep -q '^COPY optimizer/' Dockerfile.optimizer +grep -q '^ ftw-optimizer:' docker-compose.yml +grep -q 'FTW_OPTIMIZER_SOCKET: /run/ftw-optimizer/optimizer.sock' docker-compose.yml + +echo "container module boundaries verified"