Skip to content

Commit 35174bb

Browse files
hyperpolymathclaude
andcommitted
fix(container): rewrite Containerfile for Elixir stack; add dev compose
The Containerfile referenced adapter/zig/boj-server which doesn't exist. Rewritten as a 3-stage build: Zig (boj-invoke + .so files) → Elixir mix release → minimal wolfi-base runtime. Also adds container/compose.dev.yaml for immediate local dev use — mounts source tree, uses pre-built boj-invoke, runs Elixir via mix run. Adds mix release config to elixir/mix.exs so `mix release boj_rest` works. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent fe509cd commit 35174bb

3 files changed

Lines changed: 129 additions & 92 deletions

File tree

container/Containerfile

Lines changed: 84 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -1,147 +1,139 @@
11
# SPDX-License-Identifier: PMPL-1.0-or-later
22
# Bundle of Joy Server Container Image
33
#
4-
# Multi-stage build: Zig FFI compilation + Zig adapter binary (both built
5-
# inside Stage 1 — no local pre-build required).
4+
# Multi-stage build:
5+
# Stage 1 (zig-builder) — builds boj-invoke CLI + all cartridge .so files
6+
# Stage 2 (mix-builder) — compiles Elixir release (bundles ERTS)
7+
# Stage 3 (runtime) — minimal image: Elixir release + boj-invoke + .so files
68
#
79
# Build with Podman:
810
# podman build -t boj-server:latest -f container/Containerfile .
911
#
1012
# Run:
11-
# podman run -p 7700:7700 -p 7701:7701 -p 7702:7702 -p 7703:7703 boj-server:latest
12-
#
13-
# Run with persistent volume:
14-
# podman run -p 7700:7700 -p 7701:7701 -p 7702:7702 -p 7703:7703 \
15-
# -v boj-server-data:/data boj-server:latest
13+
# podman run -p 7700:7700 boj-server:latest
1614

1715
# ============================================================================
18-
# Stage 1: FFI Builder
16+
# Stage 1: Zig FFI Builder
1917
# ============================================================================
20-
#
21-
# Compiles all Zig FFI layers (core + all cartridges) in ReleaseFast mode,
22-
# then compiles the Zig adapter binary against those FFI libs — fully
23-
# self-contained, no local pre-build required.
2418

25-
FROM cgr.dev/chainguard/wolfi-base:latest AS builder
19+
FROM cgr.dev/chainguard/wolfi-base:latest AS zig-builder
2620

27-
# Install Zig toolchain, C toolchain, and sqlite dev headers (linked by some cartridge FFIs).
28-
RUN apk add --no-cache build-base zig sqlite-dev
21+
RUN apk add --no-cache zig build-base
2922

3023
WORKDIR /build
3124

32-
# Copy source tree
33-
COPY . .
25+
# Copy Zig source and cartridge FFI sources
26+
COPY ffi/zig/ ffi/zig/
27+
COPY cartridges/ cartridges/
3428

35-
# Build core FFI (catalogue, loader, guardian, federation, readiness, verisimdb, e2e)
36-
RUN cd ffi/zig && zig build -Doptimize=ReleaseFast
29+
# Build boj-invoke CLI (x86_64-linux-gnu target so it uses DlDynLib/real dlopen)
30+
RUN cd ffi/zig && zig build invoke -Doptimize=ReleaseFast
3731

38-
# Build ALL cartridge FFI shared libraries (discovers dynamically)
32+
# Build all cartridge .so files that have a build.zig
3933
RUN for ffi_dir in /build/cartridges/*/ffi; do \
4034
cart="$(basename "$(dirname "$ffi_dir")")" && \
4135
if [ -f "$ffi_dir/build.zig" ]; then \
42-
echo "Building ${cart} FFI..." && \
36+
echo "Building ${cart}..." && \
4337
cd "$ffi_dir" && \
44-
zig build -Doptimize=ReleaseFast || echo "WARN: ${cart} build failed (non-fatal)"; \
38+
zig build -Doptimize=ReleaseFast 2>/dev/null || \
39+
echo "WARN: ${cart} FFI skipped (non-fatal)"; \
4540
fi; \
4641
done
4742

48-
# Compile Zig adapter binary, linking against all Zig FFI shared libs.
49-
RUN LFLAGS="-L/build/ffi/zig/zig-out/lib" && \
50-
for d in /build/cartridges/*/ffi/zig-out/lib; do \
51-
[ -d "$d" ] && LFLAGS="${LFLAGS} -L${d}"; \
52-
done && \
53-
zig build-exe -O ReleaseFast -lc \
54-
-fno-omit-frame-pointer \
55-
-fPIE -pie \
56-
--name boj-server \
57-
--pkg-begin boj_adapter /build/adapter/zig/src \
58-
-I/build/ffi/zig/zig-out/include \
59-
${LFLAGS} \
60-
-o /build/adapter/zig/boj-server
43+
# ============================================================================
44+
# Stage 2: Elixir Release Builder
45+
# ============================================================================
46+
47+
FROM cgr.dev/chainguard/elixir:latest AS mix-builder
48+
49+
WORKDIR /build
50+
51+
# Copy Elixir project
52+
COPY elixir/ ./
53+
54+
# Fetch deps and compile release
55+
RUN mix local.hex --force && mix local.rebar --force
56+
ENV MIX_ENV=prod
57+
RUN mix deps.get --only prod
58+
RUN mix compile
59+
RUN mix release boj_rest
6160

