Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fast-cores-split.md
Original file line number Diff line number Diff line change
@@ -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.
6 changes: 3 additions & 3 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
35 changes: 11 additions & 24 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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)
Expand All @@ -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
Expand All @@ -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"]
7 changes: 5 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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"
Expand Down
5 changes: 3 additions & 2 deletions config.example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
17 changes: 17 additions & 0 deletions scripts/test-container-boundaries.sh
Original file line number Diff line number Diff line change
@@ -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"
Loading