|
1 | 1 | # SPDX-License-Identifier: PMPL-1.0-or-later |
2 | 2 | # Bundle of Joy Server Container Image |
3 | 3 | # |
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 |
6 | 8 | # |
7 | 9 | # Build with Podman: |
8 | 10 | # podman build -t boj-server:latest -f container/Containerfile . |
9 | 11 | # |
10 | 12 | # 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 |
16 | 14 |
|
17 | 15 | # ============================================================================ |
18 | | -# Stage 1: FFI Builder |
| 16 | +# Stage 1: Zig FFI Builder |
19 | 17 | # ============================================================================ |
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. |
24 | 18 |
|
25 | | -FROM cgr.dev/chainguard/wolfi-base:latest AS builder |
| 19 | +FROM cgr.dev/chainguard/wolfi-base:latest AS zig-builder |
26 | 20 |
|
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 |
29 | 22 |
|
30 | 23 | WORKDIR /build |
31 | 24 |
|
32 | | -# Copy source tree |
33 | | -COPY . . |
| 25 | +# Copy Zig source and cartridge FFI sources |
| 26 | +COPY ffi/zig/ ffi/zig/ |
| 27 | +COPY cartridges/ cartridges/ |
34 | 28 |
|
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 |
37 | 31 |
|
38 | | -# Build ALL cartridge FFI shared libraries (discovers dynamically) |
| 32 | +# Build all cartridge .so files that have a build.zig |
39 | 33 | RUN for ffi_dir in /build/cartridges/*/ffi; do \ |
40 | 34 | cart="$(basename "$(dirname "$ffi_dir")")" && \ |
41 | 35 | if [ -f "$ffi_dir/build.zig" ]; then \ |
42 | | - echo "Building ${cart} FFI..." && \ |
| 36 | + echo "Building ${cart}..." && \ |
43 | 37 | 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)"; \ |
45 | 40 | fi; \ |
46 | 41 | done |
47 | 42 |
|
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 |
61 | 60 |
|
62 | 61 | # ============================================================================ |
63 | | -# Stage 2: Runtime |
| 62 | +# Stage 3: Runtime Image |
64 | 63 | # ============================================================================ |
65 | | -# |
66 | | -# Minimal production image. Only the compiled binary, shared libraries, |
67 | | -# and runtime dependencies are included. |
68 | 64 |
|
69 | 65 | FROM cgr.dev/chainguard/wolfi-base:latest |
70 | 66 |
|
71 | | -# OCI image labels (compatible with cerro-torre .ctp bundle metadata) |
| 67 | +# OCI labels |
72 | 68 | 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" \ |
74 | 70 | org.opencontainers.image.url="https://github.com/hyperpolymath/boj-server" \ |
75 | 71 | org.opencontainers.image.source="https://github.com/hyperpolymath/boj-server" \ |
76 | 72 | org.opencontainers.image.vendor="hyperpolymath" \ |
77 | 73 | org.opencontainers.image.licenses="PMPL-1.0-or-later" \ |
78 | 74 | 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" \ |
81 | 75 | dev.stapeln.compose="container/compose.toml" |
82 | 76 |
|
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 |
85 | 79 |
|
86 | | -# Create non-root user |
| 80 | +# Non-root user |
87 | 81 | RUN addgroup -S appuser && adduser -S appuser -G appuser |
88 | 82 |
|
89 | 83 | WORKDIR /app |
90 | 84 |
|
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; \ |
105 | 104 | 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 |
118 | 118 |
|
119 | | -# Create data directory for persistent storage |
| 119 | +# Data directory |
120 | 120 | RUN mkdir -p /data && chown appuser:appuser /data |
121 | | - |
122 | | -# Set ownership |
123 | 121 | RUN chown -R appuser:appuser /app |
124 | 122 |
|
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 |
132 | 128 |
|
133 | | -# Persistent storage volume |
134 | 129 | VOLUME ["/data"] |
135 | | - |
136 | | -# Run as non-root |
137 | 130 | USER appuser |
138 | 131 |
|
139 | | -# Expose all four adapter ports (REST, gRPC, GraphQL, MCP/SSE) |
140 | | -EXPOSE 7700 7701 7702 7703 |
| 132 | +EXPOSE 7700 |
141 | 133 |
|
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 |
145 | 137 |
|
146 | | -ENTRYPOINT ["/app/entrypoint.sh"] |
147 | | -CMD ["/app/boj-server"] |
| 138 | +ENTRYPOINT ["/app/bin/boj_rest"] |
| 139 | +CMD ["start"] |
0 commit comments