6261
# ============================================================================
63-
# Stage 2: Runtime
62+
# Stage 3: Runtime Image
6463
# ============================================================================
65-
#
66-
# Minimal production image. Only the compiled binary, shared libraries,
67-
# and runtime dependencies are included.
6864

6965
FROM cgr.dev/chainguard/wolfi-base:latest
7066

71-
# OCI image labels (compatible with cerro-torre .ctp bundle metadata)
67+
# OCI labels
7268
LABEL org.opencontainers.image.title="Bundle of Joy Server" \
73-
org.opencontainers.image.description="Unified server capability catalogue with formally verified cartridges, distributed community hosting, and the Teranga menu system" \
69+
org.opencontainers.image.description="BoJ REST server — Elixir + Zig FFI cartridge host" \
7470
org.opencontainers.image.url="https://github.com/hyperpolymath/boj-server" \
7571
org.opencontainers.image.source="https://github.com/hyperpolymath/boj-server" \
7672
org.opencontainers.image.vendor="hyperpolymath" \
7773
org.opencontainers.image.licenses="PMPL-1.0-or-later" \
7874
org.opencontainers.image.authors="Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk>" \
79-
dev.cerrotorre.manifest="container/manifest.toml" \
80-
dev.cerrotorre.gatekeeper="container/.gatekeeper.yaml" \
8175
dev.stapeln.compose="container/compose.toml"
8276

83-
# Minimal runtime dependencies
84-
RUN apk add --no-cache ca-certificates curl
77+
# Minimal runtime deps: OpenSSL (Elixir TLS), ncurses (Erlang), glibc (boj-invoke + .so)
78+
RUN apk add --no-cache ca-certificates openssl ncurses-libs libstdc++ gcompat
8579

86-
# Create non-root user
80+
# Non-root user
8781
RUN addgroup -S appuser && adduser -S appuser -G appuser
8882

8983
WORKDIR /app
9084

91-
# Copy Zig adapter binary compiled in Stage 1
92-
COPY --from=builder /build/adapter/zig/boj-server /app/boj-server
93-
RUN chmod +x /app/boj-server
94-
95-
# Copy core FFI libraries from builder stage
96-
COPY --from=builder /build/ffi/zig/zig-out/ /app/lib/core/
97-
98-
# Copy ALL cartridge FFI build outputs from builder stage (single layer)
99-
COPY --from=builder /build/cartridges/ /app/lib/cartridges-src/
100-
# Extract only the zig-out directories (built .so files) — discard source
101-
RUN mkdir -p /app/lib/cartridges && \
102-
for cart_dir in /app/lib/cartridges-src/*/ffi/zig-out; do \
103-
cart="$(basename "$(dirname "$(dirname "$cart_dir")")")" && \
104-
cp -r "$cart_dir" "/app/lib/cartridges/${cart}/" 2>/dev/null || true; \
85+
# Copy Elixir release (bundles ERTS — no separate Erlang install needed)
86+
COPY --from=mix-builder /build/_build/prod/rel/boj_rest ./
87+
88+
# Copy boj-invoke binary
89+
COPY --from=zig-builder /build/ffi/zig/zig-out/bin/boj-invoke /app/bin/boj-invoke
90+
RUN chmod +x /app/bin/boj-invoke
91+
92+
# Copy cartridge .so files preserving expected layout:
93+
# /app/cartridges/<name>/ffi/zig-out/lib/<lib>.so
94+
RUN mkdir -p /app/cartridges
95+
COPY --from=zig-builder /build/cartridges/ /tmp/cartridges-src/
96+
RUN for cart_dir in /tmp/cartridges-src/*/; do \
97+
cart="$(basename "$cart_dir")" && \
98+
so_dir="${cart_dir}ffi/zig-out/lib" && \
99+
if [ -d "$so_dir" ]; then \
100+
target="/app/cartridges/${cart}/ffi/zig-out/lib" && \
101+
mkdir -p "$target" && \
102+
cp -r "${so_dir}/"*.so "$target/" 2>/dev/null || true; \
103+
fi; \
105104
done && \
106-
rm -rf /app/lib/cartridges-src
107-
108-
# Copy entrypoint script
109-
COPY container/entrypoint.sh /app/entrypoint.sh
110-
RUN chmod +x /app/entrypoint.sh
111-
112-
# Copy stapeln integration files
113-
COPY container/.gatekeeper.yaml /etc/svalinn/gatekeeper.yaml
114-
COPY container/manifest.toml /app/manifest.toml
115-
116-
# Copy machine-readable config (menu, order-ticket)
117-
COPY .machine_readable/servers/ /app/config/servers/ 2>/dev/null || true
105+
rm -rf /tmp/cartridges-src
106+
107+
# Copy cartridge metadata (cartridge.json files — loaded by Catalog GenServer)
108+
RUN mkdir -p /app-cartridges-meta
109+
COPY cartridges/ /tmp/carts-meta/
110+
RUN for cart_dir in /tmp/carts-meta/*/; do \
111+
cart="$(basename "$cart_dir")" && \
112+
if [ -f "${cart_dir}cartridge.json" ]; then \
113+
mkdir -p "/app/cartridges/${cart}" && \
114+
cp "${cart_dir}cartridge.json" "/app/cartridges/${cart}/"; \
115+
fi; \
116+
done && \
117+
rm -rf /tmp/carts-meta
118118

119-
# Create data directory for persistent storage
119+
# Data directory
120120
RUN mkdir -p /data && chown appuser:appuser /data
121-
122-
# Set ownership
123121
RUN chown -R appuser:appuser /app
124122

125-
# Environment variables
126-
ENV APP_HOST=[::]
127-
ENV APP_PORT=7700
128-
ENV APP_LOG_FORMAT=json
129-
ENV APP_DATA_DIR=/data
130-
# LD_LIBRARY_PATH is set dynamically in entrypoint.sh to cover all cartridges
131-
ENV LD_LIBRARY_PATH=/app/lib/core/lib
123+
ENV BOJ_CARTRIDGES_ROOT=/app/cartridges
124+
ENV BOJ_INVOKE_CLI=/app/bin/boj-invoke
125+
ENV PHX_HOST=0.0.0.0
126+
ENV PORT=7700
127+
ENV MIX_ENV=prod
132128

133-
# Persistent storage volume
134129
VOLUME ["/data"]
135-
136-
# Run as non-root
137130
USER appuser
138131

139-
# Expose all four adapter ports (REST, gRPC, GraphQL, MCP/SSE)
140-
EXPOSE 7700 7701 7702 7703
132+
EXPOSE 7700
141133

142-
# Health check via REST endpoint
143-
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
144-
CMD curl -sf http://localhost:${APP_PORT}/health || exit 1
134+
HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \
135+
CMD /app/bin/boj-invoke /app/cartridges/boj-health/ffi/zig-out/lib/libboj_health.so probe \
136+
| grep -q '"ok":true' || exit 1
145137

146-
ENTRYPOINT ["/app/entrypoint.sh"]
147-
CMD ["/app/boj-server"]
138+
ENTRYPOINT ["/app/bin/boj_rest"]
139+
CMD ["start"]

container/compose.dev.yaml

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# SPDX-License-Identifier: PMPL-1.0-or-later
2+
#
3+
# BoJ Server — dev compose (Podman Compose compatible)
4+
#
5+
# Runs the Elixir server with pre-built boj-invoke and cartridge .so files
6+
# mounted from the local source tree. No container build needed.
7+
#
8+
# Prerequisites:
9+
# cd ffi/zig && zig build invoke # build boj-invoke
10+
# cd cartridges/boj-health/ffi && zig build # build boj-health .so
11+
# cd elixir && mix deps.get # fetch Elixir deps
12+
#
13+
# Usage:
14+
# podman-compose -f container/compose.dev.yaml up
15+
# podman-compose -f container/compose.dev.yaml down
16+
17+
services:
18+
boj-rest:
19+
image: docker.io/hexpm/elixir:1.19.5-erlang-27.3.4-debian-bookworm-20250520-slim
20+
working_dir: /app
21+
command: ["mix", "run", "--no-halt"]
22+
ports:
23+
- "7700:7700"
24+
volumes:
25+
# Mount full source tree read-only so boj-invoke can find cartridge .so files
26+
- ../elixir:/app:z
27+
- ..:/workspace:ro,z
28+
environment:
29+
MIX_ENV: dev
30+
BOJ_CARTRIDGES_ROOT: /workspace/cartridges
31+
BOJ_INVOKE_CLI: /workspace/ffi/zig/zig-out/bin/boj-invoke
32+
BOJ_LOG_LEVEL: info
33+
healthcheck:
34+
test: ["CMD", "curl", "-sf", "http://localhost:7700/health"]
35+
interval: 15s
36+
timeout: 5s
37+
retries: 3
38+
start_period: 20s
39+
restart: unless-stopped

elixir/mix.exs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@ defmodule BojRest.MixProject do
99
version: "0.1.0",
1010
elixir: "~> 1.15",
1111
start_permanent: Mix.env() == :prod,
12+
releases: [
13+
boj_rest: [
14+
include_executables_for: [:unix],
15+
applications: [runtime_tools: :permanent]
16+
]
17+
],
1218
deps: deps(),
1319
description: "BoJ Server REST — HTTP surface for mcp-bridge",
1420
package: package()

0 commit comments

Comments
 (0)