From d3b250a0b82662689cb22964fa1184a2760fa5aa Mon Sep 17 00:00:00 2001 From: Daniel Vallance Date: Mon, 13 Jul 2026 12:52:28 +0100 Subject: [PATCH] ARM CI --- .github/workflows/test-examples.yaml | 17 +- _testlib/readiness.py | 94 ++++ _testlib/unikraft.py | 143 ++++++- basic-ops/Dockerfile | 30 +- build-environments/Dockerfile | 29 +- caddy2.7-go1.21/Dockerfile | 16 +- conftest.py | 9 +- debian-ssh/Dockerfile | 2 +- dragonflydb/Dockerfile | 2 +- duckdb-go1.21/Dockerfile | 26 +- feature-change-instance-cmd/Dockerfile | 32 +- github-webhook-node/Dockerfile | 15 +- grafana/Dockerfile | 33 +- haproxy/Dockerfile | 2 +- httpserver-boost1.74-gpp13.2/Dockerfile | 29 +- httpserver-bun/Dockerfile | 2 +- httpserver-c-debug/Dockerfile | 2 +- httpserver-dotnet10.0/Dockerfile | 3 +- httpserver-elixir1.16/Dockerfile | 8 +- httpserver-erlang26.2/Dockerfile | 46 +- httpserver-expressjs4.18-node21/Dockerfile | 18 +- httpserver-flask-redis/flask/Dockerfile | 2 +- httpserver-flask-redis/redis/Dockerfile | 2 +- httpserver-gcc13.2/Dockerfile | 29 +- httpserver-go1.21/Dockerfile | 18 +- .../httpserver-go/Dockerfile | 18 +- httpserver-go1.22-redis/redis/Dockerfile | 2 +- httpserver-gpp13.2/Dockerfile | 37 +- httpserver-gpp13.2/README.md | 2 +- httpserver-java17-spring-petclinic/Dockerfile | 42 +- httpserver-java17-springboot/Dockerfile | 42 +- httpserver-java21/README.md | 2 +- httpserver-lua5.1/Dockerfile | 2 +- httpserver-nginx-vite-vanilla/Dockerfile | 14 +- httpserver-node-express-puppeteer/Dockerfile | 266 ++++-------- httpserver-node-vite-ssr-vanilla/Dockerfile | 22 +- httpserver-node21-nextjs/Dockerfile | 17 +- httpserver-node21-solid-start/Dockerfile | 17 +- httpserver-node22-react-router/Dockerfile | 12 +- httpserver-node22-sveltekit/Dockerfile | 34 +- httpserver-node26/Dockerfile | 2 +- httpserver-perl5.42/Dockerfile | 2 +- .../Dockerfile | 2 +- httpserver-python3.12-django5.0/Dockerfile | 2 +- .../Dockerfile | 2 +- .../Dockerfile | 2 +- httpserver-python3.12-flask3.0/Dockerfile | 2 +- httpserver-python3.12/Dockerfile | 2 +- httpserver-rust-trunkrs-leptos/Dockerfile | 42 +- httpserver-rust1.75-tokio/Dockerfile | 36 +- httpserver-rust1.88-actix-web4/Dockerfile | 37 +- httpserver-rust1.88-rocket0.5/Dockerfile | 37 +- httpserver-rust1.91/Dockerfile | 30 +- hugo0.122/Dockerfile | 23 +- imaginary/Dockerfile | 27 +- mariadb/Dockerfile | 2 +- mariadb/test_mariadb.py | 26 +- mcp-server-arxiv/Dockerfile | 9 +- mcp-server-simple/Dockerfile | 2 +- memcached1.6/Dockerfile | 2 +- memcached1.6/test_memcached1.6.py | 20 +- minecraft/Dockerfile | 2 +- minio/Dockerfile | 31 +- mongodb/Dockerfile | 2 +- mongodb/test_mongodb.py | 24 +- mysql/test_mysql.py | 26 +- neo4j/Dockerfile | 2 +- nginx-flask-mongo/README.md | 12 +- nginx-flask-mongo/flask/Dockerfile | 4 +- nginx-flask-mongo/mongo/Dockerfile | 2 +- nginx-flask-mongo/nginx/Dockerfile | 8 +- nginx-flask-mongo/nginx/Kraftfile | 2 +- nginx-flask-mongo/nginx/entrypoint.sh | 11 + nginx-flask-mongo/nginx/nginx.conf | 4 +- nginx-flask-mongo/test_nginx-flask-mongo.py | 8 +- nginx/Dockerfile | 2 +- node-code-execution/Dockerfile | 13 +- node-playwright-chromium/Dockerfile | 2 +- node-playwright-firefox/Dockerfile | 2 +- node-playwright-webkit/Dockerfile | 2 +- node18-agario/Dockerfile | 2 +- node18-wingsio/Dockerfile | 2 +- node21-websocket/Dockerfile | 17 +- node24-karaoke/Dockerfile | 34 +- novnc-browser/Dockerfile | 82 +++- novnc-browser/image/tint2_startup.sh | 13 + novnc-browser/image/xvfb_startup.sh | 6 +- novnc-browser/mozillateam-ppa.asc | 29 ++ openclaw/Dockerfile | 2 +- opentelemetry-collector/Dockerfile | 2 +- phoenix-postgres/phoenix/Dockerfile | 94 +++- phoenix-postgres/postgres/Dockerfile | 32 +- postgres/Dockerfile | 212 +++++---- postgres/test_postgres.py | 30 +- python-playwright-chromium/Dockerfile | 403 ++++++++++-------- python-playwright-chromium/requirements.txt | 4 +- python-playwright-chromium/wrapper.sh | 2 +- redis7.2/Dockerfile | 2 +- redis7.2/test_redis7.2.py | 23 +- ruby3.2-rails/.dockerignore | 1 + ruby3.2-rails/Dockerfile | 69 ++- ruby3.2-rails/README.md | 2 +- skipper0.18/Dockerfile | 19 +- spin-wagi-http/Dockerfile | 48 ++- traefik/Dockerfile | 28 +- tyk/README.md | 14 +- tyk/redis/Dockerfile | 2 +- tyk/test_tyk.py | 19 +- tyk/tyk/Dockerfile | 16 +- tyk/tyk/rootfs/etc/tyk.conf | 2 +- visual-studio-code-server/Dockerfile | 6 +- vsftpd/Dockerfile | 2 +- wazero-import-go/Dockerfile | 18 +- wordpress-all-in-one/Dockerfile | 2 +- wordpress/test_wordpress.py | 10 +- wordpress/wordpress/Dockerfile | 18 +- wordpress/wordpress/wrapper.sh | 15 + 117 files changed, 2033 insertions(+), 855 deletions(-) create mode 100644 _testlib/readiness.py create mode 100755 nginx-flask-mongo/nginx/entrypoint.sh create mode 100644 novnc-browser/mozillateam-ppa.asc diff --git a/.github/workflows/test-examples.yaml b/.github/workflows/test-examples.yaml index 693ba4a0..650d7b4d 100644 --- a/.github/workflows/test-examples.yaml +++ b/.github/workflows/test-examples.yaml @@ -147,18 +147,25 @@ jobs: needs: detect-changes if: needs.detect-changes.outputs.matrix != '' timeout-minutes: 60 - runs-on: ubuntu-latest + runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: + os: [ ubuntu-latest, ubuntu-24.04-arm ] example: ${{ fromJson(needs.detect-changes.outputs.matrix) }} - name: pytest (${{ matrix.example }}) + name: pytest (${{ matrix.example }}) (${{ matrix.os }}) steps: - uses: actions/checkout@v5 + - name: Set up QEMU + if: matrix.os == 'ubuntu-24.04-arm' + uses: docker/setup-qemu-action@v4 + with: + image: tonistiigi/binfmt:qemu-v10.0.4 + - name: Set up Python uses: actions/setup-python@v5 with: @@ -175,7 +182,7 @@ jobs: pip install -r requirements.txt - name: Install Unikraft CLI - uses: unikraft/setup-action@main + uses: unikraft/setup-action@v1.0.1 with: version: ${{ inputs.unikraft_version || 'latest' }} @@ -204,11 +211,11 @@ jobs: - name: Run tests env: - UKC_TEST_ID: ${{ github.run_id }}-${{ matrix.example}}-${{ github.run_attempt }} + UKC_TEST_ID: ${{ github.run_id }}-${{ matrix.os }}-${{ matrix.example}}-${{ github.run_attempt }} run: exec pytest --log-cli-level=INFO "${{ matrix.example }}" - name: Cleanup test resources if: always() env: - UKC_TEST_ID: ${{ github.run_id }}-${{ matrix.example}}-${{ github.run_attempt }} + UKC_TEST_ID: ${{ github.run_id }}-${{ matrix.os }}-${{ matrix.example}}-${{ github.run_attempt }} run: bash scripts/cleanup-test-resources.sh diff --git a/_testlib/readiness.py b/_testlib/readiness.py new file mode 100644 index 00000000..9bc4767d --- /dev/null +++ b/_testlib/readiness.py @@ -0,0 +1,94 @@ +"""Readiness helper for services that are still warming up. + +``wait_instance(..., "running")`` reports the Unikraft Cloud *instance* +lifecycle state — the unikernel booted and its entrypoint started. It says +nothing about whether the service inside has finished initialising, and +because unikernels boot in milliseconds that second gap is routinely the +larger of the two. + +Databases make this especially visible: they accept TCP connections well +before they are ready to serve. PostgreSQL answers with ``FATAL: the database +system is starting up`` while ``initdb`` runs, and a client-side +``connect_timeout`` does not help — the connection itself *succeeds*, the +server simply rejects the session. + +:func:`retry_until_ready` closes that gap for connection-oriented clients, +mirroring the retry behaviour :mod:`_testlib.http_client` already provides for +HTTP endpoints. +""" + +from __future__ import annotations + +import logging +import time +from typing import Callable, TypeVar + +log = logging.getLogger(__name__) + +T = TypeVar("T") + +DEFAULT_TIMEOUT = 120.0 +DEFAULT_BACKOFF = 2.0 + + +def retry_until_ready( + fn: Callable[[], T], + *, + exceptions: type[BaseException] | tuple[type[BaseException], ...], + timeout: float = DEFAULT_TIMEOUT, + backoff: float = DEFAULT_BACKOFF, + description: str = "service", +) -> T: + """Call ``fn`` until it succeeds, returning its result. + + The budget is wall-clock (``timeout`` seconds in total), deliberately not a + retry count: these clients already carry their own multi-second connect + timeouts, so a fixed number of attempts would multiply into a worst case of + many minutes. A deadline bounds the total regardless of how long any single + attempt blocks. + + Only ``exceptions`` are retried; anything else propagates immediately, so a + genuine bug is never hidden behind a warm-up loop. If the budget expires, + the last failure is re-raised with its original traceback. + + Wrap whichever call actually performs I/O: + + * Eager clients (``psycopg2``, ``pymysql``) connect in the constructor, so + wrap the constructor and keep what it returns:: + + conn = retry_until_ready( + lambda: psycopg2.connect(...), + exceptions=psycopg2.OperationalError, + description="postgres", + ) + + * Lazy clients (``redis``, ``pymongo``, ``pymemcache``) do no I/O until + first use, so the constructor alone proves nothing. Build the client once + and wrap a cheap readiness probe instead:: + + client = redis.Redis(...) + retry_until_ready( + client.ping, + exceptions=redis.ConnectionError, + description="redis", + ) + """ + deadline = time.monotonic() + timeout + attempt = 0 + + while True: + attempt += 1 + try: + return fn() + except exceptions as exc: + remaining = deadline - time.monotonic() + log.warning( + "%s not ready (attempt %d, %.0fs of budget left): %s", + description, + attempt, + max(remaining, 0.0), + exc, + ) + if remaining <= backoff: + raise + time.sleep(backoff) diff --git a/_testlib/unikraft.py b/_testlib/unikraft.py index 91037b7c..f42d4347 100644 --- a/_testlib/unikraft.py +++ b/_testlib/unikraft.py @@ -10,8 +10,11 @@ import json import logging import os +import re import shutil import subprocess +import threading +import time from dataclasses import dataclass from typing import Any, Mapping, Sequence @@ -19,6 +22,13 @@ UNIKRAFT_BIN = os.environ.get("UNIKRAFT_BIN", "unikraft") +# BuildKit emits a step header (``#12 [build 3/4] RUN ...``) followed by a +# completion marker (``#12 DONE 245.3s``) carrying that step's wall time — the +# per-stage timings we care about. The byte-level layer progress in between +# (``#6 sha256:... 0B / 63.99MB``) is pure noise, so it is kept at DEBUG. +_ANSI_RE = re.compile(r"\x1b\[[0-9;?]*[a-zA-Z]") +_STEP_RE = re.compile(r"^#\d+\s+(?:\[|DONE\b|CACHED\b|ERROR\b)") + def _as_tuple(value: str | Sequence[str] | None) -> tuple[str, ...]: """Accept a single value or a sequence for repeatable flags.""" @@ -84,11 +94,28 @@ def run( check: bool = True, capture_output: bool = True, timeout: float | None = 600, + stream: bool = False, + env: Mapping[str, str] | None = None, ) -> subprocess.CompletedProcess[str]: + """Invoke the CLI. + + With ``stream=False`` the output is buffered and returned, which is + what the JSON-emitting commands need. With ``stream=True`` it is + surfaced line by line as it is produced: a long build then reports its + progress live, and — crucially — a build that is later killed by + ``timeout`` still leaves a record of how far it got. Buffered output is + discarded when the process is killed, which is why a timing-sensitive + command must not use it. + """ bin_path = _resolve_bin() cmd = [bin_path, *args] log.debug("exec: %s (cwd=%s)", " ".join(cmd), cwd) + if stream: + return self._run_streaming( + cmd, args, cwd=cwd, check=check, timeout=timeout, env=env + ) + proc = subprocess.run( cmd, cwd=str(cwd) if cwd else None, @@ -96,6 +123,7 @@ def run( capture_output=capture_output, text=True, timeout=timeout, + env={**os.environ, **env} if env else None, ) if proc.stdout: @@ -108,9 +136,79 @@ def run( f"stdout: {proc.stdout}\n" f"stderr: {proc.stderr}" ) - + return proc + def _run_streaming( + self, + cmd: Sequence[str], + args: Sequence[str], + *, + cwd: str | os.PathLike[str] | None, + check: bool, + timeout: float | None, + env: Mapping[str, str] | None, + ) -> subprocess.CompletedProcess[str]: + """Run ``cmd``, logging its merged output as it arrives. + + A reader thread pumps the pipe so the parent never blocks on a full + buffer while waiting out the timeout. + """ + popen = subprocess.Popen( + list(cmd), + cwd=str(cwd) if cwd else None, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + bufsize=1, + env={**os.environ, **env} if env else None, + ) + + collected: list[str] = [] + + def _pump() -> None: + assert popen.stdout is not None + for raw in popen.stdout: + line = _ANSI_RE.sub("", raw).rstrip() + collected.append(line) + # The CLI wraps its own output in box-drawing characters; + # strip them so BuildKit's step markers still match. + probe = line.lstrip("│┏┗├└ \t") + if _STEP_RE.match(probe) or "error" in probe.lower(): + log.info("%s", line) + else: + log.debug("%s", line) + + pump = threading.Thread(target=_pump, daemon=True) + pump.start() + + try: + popen.wait(timeout=timeout) + except subprocess.TimeoutExpired: + popen.kill() + popen.wait() + # Drain whatever is already buffered so the timeout still carries + # the progress so far, then give up on the reader: a grandchild + # (BuildKit) may keep the pipe open after the CLI is killed. The + # thread is a daemon, so it cannot hold up interpreter exit. + # Joined here and *not* in a `finally`, so this bounded wait is + # paid once rather than twice. + pump.join(timeout=5) + raise subprocess.TimeoutExpired( + list(cmd), timeout, output="\n".join(collected) + ) from None + + pump.join(timeout=5) + + output = "\n".join(collected) + if check and popen.returncode != 0: + raise UnikraftError( + f"`unikraft {' '.join(args)}` exited with {popen.returncode}\n" + f"output: {output}" + ) + + return subprocess.CompletedProcess(list(cmd), popen.returncode, output, "") + # ------------------------------------------------------------------ # High-level helpers # ------------------------------------------------------------------ @@ -121,18 +219,43 @@ def build( output: str, *, extra_args: Sequence[str] = (), + timeout: float | None = 1800, ) -> None: """Build an image from ``context`` and publish/tag it as ``output``. ``output`` is typically ``/:`` as shown in example READMEs, e.g. ``my-org/nginx:test``. + + The default timeout is deliberately generous: on a non-amd64 runner the + amd64 stages execute under QEMU and both architectures of every base + image have to be pulled, so a build that takes ~3 minutes on x86 can + take several times that. A too-tight limit SIGKILLs the build before it + can report anything, which hides the very information needed to tell + "genuinely too slow" from "slower than the limit". + + Progress is streamed, so BuildKit's per-step ``DONE s`` markers land + in the log and show where the time actually went. """ log.info( - "building image from context %s with output tag %s", - context, - output + "building image from context %s with output tag %s (timeout=%ss)", + context, + output, + timeout, ) - self.run(["build", str(context), "--output", output, *extra_args]) + + started = time.monotonic() + try: + self.run( + ["build", str(context), "--output", output, *extra_args], + timeout=timeout, + stream=True, + # Best-effort: ask for non-interactive progress so each step is + # emitted as its own line rather than a redrawn TTY display. + # Ignored by CLIs that do not honour it, which costs nothing. + env={"BUILDKIT_PROGRESS": "plain"}, + ) + finally: + log.info("build of %s took %.1fs", output, time.monotonic() - started) def run_instance( self, @@ -223,7 +346,9 @@ def run_instance( memory, name, ) + started = time.monotonic() proc = self.run(args) + log.info("instance start took %.1fs", time.monotonic() - started) return _parse_json(proc.stdout) @@ -251,6 +376,7 @@ def wait_instance( Returns the parsed JSON description of the instance once it reaches the desired state. """ + started = time.monotonic() proc = self.run( [ "instances", @@ -263,6 +389,12 @@ def wait_instance( ], timeout=timeout, ) + log.info( + "instance %s reached state %r after %.1fs", + target, + state, + time.monotonic() - started, + ) return _parse_json(proc.stdout) def delete_instance(self, target: str) -> None: @@ -362,7 +494,6 @@ def extract_instance_name(instance: dict[str, Any]) -> str: raise UnikraftError( "could not determine instance name/uuid from CLI output" ) - return name diff --git a/basic-ops/Dockerfile b/basic-ops/Dockerfile index b5259c02..487454ef 100644 --- a/basic-ops/Dockerfile +++ b/basic-ops/Dockerfile @@ -1,10 +1,36 @@ -FROM gcc:13.2.0-bookworm AS build +FROM --platform=$BUILDPLATFORM debian:bookworm AS build + +ARG TARGETARCH + +# Cross C toolchain for the target architecture via Debian multiarch (the same +# package names resolve to the native compiler when the build and target +# architectures match). libc6:${TARGETARCH} is installed in the same apt +# transaction so its runtime files land in the target multiarch lib dir and can +# be copied into the final image below. +RUN set -xe; \ + dpkg --add-architecture ${TARGETARCH}; \ + apt-get -yqq update; \ + if [ "$(dpkg --print-architecture)" = "${TARGETARCH}" ]; then \ + apt-get -yqq install build-essential; \ + else \ + apt-get -yqq install crossbuild-essential-${TARGETARCH} libc6:${TARGETARCH}; \ + fi WORKDIR /src COPY ./http_server.c /src/http_server.c -RUN gcc -Wall -Wextra -fPIC -pie -o /http_server http_server.c +RUN set -xe; \ + if [ "$(dpkg --print-architecture)" = "${TARGETARCH}" ]; then \ + CC=gcc; \ + else \ + case "${TARGETARCH}" in \ + amd64) CC=x86_64-linux-gnu-gcc ;; \ + arm64) CC=aarch64-linux-gnu-gcc ;; \ + *) echo "unsupported TARGETARCH: ${TARGETARCH}" >&2; exit 1 ;; \ + esac; \ + fi; \ + ${CC} -Wall -Wextra -fPIC -pie -o /http_server http_server.c FROM scratch diff --git a/build-environments/Dockerfile b/build-environments/Dockerfile index 201e7748..f16e72e0 100644 --- a/build-environments/Dockerfile +++ b/build-environments/Dockerfile @@ -1,19 +1,40 @@ -FROM golang:1.26.1-bookworm AS build +FROM --platform=$BUILDPLATFORM golang:1.26.1-bookworm AS build WORKDIR /src +ARG TARGETOS +ARG TARGETARCH COPY go.mod ./ COPY server.go ./ -RUN go build -o /server . \ +# The server loads Go plugins at runtime, which only works when the main +# binary is built with cgo. Cross-compiling would silently disable cgo +# (making plugin.Open return "plugin: not implemented"), so force it on and +# install a cross C toolchain when the build and target architectures differ. +RUN set -xe; \ + if [ "$(dpkg --print-architecture)" != "${TARGETARCH}" ]; then \ + apt-get update; \ + apt-get install -y --no-install-recommends crossbuild-essential-${TARGETARCH}; \ + case "${TARGETARCH}" in \ + amd64) export CC=x86_64-linux-gnu-gcc ;; \ + arm64) export CC=aarch64-linux-gnu-gcc ;; \ + *) echo "unsupported TARGETARCH: ${TARGETARCH}" >&2; exit 1 ;; \ + esac; \ + fi; \ + CGO_ENABLED=1 GOOS=${TARGETOS} GOARCH=${TARGETARCH} \ + go build -o /server . \ && go clean -cache -modcache \ && rm -rf /root/.cache /go/pkg/mod /src -FROM debian:bookworm-slim +# Go toolchain for the target architecture; /usr/local/go is copied into the +# final image. +FROM --platform=linux/x86_64 golang:1.26.1-bookworm as runtime + +FROM --platform=linux/x86_64 debian:bookworm-slim RUN apt-get update \ && apt-get install -y --no-install-recommends gcc libc6-dev ca-certificates \ && rm -rf /var/lib/apt/lists/* -COPY --from=build /usr/local/go /usr/local/go +COPY --from=runtime /usr/local/go /usr/local/go COPY --from=build /server /server COPY ./wrapper.sh /wrapper.sh diff --git a/caddy2.7-go1.21/Dockerfile b/caddy2.7-go1.21/Dockerfile index e8e7b067..d47f0ff8 100644 --- a/caddy2.7-go1.21/Dockerfile +++ b/caddy2.7-go1.21/Dockerfile @@ -1,16 +1,30 @@ -FROM --platform=linux/x86_64 golang:1.21.3-bookworm AS build +FROM --platform=$BUILDPLATFORM golang:1.21.3-bookworm AS build ARG CADDY_VERSION=2.7.6 +ARG TARGETOS +ARG TARGETARCH WORKDIR /caddy +# Cross-compile natively on the build host. -linkmode external requires a C +# toolchain for the target architecture, so install a cross toolchain when +# the build and target architectures differ. RUN --mount=type=cache,target=/root/go/pkg/mod \ --mount=type=cache,target=/root/.cache/go-build \ set -xe; \ apt-get update; \ apt-get install -y --no-install-recommends \ git; \ + if [ "$(dpkg --print-architecture)" != "${TARGETARCH}" ]; then \ + apt-get install -y --no-install-recommends crossbuild-essential-${TARGETARCH}; \ + case "${TARGETARCH}" in \ + amd64) export CC=x86_64-linux-gnu-gcc ;; \ + arm64) export CC=aarch64-linux-gnu-gcc ;; \ + *) echo "unsupported TARGETARCH: ${TARGETARCH}" >&2; exit 1 ;; \ + esac; \ + fi; \ git clone --depth=1 --branch v${CADDY_VERSION} https://github.com/caddyserver/caddy.git /caddy; \ + CGO_ENABLED=1 GOOS=${TARGETOS} GOARCH=${TARGETARCH} \ go build \ -buildmode=pie \ -ldflags "-linkmode external -extldflags -static-pie" \ diff --git a/conftest.py b/conftest.py index e77bef66..4d60ce2b 100644 --- a/conftest.py +++ b/conftest.py @@ -127,7 +127,12 @@ def test_foo(build_image): *before* this image-delete, ensuring the image is no longer in use. """ - def _build(example_dir: str, image_name: str) -> str: + def _build( + example_dir: str, + image_name: str, + *, + timeout: float | None = 1800, + ) -> str: context = repo_root / example_dir assert context.is_dir(), f"example directory not found: {context}" @@ -137,7 +142,7 @@ def _build(example_dir: str, image_name: str) -> str: # so a partial build is still cleaned up. request.addfinalizer(lambda: unikraft.delete_image(tag)) - unikraft.build(context, tag) + unikraft.build(context, tag, timeout=timeout) return tag diff --git a/debian-ssh/Dockerfile b/debian-ssh/Dockerfile index 88aa0123..7ef3f9ef 100644 --- a/debian-ssh/Dockerfile +++ b/debian-ssh/Dockerfile @@ -1,4 +1,4 @@ -FROM debian:bookworm AS build +FROM --platform=linux/x86_64 debian:bookworm AS build WORKDIR /src diff --git a/dragonflydb/Dockerfile b/dragonflydb/Dockerfile index 0706b34b..c6c9fccf 100644 --- a/dragonflydb/Dockerfile +++ b/dragonflydb/Dockerfile @@ -1,4 +1,4 @@ -FROM docker.dragonflydb.io/dragonflydb/dragonfly:v1.14.1 AS build +FROM --platform=linux/x86_64 docker.dragonflydb.io/dragonflydb/dragonfly:v1.14.1 AS build FROM scratch diff --git a/duckdb-go1.21/Dockerfile b/duckdb-go1.21/Dockerfile index ad9c9298..d43e83aa 100644 --- a/duckdb-go1.21/Dockerfile +++ b/duckdb-go1.21/Dockerfile @@ -1,10 +1,34 @@ -FROM golang:1.24.3-bookworm AS build +FROM --platform=$BUILDPLATFORM golang:1.24.3-bookworm AS build + +ARG TARGETOS +ARG TARGETARCH WORKDIR /src COPY ./src /src +# Cross-compile natively on the build host. cgo (DuckDB bindings) and +# -linkmode external require a C/C++ toolchain for the target architecture, +# so install a cross toolchain when the build and target architectures differ. +# +# Debian's cross libc ships libm.a as a linker script that references the +# static archives by their native multiarch path (/usr/lib//), which +# does not exist in the cross sysroot (/usr//lib/). Symlink the +# archives there so static -lm links resolve. RUN set -xe; \ + if [ "$(dpkg --print-architecture)" != "${TARGETARCH}" ]; then \ + apt-get update; \ + apt-get install -y --no-install-recommends crossbuild-essential-${TARGETARCH}; \ + case "${TARGETARCH}" in \ + amd64) TRIPLE=x86_64-linux-gnu ;; \ + arm64) TRIPLE=aarch64-linux-gnu ;; \ + *) echo "unsupported TARGETARCH: ${TARGETARCH}" >&2; exit 1 ;; \ + esac; \ + export CC=${TRIPLE}-gcc CXX=${TRIPLE}-g++; \ + mkdir -p /usr/lib/${TRIPLE}; \ + ln -sf /usr/${TRIPLE}/lib/*.a /usr/lib/${TRIPLE}/; \ + fi; \ + CGO_ENABLED=1 GOOS=${TARGETOS} GOARCH=${TARGETARCH} \ go build \ -buildmode=pie \ -ldflags "-linkmode external -extldflags -static-pie" \ diff --git a/feature-change-instance-cmd/Dockerfile b/feature-change-instance-cmd/Dockerfile index 6537630a..d0647dd4 100644 --- a/feature-change-instance-cmd/Dockerfile +++ b/feature-change-instance-cmd/Dockerfile @@ -1,12 +1,38 @@ -FROM gcc:13.2.0-bookworm AS build +FROM --platform=$BUILDPLATFORM debian:bookworm AS build + +ARG TARGETARCH + +# Cross C toolchain for the target architecture via Debian multiarch (the same +# package names resolve to the native compiler when the build and target +# architectures match). libc6:${TARGETARCH} is installed in the same apt +# transaction so its runtime files land in the target multiarch lib dir and can +# be copied into the final image below. +RUN set -xe; \ + dpkg --add-architecture ${TARGETARCH}; \ + apt-get -yqq update; \ + if [ "$(dpkg --print-architecture)" = "${TARGETARCH}" ]; then \ + apt-get -yqq install build-essential; \ + else \ + apt-get -yqq install crossbuild-essential-${TARGETARCH} libc6:${TARGETARCH}; \ + fi WORKDIR /src COPY ./http_server.c /src/http_server.c COPY ./http_server_args.c /src/http_server_args.c -RUN gcc -Wall -Wextra -fPIC -pie -o /http_server http_server.c -RUN gcc -Wall -Wextra -fPIC -pie -o /http_server_args http_server_args.c +RUN set -xe; \ + if [ "$(dpkg --print-architecture)" = "${TARGETARCH}" ]; then \ + CC=gcc; \ + else \ + case "${TARGETARCH}" in \ + amd64) CC=x86_64-linux-gnu-gcc ;; \ + arm64) CC=aarch64-linux-gnu-gcc ;; \ + *) echo "unsupported TARGETARCH: ${TARGETARCH}" >&2; exit 1 ;; \ + esac; \ + fi; \ + ${CC} -Wall -Wextra -fPIC -pie -o /http_server http_server.c; \ + ${CC} -Wall -Wextra -fPIC -pie -o /http_server_args http_server_args.c FROM scratch diff --git a/github-webhook-node/Dockerfile b/github-webhook-node/Dockerfile index 3315580b..cc7fbe00 100644 --- a/github-webhook-node/Dockerfile +++ b/github-webhook-node/Dockerfile @@ -1,4 +1,5 @@ -FROM node:24-alpine AS build +# Install dependencies natively on the build host — they are pure JavaScript. +FROM --platform=$BUILDPLATFORM node:24-alpine AS build WORKDIR /app @@ -6,13 +7,17 @@ COPY server.js package*.json ./ RUN npm install --omit=dev +# Node binary and system libraries for the target architecture (copy-only, +# nothing runs under emulation). +FROM --platform=linux/x86_64 node:24-alpine AS node + FROM scratch -COPY --from=build /usr/local/bin/node /usr/local/bin/node +COPY --from=node /usr/local/bin/node /usr/local/bin/node -COPY --from=build /lib/ld-musl-x86_64.so.1 /lib/ld-musl-x86_64.so.1 -COPY --from=build /usr/lib/libstdc++.so.6 /usr/lib/libstdc++.so.6 -COPY --from=build /usr/lib/libgcc_s.so.1 /usr/lib/libgcc_s.so.1 +COPY --from=node /lib/ld-musl-x86_64.so.1 /lib/ld-musl-x86_64.so.1 +COPY --from=node /usr/lib/libstdc++.so.6 /usr/lib/libstdc++.so.6 +COPY --from=node /usr/lib/libgcc_s.so.1 /usr/lib/libgcc_s.so.1 COPY --from=build /app/node_modules /app/node_modules COPY --from=build /app/server.js /app/server.js diff --git a/grafana/Dockerfile b/grafana/Dockerfile index 5bdc9cae..ae75f404 100644 --- a/grafana/Dockerfile +++ b/grafana/Dockerfile @@ -1,31 +1,44 @@ -FROM golang:1.21.4-alpine3.18 AS grafana-build +FROM --platform=$BUILDPLATFORM golang:1.21.4-bookworm AS grafana-build + +ARG TARGETOS +ARG TARGETARCH WORKDIR /grafana -# Dependencies +# Sources RUN set -xe; \ - apk --no-cache add \ - gcc \ - make \ - musl-dev \ - wget \ - ; \ wget -O /grafana.tar.gz https://github.com/grafana/grafana/archive/refs/tags/v10.2.2.tar.gz; \ tar -xzvf /grafana.tar.gz --strip-components 1 -C /grafana \ ; -# Build backend +# Build backend. Code generation runs natively on the build host; the go +# build cross-compiles to the target architecture. -linkmode external +# requires a C toolchain for the target architecture, so install a cross +# toolchain when the build and target architectures differ. RUN set -xe; \ make gen-go; \ + if [ "$(dpkg --print-architecture)" != "${TARGETARCH}" ]; then \ + apt-get update; \ + apt-get install -y --no-install-recommends crossbuild-essential-${TARGETARCH}; \ + case "${TARGETARCH}" in \ + amd64) export CC=x86_64-linux-gnu-gcc ;; \ + arm64) export CC=aarch64-linux-gnu-gcc ;; \ + *) echo "unsupported TARGETARCH: ${TARGETARCH}" >&2; exit 1 ;; \ + esac; \ + fi; \ CGO_ENABLED=1 \ + GOOS=${TARGETOS} \ + GOARCH=${TARGETARCH} \ go build -v \ -buildmode=pie \ -ldflags "-linkmode external -extldflags '-static-pie' -X 'main.version=10.2.2' -X 'main.commit=161e3ca' -X 'main.buildstamp=$(date)' -X 'main.buildBranch=main'" \ + -tags netgo \ -o /usr/share/grafana/bin/grafana \ ./pkg/cmd/grafana \ ; -FROM grafana/grafana:10.2.2 AS grafana +# Static assets (architecture-independent) from the upstream image. +FROM --platform=linux/x86_64 grafana/grafana:10.2.2 AS grafana FROM scratch diff --git a/haproxy/Dockerfile b/haproxy/Dockerfile index 0d9a3969..ecd94fca 100644 --- a/haproxy/Dockerfile +++ b/haproxy/Dockerfile @@ -1,4 +1,4 @@ -FROM haproxy:2.8.3-bullseye AS build +FROM --platform=linux/x86_64 haproxy:2.8.3-bullseye AS build FROM scratch diff --git a/httpserver-boost1.74-gpp13.2/Dockerfile b/httpserver-boost1.74-gpp13.2/Dockerfile index 86013d7c..2d58948b 100644 --- a/httpserver-boost1.74-gpp13.2/Dockerfile +++ b/httpserver-boost1.74-gpp13.2/Dockerfile @@ -1,16 +1,37 @@ -FROM --platform=linux/x86_64 debian:bookworm AS build +FROM --platform=$BUILDPLATFORM debian:bookworm AS build +ARG TARGETARCH + +# Install the Boost libraries (later copied into the final image) for the +# target architecture via Debian multiarch, plus a cross C++ toolchain when +# the build and target architectures differ. +# Toolchain and boost must be one apt transaction: as separate requests, +# apt may silently remove the cross toolchain again (-qq hides the +# "will be REMOVED" notice) while solving the boost:${TARGETARCH} closure. RUN set -xe ; \ + dpkg --add-architecture ${TARGETARCH} ; \ apt -yqq update ; \ - apt -yqq install build-essential ; \ - apt -yqq install libboost-all-dev + if [ "$(dpkg --print-architecture)" = "${TARGETARCH}" ]; then \ + apt -yqq install build-essential libboost-all-dev:${TARGETARCH} ; \ + else \ + apt -yqq install crossbuild-essential-${TARGETARCH} libboost-all-dev:${TARGETARCH} ; \ + fi WORKDIR /src COPY ./http_server.cpp /src/http_server.cpp RUN set -xe; \ - g++ \ + if [ "$(dpkg --print-architecture)" = "${TARGETARCH}" ]; then \ + CXX=g++; \ + else \ + case "${TARGETARCH}" in \ + amd64) CXX=x86_64-linux-gnu-g++ ;; \ + arm64) CXX=aarch64-linux-gnu-g++ ;; \ + *) echo "unsupported TARGETARCH: ${TARGETARCH}" >&2; exit 1 ;; \ + esac; \ + fi; \ + ${CXX} \ -Wall -Wextra \ -fPIC -pie \ -o /http_server http_server.cpp \ diff --git a/httpserver-bun/Dockerfile b/httpserver-bun/Dockerfile index 111a33b3..90c0f639 100644 --- a/httpserver-bun/Dockerfile +++ b/httpserver-bun/Dockerfile @@ -1,4 +1,4 @@ -FROM oven/bun:alpine AS bun +FROM --platform=linux/x86_64 oven/bun:alpine AS bun FROM scratch diff --git a/httpserver-c-debug/Dockerfile b/httpserver-c-debug/Dockerfile index eb4341ed..374013f4 100644 --- a/httpserver-c-debug/Dockerfile +++ b/httpserver-c-debug/Dockerfile @@ -1,4 +1,4 @@ -FROM gcc:13.2.0-bookworm AS build +FROM --platform=linux/x86_64 gcc:13.2.0-bookworm AS build WORKDIR /src diff --git a/httpserver-dotnet10.0/Dockerfile b/httpserver-dotnet10.0/Dockerfile index a8457f29..8be8aec5 100644 --- a/httpserver-dotnet10.0/Dockerfile +++ b/httpserver-dotnet10.0/Dockerfile @@ -8,7 +8,8 @@ RUN rm Program.cs COPY ./SimpleHttpServer.cs . RUN dotnet publish -a "$TARGETARCH" --self-contained -o /app -FROM --platform=$TARGETPLATFORM mcr.microsoft.com/dotnet/sdk:10.0 AS runtime +# Source of the x86_64 system libraries copied into the final image. +FROM --platform=linux/x86_64 mcr.microsoft.com/dotnet/sdk:10.0 AS runtime FROM scratch diff --git a/httpserver-elixir1.16/Dockerfile b/httpserver-elixir1.16/Dockerfile index 2072c043..d9486209 100644 --- a/httpserver-elixir1.16/Dockerfile +++ b/httpserver-elixir1.16/Dockerfile @@ -1,3 +1,5 @@ +# The release bundles the Erlang runtime (ERTS), so it must be built for the +# target architecture. FROM --platform=$BUILDPLATFORM elixir:1.16.2-slim AS build WORKDIR /src @@ -6,9 +8,9 @@ COPY . . RUN mix deps.get -RUN MIX_TARGET=$TARGETARCH mix compile && mix release +RUN mix compile && mix release -FROM alpine:3 AS sys +FROM --platform=$BUILDPLATFORM alpine:3 AS sys RUN set -xe; \ mkdir -p /target/etc; \ @@ -21,7 +23,7 @@ RUN set -xe; \ ln -sf /usr/share/zoneinfo/Etc/UTC /target/etc/localtime; \ echo "Etc/UTC" > /target/etc/timezone; -FROM --platform=$TARGETPLATFORM elixir:1.16.2-slim AS runtime +FROM --platform=linux/x86_64 elixir:1.16.2-slim AS runtime FROM scratch diff --git a/httpserver-erlang26.2/Dockerfile b/httpserver-erlang26.2/Dockerfile index 101b52d6..2b775e66 100644 --- a/httpserver-erlang26.2/Dockerfile +++ b/httpserver-erlang26.2/Dockerfile @@ -1,4 +1,6 @@ -FROM erlang:26.2.3.0-slim as build +# Compile the BEAM bytecode natively on the build host — the .beam file is +# architecture-independent. +FROM --platform=$BUILDPLATFORM erlang:26.2.3.0-slim as build WORKDIR /src @@ -7,7 +9,11 @@ COPY ./http_server.erl . RUN /usr/local/bin/erl -compile http_server -FROM alpine:3 AS sys +# Erlang runtime and x86_64 system libraries for the final image (copy-only, +# nothing runs under emulation). +FROM --platform=linux/x86_64 erlang:26.2.3.0-slim AS runtime + +FROM --platform=$BUILDPLATFORM alpine:3 AS sys RUN set -xe; \ mkdir -p /target/etc; \ @@ -28,28 +34,28 @@ COPY --from=sys /usr/share/zoneinfo/Etc/UTC /usr/share/zoneinfo/Etc/UTC COPY --from=sys /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt COPY --from=sys /blank /tmp -COPY --from=build /lib/x86_64-linux-gnu/libc.so.6 /lib/x86_64-linux-gnu/libc.so.6 -COPY --from=build /lib/x86_64-linux-gnu/libgcc_s.so.1 /lib/x86_64-linux-gnu/libgcc_s.so.1 -COPY --from=build /lib/x86_64-linux-gnu/libm.so.6 /lib/x86_64-linux-gnu/libm.so.6 -COPY --from=build /lib/x86_64-linux-gnu/libsctp.so.1 /lib/x86_64-linux-gnu/libsctp.so.1 -COPY --from=build /lib/x86_64-linux-gnu/libstdc++.so.6 /lib/x86_64-linux-gnu/libstdc++.so.6 -COPY --from=build /lib/x86_64-linux-gnu/libtinfo.so.6 /lib/x86_64-linux-gnu/libtinfo.so.6 -COPY --from=build /lib64/ld-linux-x86-64.so.2 /lib64/ld-linux-x86-64.so.2 +COPY --from=runtime /lib/x86_64-linux-gnu/libc.so.6 /lib/x86_64-linux-gnu/libc.so.6 +COPY --from=runtime /lib/x86_64-linux-gnu/libgcc_s.so.1 /lib/x86_64-linux-gnu/libgcc_s.so.1 +COPY --from=runtime /lib/x86_64-linux-gnu/libm.so.6 /lib/x86_64-linux-gnu/libm.so.6 +COPY --from=runtime /lib/x86_64-linux-gnu/libsctp.so.1 /lib/x86_64-linux-gnu/libsctp.so.1 +COPY --from=runtime /lib/x86_64-linux-gnu/libstdc++.so.6 /lib/x86_64-linux-gnu/libstdc++.so.6 +COPY --from=runtime /lib/x86_64-linux-gnu/libtinfo.so.6 /lib/x86_64-linux-gnu/libtinfo.so.6 +COPY --from=runtime /lib64/ld-linux-x86-64.so.2 /lib64/ld-linux-x86-64.so.2 -COPY --from=build /lib/x86_64-linux-gnu/libselinux.so.1 /lib/x86_64-linux-gnu/libselinux.so.1 -COPY --from=build /lib/x86_64-linux-gnu/libc.so.6 /lib/x86_64-linux-gnu/libc.so.6 -COPY --from=build /lib/x86_64-linux-gnu/libpcre2-8.so.0 /lib/x86_64-linux-gnu/libpcre2-8.so.0 -COPY --from=build /bin/rm /bin/rm -COPY --from=build /bin/mknod /bin/mknod +COPY --from=runtime /lib/x86_64-linux-gnu/libselinux.so.1 /lib/x86_64-linux-gnu/libselinux.so.1 +COPY --from=runtime /lib/x86_64-linux-gnu/libc.so.6 /lib/x86_64-linux-gnu/libc.so.6 +COPY --from=runtime /lib/x86_64-linux-gnu/libpcre2-8.so.0 /lib/x86_64-linux-gnu/libpcre2-8.so.0 +COPY --from=runtime /bin/rm /bin/rm +COPY --from=runtime /bin/mknod /bin/mknod -COPY --from=build /usr/local/bin/erl /usr/bin/erl -COPY --from=build /usr/local/lib/erlang /usr/local/lib/erlang +COPY --from=runtime /usr/local/bin/erl /usr/bin/erl +COPY --from=runtime /usr/local/lib/erlang /usr/local/lib/erlang -COPY --from=build /usr/bin/dirname /usr/bin/dirname -COPY --from=build /usr/bin/basename /usr/bin/basename +COPY --from=runtime /usr/bin/dirname /usr/bin/dirname +COPY --from=runtime /usr/bin/basename /usr/bin/basename -COPY --from=build /bin/bash /bin/bash -COPY --from=build /bin/sh /bin/sh +COPY --from=runtime /bin/bash /bin/bash +COPY --from=runtime /bin/sh /bin/sh COPY --from=build /src/http_server.beam /usr/src/http_server.beam diff --git a/httpserver-expressjs4.18-node21/Dockerfile b/httpserver-expressjs4.18-node21/Dockerfile index e5698257..6595617e 100644 --- a/httpserver-expressjs4.18-node21/Dockerfile +++ b/httpserver-expressjs4.18-node21/Dockerfile @@ -1,4 +1,6 @@ -FROM node:21-alpine AS build +# Install dependencies natively on the build host — express is pure +# JavaScript. +FROM --platform=$BUILDPLATFORM node:21-alpine AS build WORKDIR /usr/src @@ -6,18 +8,22 @@ COPY . /usr/src/ RUN npm install +# Node binary and system libraries for the target architecture (copy-only, +# nothing runs under emulation). +FROM --platform=linux/x86_64 node:21-alpine AS node + FROM scratch # Node binary -COPY --from=build /usr/local/bin/node /usr/bin/node +COPY --from=node /usr/local/bin/node /usr/bin/node # System libraries -COPY --from=build /lib/ld-musl-x86_64.so.1 /lib/ld-musl-x86_64.so.1 -COPY --from=build /usr/lib/libgcc_s.so.1 /usr/lib/libgcc_s.so.1 -COPY --from=build /usr/lib/libstdc++.so.6 /usr/lib/libstdc++.so.6 +COPY --from=node /lib/ld-musl-x86_64.so.1 /lib/ld-musl-x86_64.so.1 +COPY --from=node /usr/lib/libgcc_s.so.1 /usr/lib/libgcc_s.so.1 +COPY --from=node /usr/lib/libstdc++.so.6 /usr/lib/libstdc++.so.6 # Distribution configuration -COPY --from=build /etc/os-release /etc/os-release +COPY --from=node /etc/os-release /etc/os-release # Express.js COPY --from=build /usr/src/node_modules /usr/src/node_modules diff --git a/httpserver-flask-redis/flask/Dockerfile b/httpserver-flask-redis/flask/Dockerfile index 19f2f90b..e6ce2ed7 100644 --- a/httpserver-flask-redis/flask/Dockerfile +++ b/httpserver-flask-redis/flask/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.12-bookworm AS build +FROM --platform=linux/x86_64 python:3.12-bookworm AS build WORKDIR /app diff --git a/httpserver-flask-redis/redis/Dockerfile b/httpserver-flask-redis/redis/Dockerfile index b52f12d5..ff4dc126 100644 --- a/httpserver-flask-redis/redis/Dockerfile +++ b/httpserver-flask-redis/redis/Dockerfile @@ -1,4 +1,4 @@ -FROM redis:7.2.2-bookworm AS build +FROM --platform=linux/x86_64 redis:7.2.2-bookworm AS build FROM scratch diff --git a/httpserver-gcc13.2/Dockerfile b/httpserver-gcc13.2/Dockerfile index 025d05c9..5558c36d 100644 --- a/httpserver-gcc13.2/Dockerfile +++ b/httpserver-gcc13.2/Dockerfile @@ -1,11 +1,36 @@ -FROM --platform=linux/x86_64 gcc:13.2.0-bookworm AS build +FROM --platform=$BUILDPLATFORM debian:bookworm AS build + +ARG TARGETARCH + +# Cross C toolchain for the target architecture via Debian multiarch (the same +# package names resolve to the native compiler when the build and target +# architectures match). libc6:${TARGETARCH} is installed in the same apt +# transaction so its runtime files land in the target multiarch lib dir and can +# be copied into the final image below. +RUN set -xe; \ + dpkg --add-architecture ${TARGETARCH}; \ + apt-get -yqq update; \ + if [ "$(dpkg --print-architecture)" = "${TARGETARCH}" ]; then \ + apt-get -yqq install build-essential; \ + else \ + apt-get -yqq install crossbuild-essential-${TARGETARCH} libc6:${TARGETARCH}; \ + fi WORKDIR /src COPY ./http_server.c /src/http_server.c RUN set -xe; \ - gcc \ + if [ "$(dpkg --print-architecture)" = "${TARGETARCH}" ]; then \ + CC=gcc; \ + else \ + case "${TARGETARCH}" in \ + amd64) CC=x86_64-linux-gnu-gcc ;; \ + arm64) CC=aarch64-linux-gnu-gcc ;; \ + *) echo "unsupported TARGETARCH: ${TARGETARCH}" >&2; exit 1 ;; \ + esac; \ + fi; \ + ${CC} \ -Wall -Wextra \ -fPIC -pie \ -o /http_server http_server.c diff --git a/httpserver-go1.21/Dockerfile b/httpserver-go1.21/Dockerfile index b136fc10..1baeabc6 100644 --- a/httpserver-go1.21/Dockerfile +++ b/httpserver-go1.21/Dockerfile @@ -1,10 +1,26 @@ -FROM --platform=linux/x86_64 golang:1.21.3-bookworm AS build +FROM --platform=$BUILDPLATFORM golang:1.21.3-bookworm AS build + +ARG TARGETOS +ARG TARGETARCH WORKDIR /src COPY ./server.go /src/server.go +# Cross-compile natively on the build host. -linkmode external requires a C +# toolchain for the target architecture, so install a cross toolchain when +# the build and target architectures differ. RUN set -xe; \ + if [ "$(dpkg --print-architecture)" != "${TARGETARCH}" ]; then \ + apt-get update; \ + apt-get install -y --no-install-recommends crossbuild-essential-${TARGETARCH}; \ + case "${TARGETARCH}" in \ + amd64) export CC=x86_64-linux-gnu-gcc ;; \ + arm64) export CC=aarch64-linux-gnu-gcc ;; \ + *) echo "unsupported TARGETARCH: ${TARGETARCH}" >&2; exit 1 ;; \ + esac; \ + fi; \ + CGO_ENABLED=1 GOOS=${TARGETOS} GOARCH=${TARGETARCH} \ go build \ -buildmode=pie \ -ldflags "-linkmode external -extldflags -static-pie" \ diff --git a/httpserver-go1.22-redis/httpserver-go/Dockerfile b/httpserver-go1.22-redis/httpserver-go/Dockerfile index b97508ed..34333222 100644 --- a/httpserver-go1.22-redis/httpserver-go/Dockerfile +++ b/httpserver-go1.22-redis/httpserver-go/Dockerfile @@ -1,10 +1,26 @@ -FROM golang:1.22.2-bookworm AS build +FROM --platform=$BUILDPLATFORM golang:1.22.2-bookworm AS build + +ARG TARGETOS +ARG TARGETARCH WORKDIR /src COPY . /src +# Cross-compile natively on the build host. -linkmode external requires a C +# toolchain for the target architecture, so install a cross toolchain when +# the build and target architectures differ. RUN set -xe; \ + if [ "$(dpkg --print-architecture)" != "${TARGETARCH}" ]; then \ + apt-get update; \ + apt-get install -y --no-install-recommends crossbuild-essential-${TARGETARCH}; \ + case "${TARGETARCH}" in \ + amd64) export CC=x86_64-linux-gnu-gcc ;; \ + arm64) export CC=aarch64-linux-gnu-gcc ;; \ + *) echo "unsupported TARGETARCH: ${TARGETARCH}" >&2; exit 1 ;; \ + esac; \ + fi; \ + CGO_ENABLED=1 GOOS=${TARGETOS} GOARCH=${TARGETARCH} \ go build \ -v \ -buildmode=pie \ diff --git a/httpserver-go1.22-redis/redis/Dockerfile b/httpserver-go1.22-redis/redis/Dockerfile index 87bbb1a0..4d9f5473 100644 --- a/httpserver-go1.22-redis/redis/Dockerfile +++ b/httpserver-go1.22-redis/redis/Dockerfile @@ -1,4 +1,4 @@ -FROM redis:7.2.2-bookworm AS build +FROM --platform=linux/x86_64 redis:7.2.2-bookworm AS build FROM scratch diff --git a/httpserver-gpp13.2/Dockerfile b/httpserver-gpp13.2/Dockerfile index d1c89a60..a3dd33e5 100644 --- a/httpserver-gpp13.2/Dockerfile +++ b/httpserver-gpp13.2/Dockerfile @@ -1,11 +1,37 @@ -FROM --platform=linux/x86_64 gcc:13.2.0-bookworm AS build +FROM --platform=$BUILDPLATFORM debian:bookworm AS build + +ARG TARGETARCH + +# Cross C++ toolchain for the target architecture via Debian multiarch (the +# same package names resolve to the native compiler when the build and target +# architectures match). The C++ runtime libraries are installed as +# :${TARGETARCH} in the same apt transaction so they land in the target +# multiarch lib dir and can be copied into the final image below. +RUN set -xe; \ + dpkg --add-architecture ${TARGETARCH}; \ + apt-get -yqq update; \ + if [ "$(dpkg --print-architecture)" = "${TARGETARCH}" ]; then \ + apt-get -yqq install build-essential; \ + else \ + apt-get -yqq install crossbuild-essential-${TARGETARCH} \ + libc6:${TARGETARCH} libstdc++6:${TARGETARCH} libgcc-s1:${TARGETARCH}; \ + fi WORKDIR /src COPY ./http_server.cpp /src/http_server.cpp RUN set -xe; \ - g++ \ + if [ "$(dpkg --print-architecture)" = "${TARGETARCH}" ]; then \ + CXX=g++; \ + else \ + case "${TARGETARCH}" in \ + amd64) CXX=x86_64-linux-gnu-g++ ;; \ + arm64) CXX=aarch64-linux-gnu-g++ ;; \ + *) echo "unsupported TARGETARCH: ${TARGETARCH}" >&2; exit 1 ;; \ + esac; \ + fi; \ + ${CXX} \ -Wall -Wextra \ -fPIC -pie \ -o /http_server http_server.cpp @@ -13,12 +39,11 @@ RUN set -xe; \ FROM scratch # System / C++ libraries -COPY --from=build /lib/x86_64-linux-gnu/libc.so.6 /lib/x86_64-linux-gnu/ +COPY --from=build /lib/x86_64-linux-gnu/libc.so.6 /lib/x86_64-linux-gnu/libc.so.6 COPY --from=build /lib/x86_64-linux-gnu/libm.so.6 /lib/x86_64-linux-gnu/libm.so.6 -COPY --from=build /usr/local/lib64/libgcc_s.so.1 /usr/local/lib64/libgcc_s.so.1 -COPY --from=build /usr/local/lib64/libstdc++.so.6 /usr/local/lib64/libstdc++.so.6 +COPY --from=build /lib/x86_64-linux-gnu/libgcc_s.so.1 /lib/x86_64-linux-gnu/libgcc_s.so.1 +COPY --from=build /lib/x86_64-linux-gnu/libstdc++.so.6 /lib/x86_64-linux-gnu/libstdc++.so.6 COPY --from=build /lib64/ld-linux-x86-64.so.2 /lib64/ld-linux-x86-64.so.2 -COPY --from=build /etc/ld.so.cache /etc/ld.so.cache # C++ HTTP server COPY --from=build /http_server /http_server diff --git a/httpserver-gpp13.2/README.md b/httpserver-gpp13.2/README.md index 9d69f47b..1764828b 100644 --- a/httpserver-gpp13.2/README.md +++ b/httpserver-gpp13.2/README.md @@ -168,7 +168,7 @@ Lines in the `Kraftfile` have the following roles: Lines in the `Dockerfile` have the following roles: -* `FROM --platform=linux/x86_64 gcc:13.2.0-bookworm AS build`: Build the filesystem from the `bookworm gcc` container image, to [create a base image](https://docs.docker.com/build/building/base-images/). +* `FROM gcc:13.2.0-bookworm AS build`: Build the filesystem from the `bookworm gcc` container image, to [create a base image](https://docs.docker.com/build/building/base-images/). * `COPY ./http_server.cpp /src/http_server.cpp`: Copy the server implementation file (`http_server.cpp`) in the Docker filesystem (in `/src/http_server.cpp`). diff --git a/httpserver-java17-spring-petclinic/Dockerfile b/httpserver-java17-spring-petclinic/Dockerfile index d881827b..53633cd5 100644 --- a/httpserver-java17-spring-petclinic/Dockerfile +++ b/httpserver-java17-spring-petclinic/Dockerfile @@ -1,4 +1,6 @@ -FROM --platform=linux/x86_64 debian:bookworm AS build +# Build the application natively on the build host — the resulting jar is +# architecture-independent. +FROM --platform=$BUILDPLATFORM debian:bookworm AS build RUN set -xe ; \ apt -yqq update ; \ @@ -7,8 +9,6 @@ RUN set -xe ; \ apt -yqq install curl zip unzip git \ ; -RUN ldconfig /usr/lib/jvm/java-17-openjdk-amd64/lib/ - ARG BUILD_COMMIT=d8fcd11e RUN git clone https://github.com/spring-projects/spring-petclinic.git /src @@ -18,19 +18,35 @@ RUN git checkout -b build ${BUILD_COMMIT} RUN --mount=type=cache,target=/root/.m2 ./mvnw package +# JVM and x86_64 system libraries for the final image, installed for the +# target architecture via Debian multiarch so dpkg/apt run natively (only the +# JDK's own postinst hooks run under emulation). +FROM --platform=$BUILDPLATFORM debian:bookworm AS jvm + +ARG TARGETARCH + +RUN set -xe ; \ + dpkg --add-architecture ${TARGETARCH} ; \ + apt -yqq update ; \ + apt -yqq install default-jre:${TARGETARCH} ; \ + apt -yqq install default-jdk:${TARGETARCH} \ + ; + +RUN ldconfig /usr/lib/jvm/java-17-openjdk-amd64/lib/ + FROM scratch -COPY --from=build /lib/x86_64-linux-gnu/libc.so.6 /lib/x86_64-linux-gnu/libc.so.6 -COPY --from=build /lib/x86_64-linux-gnu/libstdc++.so.6 /lib/x86_64-linux-gnu/libstdc++.so.6 -COPY --from=build /lib/x86_64-linux-gnu/libm.so.6 /lib/x86_64-linux-gnu/libm.so.6 -COPY --from=build /usr/lib/x86_64-linux-gnu/libz.so.1 /usr/lib/x86_64-linux-gnu/libz.so.1 -COPY --from=build /lib/x86_64-linux-gnu/libgcc_s.so.1 /lib/x86_64-linux-gnu/libgcc_s.so.1 -COPY --from=build /lib64/ld-linux-x86-64.so.2 /lib64/ld-linux-x86-64.so.2 -COPY --from=build /etc/ld.so.cache /etc/ld.so.cache +COPY --from=jvm /lib/x86_64-linux-gnu/libc.so.6 /lib/x86_64-linux-gnu/libc.so.6 +COPY --from=jvm /lib/x86_64-linux-gnu/libstdc++.so.6 /lib/x86_64-linux-gnu/libstdc++.so.6 +COPY --from=jvm /lib/x86_64-linux-gnu/libm.so.6 /lib/x86_64-linux-gnu/libm.so.6 +COPY --from=jvm /usr/lib/x86_64-linux-gnu/libz.so.1 /usr/lib/x86_64-linux-gnu/libz.so.1 +COPY --from=jvm /lib/x86_64-linux-gnu/libgcc_s.so.1 /lib/x86_64-linux-gnu/libgcc_s.so.1 +COPY --from=jvm /lib64/ld-linux-x86-64.so.2 /lib64/ld-linux-x86-64.so.2 +COPY --from=jvm /etc/ld.so.cache /etc/ld.so.cache -COPY --from=build /usr/lib/jvm/java-17-openjdk-amd64/ /usr/lib/jvm/java-17-openjdk-amd64/ -COPY --from=build /etc/ssl/certs/java/cacerts /etc/ssl/certs/java/cacerts +COPY --from=jvm /usr/lib/jvm/java-17-openjdk-amd64/ /usr/lib/jvm/java-17-openjdk-amd64/ +COPY --from=jvm /etc/ssl/certs/java/cacerts /etc/ssl/certs/java/cacerts -COPY --from=build /etc/java-17-openjdk/security/ /etc/java-17-openjdk/security/ +COPY --from=jvm /etc/java-17-openjdk/security/ /etc/java-17-openjdk/security/ COPY --from=build /src/target/spring-petclinic-3.3.0-SNAPSHOT.jar /usr/src/spring-petclinic-3.3.0-SNAPSHOT.jar diff --git a/httpserver-java17-springboot/Dockerfile b/httpserver-java17-springboot/Dockerfile index 61c1d3ce..a20fc70a 100644 --- a/httpserver-java17-springboot/Dockerfile +++ b/httpserver-java17-springboot/Dockerfile @@ -1,4 +1,6 @@ -FROM --platform=linux/x86_64 debian:bookworm AS build +# Build the application natively on the build host — the resulting jar is +# architecture-independent. +FROM --platform=$BUILDPLATFORM debian:bookworm AS build RUN set -xe ; \ apt -yqq update ; \ @@ -7,8 +9,6 @@ RUN set -xe ; \ apt -yqq install curl zip \ ; -RUN ldconfig /usr/lib/jvm/java-17-openjdk-amd64/lib/ - WORKDIR /src ARG BOOT_VERSION=4.1.0 @@ -36,22 +36,38 @@ COPY DemoApplication.java src/main/java/com/example/demo/ RUN set -xe ; \ ./mvnw compile package install +# JVM and x86_64 system libraries for the final image, installed for the +# target architecture via Debian multiarch so dpkg/apt run natively (only the +# JDK's own postinst hooks run under emulation). +FROM --platform=$BUILDPLATFORM debian:bookworm AS jvm + +ARG TARGETARCH + +RUN set -xe ; \ + dpkg --add-architecture ${TARGETARCH} ; \ + apt -yqq update ; \ + apt -yqq install default-jre:${TARGETARCH} ; \ + apt -yqq install default-jdk:${TARGETARCH} \ + ; + +RUN ldconfig /usr/lib/jvm/java-17-openjdk-amd64/lib/ + RUN rm -f /usr/lib/jvm/java-17-openjdk-amd64/lib/src.zip && \ cp -rL /usr/lib/jvm/java-17-openjdk-amd64/ /jvm-resolved/ FROM scratch -COPY --from=build /lib/x86_64-linux-gnu/libc.so.6 /lib/x86_64-linux-gnu/libc.so.6 -COPY --from=build /lib/x86_64-linux-gnu/libstdc++.so.6 /lib/x86_64-linux-gnu/libstdc++.so.6 -COPY --from=build /lib/x86_64-linux-gnu/libm.so.6 /lib/x86_64-linux-gnu/libm.so.6 -COPY --from=build /usr/lib/x86_64-linux-gnu/libz.so.1 /usr/lib/x86_64-linux-gnu/libz.so.1 -COPY --from=build /lib/x86_64-linux-gnu/libgcc_s.so.1 /lib/x86_64-linux-gnu/libgcc_s.so.1 -COPY --from=build /lib64/ld-linux-x86-64.so.2 /lib64/ld-linux-x86-64.so.2 -COPY --from=build /etc/ld.so.cache /etc/ld.so.cache +COPY --from=jvm /lib/x86_64-linux-gnu/libc.so.6 /lib/x86_64-linux-gnu/libc.so.6 +COPY --from=jvm /lib/x86_64-linux-gnu/libstdc++.so.6 /lib/x86_64-linux-gnu/libstdc++.so.6 +COPY --from=jvm /lib/x86_64-linux-gnu/libm.so.6 /lib/x86_64-linux-gnu/libm.so.6 +COPY --from=jvm /usr/lib/x86_64-linux-gnu/libz.so.1 /usr/lib/x86_64-linux-gnu/libz.so.1 +COPY --from=jvm /lib/x86_64-linux-gnu/libgcc_s.so.1 /lib/x86_64-linux-gnu/libgcc_s.so.1 +COPY --from=jvm /lib64/ld-linux-x86-64.so.2 /lib64/ld-linux-x86-64.so.2 +COPY --from=jvm /etc/ld.so.cache /etc/ld.so.cache -COPY --from=build /jvm-resolved/ /usr/lib/jvm/java-17-openjdk-amd64/ -COPY --from=build /etc/ssl/certs/java/cacerts /etc/ssl/certs/java/cacerts +COPY --from=jvm /jvm-resolved/ /usr/lib/jvm/java-17-openjdk-amd64/ +COPY --from=jvm /etc/ssl/certs/java/cacerts /etc/ssl/certs/java/cacerts -COPY --from=build /etc/java-17-openjdk/security/ /etc/java-17-openjdk/security/ +COPY --from=jvm /etc/java-17-openjdk/security/ /etc/java-17-openjdk/security/ COPY --from=build /src/target/demo-0.0.1-SNAPSHOT.jar /usr/src/demo-0.0.1-SNAPSHOT.jar diff --git a/httpserver-java21/README.md b/httpserver-java21/README.md index 0b2671bf..80753e41 100644 --- a/httpserver-java21/README.md +++ b/httpserver-java21/README.md @@ -169,7 +169,7 @@ Lines in the `Kraftfile` have the following roles: Lines in the `Dockerfile` have the following roles: -* `FROM --platform=linux/x86_64 ubuntu:24.04 AS build`: Build the app using Ubuntu 24.04, which includes Java 21 in its official repositories. +* `FROM ubuntu:24.04 AS build`: Build the app using Ubuntu 24.04, which includes Java 21 in its official repositories. * `RUN apt-get ... install openjdk-21-jdk ...`: Install OpenJDK 21. diff --git a/httpserver-lua5.1/Dockerfile b/httpserver-lua5.1/Dockerfile index 22b519ec..e94d7216 100644 --- a/httpserver-lua5.1/Dockerfile +++ b/httpserver-lua5.1/Dockerfile @@ -1,4 +1,4 @@ -FROM nickblah/lua:5.1.5-luarocks-bionic AS build +FROM --platform=linux/x86_64 nickblah/lua:5.1.5-luarocks-bionic AS build RUN set -xe; \ apt update; \ diff --git a/httpserver-nginx-vite-vanilla/Dockerfile b/httpserver-nginx-vite-vanilla/Dockerfile index 4e8b5b9d..744a689c 100644 --- a/httpserver-nginx-vite-vanilla/Dockerfile +++ b/httpserver-nginx-vite-vanilla/Dockerfile @@ -1,4 +1,4 @@ -FROM node:23 AS build +FROM --platform=$BUILDPLATFORM node:23 AS build WORKDIR /app @@ -8,14 +8,9 @@ RUN set -xe; \ npm ci; \ npm run build -FROM nginx:1.25.3-bookworm AS nginx +RUN touch /empty -RUN set -xe; \ - rm -f /var/log/nginx/access.log; \ - rm -f /var/log/nginx/error.log; \ - touch /var/log/nginx/access.log; \ - touch /var/log/nginx/error.log \ - ; +FROM --platform=linux/x86_64 nginx:1.25.3-bookworm AS nginx FROM scratch @@ -25,9 +20,10 @@ COPY --from=nginx /usr/lib/nginx /usr/lib/nginx COPY --from=nginx /etc/nginx /etc/nginx COPY --from=nginx /etc/passwd /etc/passwd COPY --from=nginx /etc/group /etc/group -COPY --from=nginx /var/log/nginx /var/log/nginx COPY --from=nginx /var/cache/nginx /var/cache/nginx COPY --from=nginx /var/run /var/run +COPY --from=build /empty /var/log/nginx/access.log +COPY --from=build /empty /var/log/nginx/error.log # Libraries COPY --from=nginx /lib/x86_64-linux-gnu/libcrypt.so.1 /lib/x86_64-linux-gnu/libcrypt.so.1 diff --git a/httpserver-node-express-puppeteer/Dockerfile b/httpserver-node-express-puppeteer/Dockerfile index a8aff568..a0c69344 100644 --- a/httpserver-node-express-puppeteer/Dockerfile +++ b/httpserver-node-express-puppeteer/Dockerfile @@ -1,188 +1,108 @@ -FROM debian:bookworm AS build +# The unikernel targets linux/amd64, but nothing here needs to *execute* +# x86_64 code at build time, so every stage runs natively on the build host: +# npm runs on the native Node, the amd64 shared libraries are unpacked from +# prebuilt Debian packages with dpkg-deb (which runs no maintainer scripts), +# and the base userland is copied out of an amd64 image that is only ever read, +# never run. + +### +### Application build (native) +### +# Puppeteer downloads Chrome for Testing for the *linux* platform, which Google +# only publishes as linux64 (x86_64) and which @puppeteer/browsers selects +# regardless of the host CPU. So installing on the native (arm) builder still +# fetches the x86_64 Chrome the final amd64 image needs. The app's +# dependencies are pure JavaScript, so node_modules is architecture-independent. +FROM --platform=$BUILDPLATFORM debian:bookworm AS app RUN set -xe; \ apt-get -yqq update; \ - apt-get -yqq install \ - libcups2 \ - libnss3 \ - libatk1.0-0 \ - libnspr4 \ - libpango1.0-0 \ - libasound2 \ - libatspi2.0-0 \ - libxdamage1 \ - libatk-bridge2.0-0 \ - libxkbcommon0; \ - apt-get -yqq install \ - git \ - nodejs \ - npm \ - ; + apt-get -yqq install --no-install-recommends git nodejs npm ca-certificates WORKDIR /app -COPY . . - +COPY package*.json ./ RUN npm install +COPY . . -RUN mkdir /home/tmp +### +### amd64 shared libraries and node binary (native) +### +# dpkg-deb -x unpacks the amd64 debs without running any of their (x86_64) +# maintainer scripts. The last three packages are dlopen'd by Chrome/NSS and +# are not captured by package dependencies, so they are requested explicitly. +# +# Dir::State::status=/dev/null is load-bearing: it makes apt treat *nothing* as +# installed. Without it, --download-only fetches only what apt would actually +# install and silently skips whatever the builder already has — which makes the +# result depend on the build host's architecture. On an arm64 builder amd64 is +# foreign, nothing is "already installed", and the closure comes out complete. +# On an amd64 builder amd64 is native, so libc6 and ~30 other base packages of +# debian:bookworm are skipped and the rootfs ships with no libc at all, failing +# at boot with "/bin/sh: libc.so.6: cannot open shared object file" (exit 127). +# The empty status file makes the download host-architecture-independent. +FROM --platform=$BUILDPLATFORM debian:bookworm AS deps +RUN set -xe; \ + dpkg --add-architecture amd64; \ + apt-get -yqq update; \ + apt-get -yqq install --no-install-recommends --download-only \ + -o Dir::State::status=/dev/null \ + libcups2:amd64 \ + libnss3:amd64 \ + libatk1.0-0:amd64 \ + libnspr4:amd64 \ + libpango1.0-0:amd64 \ + libasound2:amd64 \ + libatspi2.0-0:amd64 \ + libxdamage1:amd64 \ + libatk-bridge2.0-0:amd64 \ + libxkbcommon0:amd64 \ + nodejs:amd64 \ + libsqlite3-0:amd64 \ + libsm6:amd64 \ + libudev1:amd64 \ + ; \ + mkdir /rootfs; \ + for deb in /var/cache/apt/archives/*.deb; do dpkg-deb -x "$deb" /rootfs; done; \ + find /rootfs -name libc.so.6 | grep -q . || \ + { echo 'ERROR: no libc in rootfs -- apt skipped already-installed packages' >&2; exit 1; }; \ + mkdir -p /rootfs/root; \ + mkdir -p -m 1777 /rootfs/tmp + +### +### amd64 base userland (copy-only, never executed -> not emulated) +### +# A plain amd64 debian image supplies a correctly-formed base filesystem +# (the /bin/sh alternative symlink, /etc scaffolding such as passwd/nsswitch, +# the dynamic loader) that dpkg-deb -x cannot reproduce because those files are +# generated by maintainer scripts. Nothing runs in this stage, so pinning it +# to x86_64 costs no emulation. +FROM --platform=linux/x86_64 debian:bookworm AS base + +### +### Final root file system +### FROM scratch -# Create required directories. -COPY --from=build /home/tmp /tmp - -# Chrome binary -COPY --from=build /root/.cache/puppeteer/chrome /root/.cache/puppeteer/chrome - -# Chrome libraries -COPY --from=build /lib/x86_64-linux-gnu/libdl.so.2 \ - /lib/x86_64-linux-gnu/libpthread.so.0 \ - /lib/x86_64-linux-gnu/libgobject-2.0.so.0 \ - /lib/x86_64-linux-gnu/libglib-2.0.so.0 \ - /lib/x86_64-linux-gnu/libnss3.so \ - /lib/x86_64-linux-gnu/libnssutil3.so \ - /lib/x86_64-linux-gnu/libsmime3.so \ - /lib/x86_64-linux-gnu/libnspr4.so \ - /lib/x86_64-linux-gnu/libatk-1.0.so.0 \ - /lib/x86_64-linux-gnu/libatk-bridge-2.0.so.0 \ - /lib/x86_64-linux-gnu/libcups.so.2 \ - /lib/x86_64-linux-gnu/libgio-2.0.so.0 \ - /lib/x86_64-linux-gnu/libdrm.so.2 \ - /lib/x86_64-linux-gnu/libdbus-1.so.3 \ - /lib/x86_64-linux-gnu/libexpat.so.1 \ - /lib/x86_64-linux-gnu/libxcb.so.1 \ - /lib/x86_64-linux-gnu/libxkbcommon.so.0 \ - /lib/x86_64-linux-gnu/libatspi.so.0 \ - /lib/x86_64-linux-gnu/libm.so.6 \ - /lib/x86_64-linux-gnu/libX11.so.6 \ - /lib/x86_64-linux-gnu/libXcomposite.so.1 \ - /lib/x86_64-linux-gnu/libXdamage.so.1 \ - /lib/x86_64-linux-gnu/libXext.so.6 \ - /lib/x86_64-linux-gnu/libXfixes.so.3 \ - /lib/x86_64-linux-gnu/libXrandr.so.2 \ - /lib/x86_64-linux-gnu/libgbm.so.1 \ - /lib/x86_64-linux-gnu/libpango-1.0.so.0 \ - /lib/x86_64-linux-gnu/libcairo.so.2 \ - /lib/x86_64-linux-gnu/libasound.so.2 \ - /lib/x86_64-linux-gnu/libgcc_s.so.1 \ - /lib/x86_64-linux-gnu/libc.so.6 \ - /lib/x86_64-linux-gnu/libffi.so.8 \ - /lib/x86_64-linux-gnu/libpcre2-8.so.0 \ - /lib/x86_64-linux-gnu/libplc4.so \ - /lib/x86_64-linux-gnu/libplds4.so \ - /lib/x86_64-linux-gnu/libgssapi_krb5.so.2 \ - /lib/x86_64-linux-gnu/libavahi-common.so.3 \ - /lib/x86_64-linux-gnu/libavahi-client.so.3 \ - /lib/x86_64-linux-gnu/libgnutls.so.30 \ - /lib/x86_64-linux-gnu/libz.so.1 \ - /lib/x86_64-linux-gnu/libgmodule-2.0.so.0 \ - /lib/x86_64-linux-gnu/libmount.so.1 \ - /lib/x86_64-linux-gnu/libselinux.so.1 \ - /lib/x86_64-linux-gnu/libsystemd.so.0 \ - /lib/x86_64-linux-gnu/libXau.so.6 \ - /lib/x86_64-linux-gnu/libXdmcp.so.6 \ - /lib/x86_64-linux-gnu/libXi.so.6 \ - /lib/x86_64-linux-gnu/libXrender.so.1 \ - /lib/x86_64-linux-gnu/libwayland-server.so.0 \ - /lib/x86_64-linux-gnu/libfribidi.so.0 \ - /lib/x86_64-linux-gnu/libthai.so.0 \ - /lib/x86_64-linux-gnu/libharfbuzz.so.0 \ - /lib/x86_64-linux-gnu/libpixman-1.so.0 \ - /lib/x86_64-linux-gnu/libfontconfig.so.1 \ - /lib/x86_64-linux-gnu/libfreetype.so.6 \ - /lib/x86_64-linux-gnu/libpng16.so.16 \ - /lib/x86_64-linux-gnu/libxcb-shm.so.0 \ - /lib/x86_64-linux-gnu/libxcb-render.so.0 \ - /lib/x86_64-linux-gnu/libkrb5.so.3 \ - /lib/x86_64-linux-gnu/libk5crypto.so.3 \ - /lib/x86_64-linux-gnu/libcom_err.so.2 \ - /lib/x86_64-linux-gnu/libkrb5support.so.0 \ - /lib/x86_64-linux-gnu/libp11-kit.so.0 \ - /lib/x86_64-linux-gnu/libidn2.so.0 \ - /lib/x86_64-linux-gnu/libunistring.so.2 \ - /lib/x86_64-linux-gnu/libtasn1.so.6 \ - /lib/x86_64-linux-gnu/libnettle.so.8 \ - /lib/x86_64-linux-gnu/libhogweed.so.6 \ - /lib/x86_64-linux-gnu/libgmp.so.10 \ - /lib/x86_64-linux-gnu/libblkid.so.1 \ - /lib/x86_64-linux-gnu/libcap.so.2 \ - /lib/x86_64-linux-gnu/libgcrypt.so.20 \ - /lib/x86_64-linux-gnu/liblzma.so.5 \ - /lib/x86_64-linux-gnu/libzstd.so.1 \ - /lib/x86_64-linux-gnu/liblz4.so.1 \ - /lib/x86_64-linux-gnu/libbsd.so.0 \ - /lib/x86_64-linux-gnu/libdatrie.so.1 \ - /lib/x86_64-linux-gnu/libgraphite2.so.3 \ - /lib/x86_64-linux-gnu/libbrotlidec.so.1 \ - /lib/x86_64-linux-gnu/libkeyutils.so.1 \ - /lib/x86_64-linux-gnu/libresolv.so.2 \ - /lib/x86_64-linux-gnu/libgpg-error.so.0 \ - /lib/x86_64-linux-gnu/libmd.so.0 \ - /lib/x86_64-linux-gnu/libbrotlicommon.so.1 \ - /lib/x86_64-linux-gnu/ - -# Other Chrome-related libraries -COPY --from=build /usr/lib/x86_64-linux-gnu/libfreebl3.so \ - /usr/lib/x86_64-linux-gnu/libfreeblpriv3.so \ - /usr/lib/x86_64-linux-gnu/libSM.so.6 \ - /usr/lib/x86_64-linux-gnu/libsoftokn3.so \ - /usr/lib/x86_64-linux-gnu/libsqlite3.so.0 \ - /usr/lib/x86_64-linux-gnu/libudev.so.1 \ - /usr/lib/x86_64-linux-gnu/ +# amd64 libraries, the node binary and its data files, unpacked from Debian +# packages. Copying the whole tree keeps every library in the path its package +# uses (/lib/x86_64-linux-gnu or /usr/lib/x86_64-linux-gnu), both of which the +# loader searches, so nothing has to be enumerated by hand. +COPY --from=deps /rootfs/ / -# Node binary -COPY --from=build /usr/bin/node /usr/bin/node +# Base userland from the amd64 image: shells, /etc, the loader. +COPY --from=base /bin/bash /bin/bash +COPY --from=base /bin/dash /bin/dash +COPY --from=base /bin/sh /bin/sh +COPY --from=base /lib64/ld-linux-x86-64.so.2 /lib64/ld-linux-x86-64.so.2 +COPY --from=base /etc /etc +COPY --from=base /usr/lib/os-release /usr/lib/os-release -# System libraries -COPY --from=build /lib/x86_64-linux-gnu/libnode.so.108 \ - /lib/x86_64-linux-gnu/libc.so.6 \ - /lib/x86_64-linux-gnu/libz.so.1 \ - /lib/x86_64-linux-gnu/libuv.so.1 \ - /lib/x86_64-linux-gnu/libbrotlidec.so.1 \ - /lib/x86_64-linux-gnu/libbrotlienc.so.1 \ - /lib/x86_64-linux-gnu/libcares.so.2 \ - /lib/x86_64-linux-gnu/libnghttp2.so.14 \ - /lib/x86_64-linux-gnu/libcrypto.so.3 \ - /lib/x86_64-linux-gnu/libssl.so.3 \ - /lib/x86_64-linux-gnu/libicui18n.so.72 \ - /lib/x86_64-linux-gnu/libicuuc.so.72 \ - /lib/x86_64-linux-gnu/libstdc++.so.6 \ - /lib/x86_64-linux-gnu/libm.so.6 \ - /lib/x86_64-linux-gnu/libgcc_s.so.1 \ - /lib/x86_64-linux-gnu/libpthread.so.0 \ - /lib/x86_64-linux-gnu/libdl.so.2 \ - /lib/x86_64-linux-gnu/libbrotlicommon.so.1 \ - /lib/x86_64-linux-gnu/libicudata.so.72 \ - /lib/x86_64-linux-gnu/librt.so.1 \ - /lib/x86_64-linux-gnu/ +# Chrome for Testing (x86_64), fetched by Puppeteer during the native build. +COPY --from=app /root/.cache/puppeteer/chrome /root/.cache/puppeteer/chrome -COPY --from=build /lib64/ld-linux-x86-64.so.2 /lib64/ld-linux-x86-64.so.2 -COPY --from=build /etc/ld.so.cache /etc/ld.so.cache - -# Dbus and system files -COPY --from=build /usr/lib/dbus-1.0 /usr/lib/dbus-1.0 -COPY --from=build /usr/lib/systemd /usr/lib/systemd -COPY --from=build /usr/lib/tmpfiles.d /usr/lib/tmpfiles.d -COPY --from=build /usr/lib/sysusers.d /usr/lib/sysusers.d -COPY --from=build /usr/lib/sysctl.d /usr/lib/sysctl.d - -# Data files -COPY --from=build /usr/share/fonts /usr/share/fonts -COPY --from=build /usr/share/nodejs /usr/share/nodejs - -COPY --from=build /run /run - -# Distro definition -COPY --from=build /etc/os-release /etc/os-release -COPY --from=build /usr/lib/os-release /usr/lib/os-release - -# Configuration files -COPY --from=build /etc /etc - -# Node modules, including Puppeteer and application -COPY --from=build /app /app +# Node application, including Puppeteer and the (architecture-independent) +# node_modules. +COPY --from=app /app /app COPY ./wrapper.sh /usr/bin/wrapper.sh - -COPY --from=build /bin/bash /bin/bash -COPY --from=build /bin/sh /bin/sh diff --git a/httpserver-node-vite-ssr-vanilla/Dockerfile b/httpserver-node-vite-ssr-vanilla/Dockerfile index 31451a34..4fe06892 100644 --- a/httpserver-node-vite-ssr-vanilla/Dockerfile +++ b/httpserver-node-vite-ssr-vanilla/Dockerfile @@ -1,4 +1,4 @@ -FROM node:23 AS build +FROM --platform=$BUILDPLATFORM node:23 AS build WORKDIR /app @@ -12,7 +12,25 @@ COPY . /app/ RUN set -xe; \ npm run build -FROM node:23-alpine3.20 +# The deployed server runs with NODE_ENV=production and never loads vite +# (dev mode is unsupported in the image: src/ is not shipped), so only the +# production dependencies are installed for the final image. They are pure +# JS, so this natively-built tree is valid for the x86_64 rootfs. +RUN set -xe; \ + npm ci --omit=dev + +# x86_64 node binary and its musl libraries; pulled, never run. +FROM --platform=linux/x86_64 node:23-alpine3.20 AS runtime + +FROM scratch + +# Node binary +COPY --from=runtime /usr/local/bin/node /usr/local/bin/node + +# System libraries +COPY --from=runtime /lib/ld-musl-x86_64.so.1 /lib/ld-musl-x86_64.so.1 +COPY --from=runtime /usr/lib/libgcc_s.so.1 /usr/lib/libgcc_s.so.1 +COPY --from=runtime /usr/lib/libstdc++.so.6 /usr/lib/libstdc++.so.6 COPY --from=build /app/dist /app/dist COPY --from=build /app/node_modules /app/node_modules diff --git a/httpserver-node21-nextjs/Dockerfile b/httpserver-node21-nextjs/Dockerfile index 4df05b8e..dee51f34 100644 --- a/httpserver-node21-nextjs/Dockerfile +++ b/httpserver-node21-nextjs/Dockerfile @@ -1,4 +1,6 @@ -FROM node:21-alpine AS build +# Build the app natively on the build host — the standalone output is +# architecture-independent JavaScript. +FROM --platform=$BUILDPLATFORM node:21-alpine AS build WORKDIR /usr/src @@ -8,18 +10,21 @@ RUN set -xe; \ npm install; \ npm run build +# Node binary and system libraries for the target architecture. +FROM --platform=linux/x86_64 node:21-alpine AS node + FROM scratch # Node binary -COPY --from=build /usr/local/bin/node /usr/bin/node +COPY --from=node /usr/local/bin/node /usr/bin/node # System libraries -COPY --from=build /lib/ld-musl-x86_64.so.1 /lib/ld-musl-x86_64.so.1 -COPY --from=build /usr/lib/libgcc_s.so.1 /usr/lib/libgcc_s.so.1 -COPY --from=build /usr/lib/libstdc++.so.6 /usr/lib/libstdc++.so.6 +COPY --from=node /lib/ld-musl-x86_64.so.1 /lib/ld-musl-x86_64.so.1 +COPY --from=node /usr/lib/libgcc_s.so.1 /usr/lib/libgcc_s.so.1 +COPY --from=node /usr/lib/libstdc++.so.6 /usr/lib/libstdc++.so.6 # Distribution configuration -COPY --from=build /etc/os-release /etc/os-release +COPY --from=node /etc/os-release /etc/os-release # NextJS COPY --from=build /usr/src/.next/standalone /usr/src/ diff --git a/httpserver-node21-solid-start/Dockerfile b/httpserver-node21-solid-start/Dockerfile index 7cd25375..c93fbf49 100644 --- a/httpserver-node21-solid-start/Dockerfile +++ b/httpserver-node21-solid-start/Dockerfile @@ -1,4 +1,6 @@ -FROM node:21-alpine AS build +# Build the app natively on the build host — the .output bundle is +# architecture-independent JavaScript. +FROM --platform=$BUILDPLATFORM node:21-alpine AS build WORKDIR /usr/src @@ -9,18 +11,21 @@ RUN set -xe && \ pnpm install && \ pnpm build +# Node binary and system libraries for the target architecture. +FROM --platform=linux/x86_64 node:21-alpine AS node + FROM scratch # Node binary -COPY --from=build /usr/local/bin/node /usr/bin/node +COPY --from=node /usr/local/bin/node /usr/bin/node # System libraries -COPY --from=build /lib/ld-musl-x86_64.so.1 /lib/ld-musl-x86_64.so.1 -COPY --from=build /usr/lib/libgcc_s.so.1 /usr/lib/libgcc_s.so.1 -COPY --from=build /usr/lib/libstdc++.so.6 /usr/lib/libstdc++.so.6 +COPY --from=node /lib/ld-musl-x86_64.so.1 /lib/ld-musl-x86_64.so.1 +COPY --from=node /usr/lib/libgcc_s.so.1 /usr/lib/libgcc_s.so.1 +COPY --from=node /usr/lib/libstdc++.so.6 /usr/lib/libstdc++.so.6 # Distribution configuration -COPY --from=build /etc/os-release /etc/os-release +COPY --from=node /etc/os-release /etc/os-release # Solid COPY --from=build /usr/src/.output/ /usr/src/ diff --git a/httpserver-node22-react-router/Dockerfile b/httpserver-node22-react-router/Dockerfile index 1aa19911..a0f06c95 100644 --- a/httpserver-node22-react-router/Dockerfile +++ b/httpserver-node22-react-router/Dockerfile @@ -1,8 +1,14 @@ -FROM node:22-alpine AS base +# Node binary, system libraries and runtime node_modules for the target +# architecture. +FROM --platform=linux/x86_64 node:22-alpine AS base WORKDIR /usr/src -FROM base AS build +# Build the app natively on the build host — the build output is +# architecture-independent JavaScript. +FROM --platform=$BUILDPLATFORM node:22-alpine AS build + +WORKDIR /usr/src COPY package.json /usr/src/package.json COPY package-lock.json /usr/src/package-lock.json @@ -11,6 +17,8 @@ RUN npm install COPY . /usr/src/ RUN npm run build +# Runtime dependencies are installed for the target architecture in case any +# of them ship native code. FROM base AS runtime COPY package.json /usr/src/package.json diff --git a/httpserver-node22-sveltekit/Dockerfile b/httpserver-node22-sveltekit/Dockerfile index 75090318..7ce4a288 100644 --- a/httpserver-node22-sveltekit/Dockerfile +++ b/httpserver-node22-sveltekit/Dockerfile @@ -1,4 +1,6 @@ -FROM node:22-alpine AS base +# Build the app natively on the build host — the build output is +# architecture-independent JavaScript. +FROM --platform=$BUILDPLATFORM node:22-alpine AS build WORKDIR /app @@ -6,30 +8,40 @@ COPY package.json /app RUN npm install -FROM base AS build - COPY . /app -RUN npm run build; \ - npm prune --production +RUN npm run build RUN <> /app/build/package.json {"type": "module"} EOF +# Node binary, system libraries and runtime node_modules for the target +# architecture in case any of them ship native code. +FROM --platform=linux/x86_64 node:22-alpine AS runtime + +WORKDIR /app + +COPY package.json /app + +# mkdir keeps the node_modules COPY below working even when there are no +# production dependencies to install (npm then creates no node_modules). +RUN npm install --omit=dev; \ + mkdir -p node_modules + FROM scratch # Node binary -COPY --from=build /usr/local/bin/node /usr/bin/node +COPY --from=runtime /usr/local/bin/node /usr/bin/node # System libraries -COPY --from=build /lib/ld-musl-x86_64.so.1 /lib/ld-musl-x86_64.so.1 -COPY --from=build /usr/lib/libgcc_s.so.1 /usr/lib/libgcc_s.so.1 -COPY --from=build /usr/lib/libstdc++.so.6 /usr/lib/libstdc++.so.6 +COPY --from=runtime /lib/ld-musl-x86_64.so.1 /lib/ld-musl-x86_64.so.1 +COPY --from=runtime /usr/lib/libgcc_s.so.1 /usr/lib/libgcc_s.so.1 +COPY --from=runtime /usr/lib/libstdc++.so.6 /usr/lib/libstdc++.so.6 # Distribution configuration -COPY --from=build /etc/os-release /etc/os-release +COPY --from=runtime /etc/os-release /etc/os-release # Svelte COPY --from=build /app/build /app/build -COPY --from=build /app/node_modules /app/node_modules +COPY --from=runtime /app/node_modules /app/node_modules diff --git a/httpserver-node26/Dockerfile b/httpserver-node26/Dockerfile index c53f6814..d58c8b72 100644 --- a/httpserver-node26/Dockerfile +++ b/httpserver-node26/Dockerfile @@ -1,4 +1,4 @@ -FROM node:26-alpine AS node +FROM --platform=linux/x86_64 node:26-alpine AS node FROM scratch diff --git a/httpserver-perl5.42/Dockerfile b/httpserver-perl5.42/Dockerfile index 81aea101..8caeda58 100644 --- a/httpserver-perl5.42/Dockerfile +++ b/httpserver-perl5.42/Dockerfile @@ -1,4 +1,4 @@ -FROM perl:5.42.0-bookworm AS build +FROM --platform=linux/x86_64 perl:5.42.0-bookworm AS build RUN set -xe ; \ cpanm HTTP::Daemon diff --git a/httpserver-prisma-expressjs4.19-node18/Dockerfile b/httpserver-prisma-expressjs4.19-node18/Dockerfile index 993a6bb1..eb64f0f5 100644 --- a/httpserver-prisma-expressjs4.19-node18/Dockerfile +++ b/httpserver-prisma-expressjs4.19-node18/Dockerfile @@ -1,4 +1,4 @@ -FROM node:18-alpine AS build +FROM --platform=linux/x86_64 node:18-alpine AS build RUN set -xe; \ apk --no-cache add openssl3; diff --git a/httpserver-python3.12-django5.0/Dockerfile b/httpserver-python3.12-django5.0/Dockerfile index 06bf4869..12f3610c 100644 --- a/httpserver-python3.12-django5.0/Dockerfile +++ b/httpserver-python3.12-django5.0/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.12-bookworm AS build +FROM --platform=linux/x86_64 python:3.12-bookworm AS build WORKDIR /app diff --git a/httpserver-python3.12-fastapi-0.121.3/Dockerfile b/httpserver-python3.12-fastapi-0.121.3/Dockerfile index d8507f2a..cc6669b3 100644 --- a/httpserver-python3.12-fastapi-0.121.3/Dockerfile +++ b/httpserver-python3.12-fastapi-0.121.3/Dockerfile @@ -1,5 +1,5 @@ # Add the image of Python 3.12. -FROM python:3.12-bookworm AS build +FROM --platform=linux/x86_64 python:3.12-bookworm AS build # Check for newly installed shared libraries with error output if necessary. RUN set -xe; \ diff --git a/httpserver-python3.12-flask3.0-sqlite/Dockerfile b/httpserver-python3.12-flask3.0-sqlite/Dockerfile index d6f3e5fc..8391a85e 100644 --- a/httpserver-python3.12-flask3.0-sqlite/Dockerfile +++ b/httpserver-python3.12-flask3.0-sqlite/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.12-bookworm AS build +FROM --platform=linux/x86_64 python:3.12-bookworm AS build RUN set -xe; \ /usr/sbin/ldconfig /usr/local/lib diff --git a/httpserver-python3.12-flask3.0/Dockerfile b/httpserver-python3.12-flask3.0/Dockerfile index af331146..387f1856 100644 --- a/httpserver-python3.12-flask3.0/Dockerfile +++ b/httpserver-python3.12-flask3.0/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.12-bookworm AS build +FROM --platform=linux/x86_64 python:3.12-bookworm AS build RUN set -xe; \ /usr/sbin/ldconfig /usr/local/lib diff --git a/httpserver-python3.12/Dockerfile b/httpserver-python3.12/Dockerfile index c1f02d5b..6a56d855 100644 --- a/httpserver-python3.12/Dockerfile +++ b/httpserver-python3.12/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.12 AS build +FROM --platform=linux/x86_64 python:3.12 AS build RUN set -xe; \ /usr/sbin/ldconfig /usr/local/lib diff --git a/httpserver-rust-trunkrs-leptos/Dockerfile b/httpserver-rust-trunkrs-leptos/Dockerfile index 6e04248a..c4051bab 100644 --- a/httpserver-rust-trunkrs-leptos/Dockerfile +++ b/httpserver-rust-trunkrs-leptos/Dockerfile @@ -1,11 +1,20 @@ -FROM --platform=linux/x86_64 rust:1.88.0-slim-bullseye AS builder +# bookworm (glibc 2.36) is required: trunk downloads a prebuilt wasm-bindgen +# whose aarch64 binary is glibc-linked and needs glibc >= 2.34. +FROM --platform=$BUILDPLATFORM rust:1.88.0-slim-bookworm AS builder -# Install trunk from pre-built binary instead of compiling from source (~10min saved) +# Install trunk from pre-built binary instead of compiling from source (~10min saved). +# trunk runs on the build host, so pick the tarball matching its architecture. ARG TRUNK_VERSION=0.21.14 -RUN apt-get update && apt-get install -y --no-install-recommends wget ca-certificates \ - && wget -qO- "https://github.com/trunk-rs/trunk/releases/download/v${TRUNK_VERSION}/trunk-x86_64-unknown-linux-musl.tar.gz" \ - | tar -xz -C /usr/local/bin \ - && rm -rf /var/lib/apt/lists/* +RUN set -e; \ + apt-get update && apt-get install -y --no-install-recommends wget ca-certificates; \ + case "$(dpkg --print-architecture)" in \ + amd64) TRUNK_ARCH=x86_64-unknown-linux-musl ;; \ + arm64) TRUNK_ARCH=aarch64-unknown-linux-gnu ;; \ + *) echo "unsupported build architecture" >&2; exit 1 ;; \ + esac; \ + wget -qO- "https://github.com/trunk-rs/trunk/releases/download/v${TRUNK_VERSION}/trunk-${TRUNK_ARCH}.tar.gz" \ + | tar -xz -C /usr/local/bin; \ + rm -rf /var/lib/apt/lists/* # Only need the wasm32 target; trunk auto-downloads the correct wasm-bindgen-cli RUN rustup target add wasm32-unknown-unknown @@ -21,16 +30,19 @@ COPY ./shared_lib ./shared_lib ENV RUSTFLAGS='--cfg getrandom_backend="wasm_js"' RUN trunk build --config ./frontend/Trunk.toml --release -EXPOSE 8080 +# Empty log files for the final image. nginx opens its default error log +# before it parses "error_log off", so the file must exist; the stock nginx +# image ships /var/log/nginx/{access,error}.log as symlinks to the container's +# stdout/stderr, which do not resolve under the unikernel. Creating real empty +# files here (a plain file operation on the native build host) keeps the amd64 +# nginx stage below free of any RUN step, so it is never emulated on arm. +RUN mkdir -p /var/log/nginx; \ + : > /var/log/nginx/access.log; \ + : > /var/log/nginx/error.log -FROM nginx:1.25.3-bookworm AS nginx +EXPOSE 8080 -RUN set -xe; \ - rm -f /var/log/nginx/access.log; \ - rm -f /var/log/nginx/error.log; \ - touch /var/log/nginx/access.log; \ - touch /var/log/nginx/error.log \ - ; +FROM --platform=linux/x86_64 nginx:1.25.3-bookworm AS nginx # Use a smaller base image for the final runtime # FROM nginx:1.27.4-alpine @@ -42,7 +54,7 @@ COPY --from=nginx /usr/lib/nginx /usr/lib/nginx COPY --from=nginx /etc/nginx /etc/nginx COPY --from=nginx /etc/passwd /etc/passwd COPY --from=nginx /etc/group /etc/group -COPY --from=nginx /var/log/nginx /var/log/nginx +COPY --from=builder /var/log/nginx /var/log/nginx COPY --from=nginx /var/cache/nginx /var/cache/nginx COPY --from=nginx /var/run /var/run diff --git a/httpserver-rust1.75-tokio/Dockerfile b/httpserver-rust1.75-tokio/Dockerfile index 0c01dad3..51fd6530 100644 --- a/httpserver-rust1.75-tokio/Dockerfile +++ b/httpserver-rust1.75-tokio/Dockerfile @@ -1,4 +1,6 @@ -FROM rust:1.75.0-bookworm AS build +FROM --platform=$BUILDPLATFORM rust:1.75.0-bookworm AS build + +ARG TARGETARCH WORKDIR /src @@ -6,12 +8,32 @@ COPY ./src /src/src COPY ./Cargo.toml /src/Cargo.toml COPY ./Cargo.lock /src/Cargo.lock -RUN cargo build +# Cross-compile natively on the build host, installing the Rust target and a +# cross C toolchain (for linking) when the build and target architectures +# differ. +RUN set -xe; \ + case "${TARGETARCH}" in \ + amd64) RUST_TARGET=x86_64-unknown-linux-gnu ;; \ + arm64) RUST_TARGET=aarch64-unknown-linux-gnu ;; \ + *) echo "unsupported TARGETARCH: ${TARGETARCH}" >&2; exit 1 ;; \ + esac; \ + if [ "$(dpkg --print-architecture)" != "${TARGETARCH}" ]; then \ + rustup target add "${RUST_TARGET}"; \ + apt-get update; \ + apt-get install -y --no-install-recommends crossbuild-essential-${TARGETARCH}; \ + export CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER=x86_64-linux-gnu-gcc; \ + export CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc; \ + fi; \ + cargo build --target "${RUST_TARGET}"; \ + cp "/src/target/${RUST_TARGET}/debug/http-tokio" /server + +# Source of the x86_64 system libraries copied into the final image. +FROM --platform=linux/x86_64 debian:bookworm-slim AS libs FROM scratch -COPY --from=build src/target/debug/http-tokio /server -COPY --from=build /lib/x86_64-linux-gnu/libc.so.6 /lib/x86_64-linux-gnu/libc.so.6 -COPY --from=build /lib/x86_64-linux-gnu/libm.so.6 /lib/x86_64-linux-gnu/libm.so.6 -COPY --from=build /lib/x86_64-linux-gnu/libgcc_s.so.1 /lib/x86_64-linux-gnu/libgcc_s.so.1 -COPY --from=build /lib64/ld-linux-x86-64.so.2 /lib64/ld-linux-x86-64.so.2 +COPY --from=build /server /server +COPY --from=libs /lib/x86_64-linux-gnu/libc.so.6 /lib/x86_64-linux-gnu/libc.so.6 +COPY --from=libs /lib/x86_64-linux-gnu/libm.so.6 /lib/x86_64-linux-gnu/libm.so.6 +COPY --from=libs /lib/x86_64-linux-gnu/libgcc_s.so.1 /lib/x86_64-linux-gnu/libgcc_s.so.1 +COPY --from=libs /lib64/ld-linux-x86-64.so.2 /lib64/ld-linux-x86-64.so.2 diff --git a/httpserver-rust1.88-actix-web4/Dockerfile b/httpserver-rust1.88-actix-web4/Dockerfile index 9809f1ac..3b444952 100644 --- a/httpserver-rust1.88-actix-web4/Dockerfile +++ b/httpserver-rust1.88-actix-web4/Dockerfile @@ -1,16 +1,39 @@ -FROM rust:1.88.0-bookworm AS build +FROM --platform=$BUILDPLATFORM rust:1.88.0-bookworm AS build + +ARG TARGETARCH RUN cargo new --bin app WORKDIR /app COPY Cargo.toml ./ COPY Cargo.lock ./ COPY src ./src -RUN cargo build --release + +# Cross-compile natively on the build host, installing the Rust target and a +# cross C toolchain (for linking) when the build and target architectures +# differ. +RUN set -xe; \ + case "${TARGETARCH}" in \ + amd64) RUST_TARGET=x86_64-unknown-linux-gnu ;; \ + arm64) RUST_TARGET=aarch64-unknown-linux-gnu ;; \ + *) echo "unsupported TARGETARCH: ${TARGETARCH}" >&2; exit 1 ;; \ + esac; \ + if [ "$(dpkg --print-architecture)" != "${TARGETARCH}" ]; then \ + rustup target add "${RUST_TARGET}"; \ + apt-get update; \ + apt-get install -y --no-install-recommends crossbuild-essential-${TARGETARCH}; \ + export CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER=x86_64-linux-gnu-gcc; \ + export CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc; \ + fi; \ + cargo build --release --target "${RUST_TARGET}"; \ + cp "/app/target/${RUST_TARGET}/release/hello" /server + +# Source of the x86_64 system libraries copied into the final image. +FROM --platform=linux/x86_64 debian:bookworm-slim AS libs FROM scratch -COPY --from=build /app/target/release/hello /server -COPY --from=build /lib/x86_64-linux-gnu/libc.so.6 /lib/x86_64-linux-gnu/ -COPY --from=build /lib/x86_64-linux-gnu/libm.so.6 /lib/x86_64-linux-gnu/ -COPY --from=build /lib/x86_64-linux-gnu/libgcc_s.so.1 /lib/x86_64-linux-gnu/ -COPY --from=build /lib64/ld-linux-x86-64.so.2 /lib64/ +COPY --from=build /server /server +COPY --from=libs /lib/x86_64-linux-gnu/libc.so.6 /lib/x86_64-linux-gnu/ +COPY --from=libs /lib/x86_64-linux-gnu/libm.so.6 /lib/x86_64-linux-gnu/ +COPY --from=libs /lib/x86_64-linux-gnu/libgcc_s.so.1 /lib/x86_64-linux-gnu/ +COPY --from=libs /lib64/ld-linux-x86-64.so.2 /lib64/ diff --git a/httpserver-rust1.88-rocket0.5/Dockerfile b/httpserver-rust1.88-rocket0.5/Dockerfile index 55129a73..86ef4a2f 100644 --- a/httpserver-rust1.88-rocket0.5/Dockerfile +++ b/httpserver-rust1.88-rocket0.5/Dockerfile @@ -1,4 +1,6 @@ -FROM --platform=x86_64 rust:1.88.0-bookworm AS build +FROM --platform=$BUILDPLATFORM rust:1.88.0-bookworm AS build + +ARG TARGETARCH RUN cargo new --bin app WORKDIR /app @@ -6,13 +8,34 @@ COPY Cargo.toml ./ COPY Cargo.lock ./ COPY Rocket.toml ./ COPY src ./src -RUN cargo build --release + +# Cross-compile natively on the build host, installing the Rust target and a +# cross C toolchain (for linking) when the build and target architectures +# differ. +RUN set -xe; \ + case "${TARGETARCH}" in \ + amd64) RUST_TARGET=x86_64-unknown-linux-gnu ;; \ + arm64) RUST_TARGET=aarch64-unknown-linux-gnu ;; \ + *) echo "unsupported TARGETARCH: ${TARGETARCH}" >&2; exit 1 ;; \ + esac; \ + if [ "$(dpkg --print-architecture)" != "${TARGETARCH}" ]; then \ + rustup target add "${RUST_TARGET}"; \ + apt-get update; \ + apt-get install -y --no-install-recommends crossbuild-essential-${TARGETARCH}; \ + export CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER=x86_64-linux-gnu-gcc; \ + export CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc; \ + fi; \ + cargo build --release --target "${RUST_TARGET}"; \ + cp "/app/target/${RUST_TARGET}/release/hello" /server + +# Source of the x86_64 system libraries copied into the final image. +FROM --platform=linux/x86_64 debian:bookworm-slim AS libs FROM scratch COPY ./Rocket.toml /Rocket.toml -COPY --from=build /app/target/release/hello /server -COPY --from=build /lib/x86_64-linux-gnu/libc.so.6 /lib/x86_64-linux-gnu/ -COPY --from=build /lib/x86_64-linux-gnu/libm.so.6 /lib/x86_64-linux-gnu/ -COPY --from=build /lib/x86_64-linux-gnu/libgcc_s.so.1 /lib/x86_64-linux-gnu/ -COPY --from=build /lib64/ld-linux-x86-64.so.2 /lib64/ld-linux-x86-64.so.2 +COPY --from=build /server /server +COPY --from=libs /lib/x86_64-linux-gnu/libc.so.6 /lib/x86_64-linux-gnu/ +COPY --from=libs /lib/x86_64-linux-gnu/libm.so.6 /lib/x86_64-linux-gnu/ +COPY --from=libs /lib/x86_64-linux-gnu/libgcc_s.so.1 /lib/x86_64-linux-gnu/ +COPY --from=libs /lib64/ld-linux-x86-64.so.2 /lib64/ld-linux-x86-64.so.2 diff --git a/httpserver-rust1.91/Dockerfile b/httpserver-rust1.91/Dockerfile index 738a46c4..c126ef85 100644 --- a/httpserver-rust1.91/Dockerfile +++ b/httpserver-rust1.91/Dockerfile @@ -1,15 +1,35 @@ -FROM --platform=linux/x86_64 rust:1.91.0-bookworm AS build +# Single-file compile: build natively on the build host, cross-compiling and +# installing a cross C toolchain (for linking) when the build and target +# architectures differ. +FROM --platform=$BUILDPLATFORM rust:1.91.0-bookworm AS build + +ARG TARGETARCH WORKDIR /src COPY ./server.rs /src/server.rs RUN set -xe; \ - rustc -o /server /src/server.rs + case "${TARGETARCH}" in \ + amd64) RUST_TARGET=x86_64-unknown-linux-gnu; CROSS_GCC=x86_64-linux-gnu-gcc ;; \ + arm64) RUST_TARGET=aarch64-unknown-linux-gnu; CROSS_GCC=aarch64-linux-gnu-gcc ;; \ + *) echo "unsupported TARGETARCH: ${TARGETARCH}" >&2; exit 1 ;; \ + esac; \ + LINKER_FLAGS=""; \ + if [ "$(dpkg --print-architecture)" != "${TARGETARCH}" ]; then \ + rustup target add "${RUST_TARGET}"; \ + apt-get update; \ + apt-get install -y --no-install-recommends crossbuild-essential-${TARGETARCH}; \ + LINKER_FLAGS="-C linker=${CROSS_GCC}"; \ + fi; \ + rustc --target "${RUST_TARGET}" ${LINKER_FLAGS} -o /server /src/server.rs + +# Source of the x86_64 system libraries copied into the final image. +FROM --platform=linux/x86_64 debian:bookworm-slim AS libs FROM scratch COPY --from=build /server /server -COPY --from=build /lib/x86_64-linux-gnu/libc.so.6 /lib/x86_64-linux-gnu/ -COPY --from=build /lib/x86_64-linux-gnu/libgcc_s.so.1 /lib/x86_64-linux-gnu/ -COPY --from=build /lib64/ld-linux-x86-64.so.2 /lib64/ +COPY --from=libs /lib/x86_64-linux-gnu/libc.so.6 /lib/x86_64-linux-gnu/ +COPY --from=libs /lib/x86_64-linux-gnu/libgcc_s.so.1 /lib/x86_64-linux-gnu/ +COPY --from=libs /lib64/ld-linux-x86-64.so.2 /lib64/ diff --git a/hugo0.122/Dockerfile b/hugo0.122/Dockerfile index 67d04cf7..45845f81 100644 --- a/hugo0.122/Dockerfile +++ b/hugo0.122/Dockerfile @@ -1,4 +1,5 @@ -FROM --platform=linux/x86_64 debian:bookworm-slim AS build +# Download the (target-architecture) hugo release natively on the build host. +FROM --platform=$BUILDPLATFORM debian:bookworm-slim AS build ARG HUGO_VERSION=0.160.0 @@ -9,20 +10,24 @@ RUN set -xe; \ | tar -xz -C /usr/local/bin hugo; \ rm -rf /var/lib/apt/lists/* +# x86_64 system libraries for the final image (copy-only, nothing runs under +# emulation). +FROM --platform=linux/x86_64 debian:bookworm-slim AS libs + FROM scratch # Binary executable COPY --from=build /usr/local/bin/hugo /usr/bin/hugo # System libraries -COPY --from=build /lib/x86_64-linux-gnu/libc.so.6 /lib/x86_64-linux-gnu/libc.so.6 -COPY --from=build /lib/x86_64-linux-gnu/libdl.so.2 /lib/x86_64-linux-gnu/libdl.so.2 -COPY --from=build /lib/x86_64-linux-gnu/libgcc_s.so.1 /lib/x86_64-linux-gnu/libgcc_s.so.1 -COPY --from=build /lib/x86_64-linux-gnu/libm.so.6 /lib/x86_64-linux-gnu/libm.so.6 -COPY --from=build /lib/x86_64-linux-gnu/libpthread.so.0 /lib/x86_64-linux-gnu/libpthread.so.0 -COPY --from=build /lib/x86_64-linux-gnu/libresolv.so.2 /lib/x86_64-linux-gnu/libresolv.so.2 -COPY --from=build /lib/x86_64-linux-gnu/libstdc++.so.6 /lib/x86_64-linux-gnu/libstdc++.so.6 -COPY --from=build /lib64/ld-linux-x86-64.so.2 /lib64/ld-linux-x86-64.so.2 +COPY --from=libs /lib/x86_64-linux-gnu/libc.so.6 /lib/x86_64-linux-gnu/libc.so.6 +COPY --from=libs /lib/x86_64-linux-gnu/libdl.so.2 /lib/x86_64-linux-gnu/libdl.so.2 +COPY --from=libs /lib/x86_64-linux-gnu/libgcc_s.so.1 /lib/x86_64-linux-gnu/libgcc_s.so.1 +COPY --from=libs /lib/x86_64-linux-gnu/libm.so.6 /lib/x86_64-linux-gnu/libm.so.6 +COPY --from=libs /lib/x86_64-linux-gnu/libpthread.so.0 /lib/x86_64-linux-gnu/libpthread.so.0 +COPY --from=libs /lib/x86_64-linux-gnu/libresolv.so.2 /lib/x86_64-linux-gnu/libresolv.so.2 +COPY --from=libs /lib/x86_64-linux-gnu/libstdc++.so.6 /lib/x86_64-linux-gnu/libstdc++.so.6 +COPY --from=libs /lib64/ld-linux-x86-64.so.2 /lib64/ld-linux-x86-64.so.2 # Site COPY ./site /site diff --git a/imaginary/Dockerfile b/imaginary/Dockerfile index 526204e6..d705466e 100644 --- a/imaginary/Dockerfile +++ b/imaginary/Dockerfile @@ -1,14 +1,23 @@ -FROM golang:1.21.6-bookworm AS build +FROM --platform=$BUILDPLATFORM golang:1.21.6-bookworm AS build ARG IMAGINARY_VERSION=1.2.4 +ARG TARGETOS +ARG TARGETARCH +# Install libvips (and its whole dependency tree, later copied into the final +# image) for the target architecture via Debian multiarch, plus a cross C +# toolchain when the build and target architectures differ. RUN set -xe; \ + dpkg --add-architecture ${TARGETARCH}; \ apt-get -yqq update; \ apt-get -yqq install \ wget \ unzip \ - libvips-dev \ - ; + libvips-dev:${TARGETARCH} \ + ; \ + if [ "$(dpkg --print-architecture)" != "${TARGETARCH}" ]; then \ + apt-get -yqq install crossbuild-essential-${TARGETARCH}; \ + fi WORKDIR /src @@ -18,8 +27,18 @@ RUN set -xe; \ ; RUN set -xe; \ + if [ "$(dpkg --print-architecture)" != "${TARGETARCH}" ]; then \ + case "${TARGETARCH}" in \ + amd64) TRIPLE=x86_64-linux-gnu ;; \ + arm64) TRIPLE=aarch64-linux-gnu ;; \ + *) echo "unsupported TARGETARCH: ${TARGETARCH}" >&2; exit 1 ;; \ + esac; \ + export CC=${TRIPLE}-gcc CXX=${TRIPLE}-g++; \ + export PKG_CONFIG_LIBDIR=/usr/lib/${TRIPLE}/pkgconfig:/usr/lib/pkgconfig:/usr/share/pkgconfig; \ + fi; \ cd imaginary-${IMAGINARY_VERSION}; \ - CGO_ENABLED=1 go build -buildmode=pie -o /usr/bin/imaginary \ + CGO_ENABLED=1 GOOS=${TARGETOS} GOARCH=${TARGETARCH} \ + go build -buildmode=pie -o /usr/bin/imaginary \ ; FROM scratch diff --git a/mariadb/Dockerfile b/mariadb/Dockerfile index 6841cbcd..8c3fa714 100644 --- a/mariadb/Dockerfile +++ b/mariadb/Dockerfile @@ -1,4 +1,4 @@ -FROM mariadb:11.7.2-noble AS build +FROM --platform=linux/x86_64 mariadb:11.7.2-noble AS build FROM scratch diff --git a/mariadb/test_mariadb.py b/mariadb/test_mariadb.py index bb287acc..23842425 100644 --- a/mariadb/test_mariadb.py +++ b/mariadb/test_mariadb.py @@ -17,6 +17,7 @@ import pymysql +from _testlib.readiness import retry_until_ready from _testlib.unikraft import extract_instance_fqdn, extract_instance_name MARIA_USER = "root" @@ -26,14 +27,23 @@ def _connect(port: int): - """Open a plaintext PyMySQL connection through the socat TLS tunnel.""" - return pymysql.connect( - host="127.0.0.1", - port=port, - user=MARIA_USER, - password=MARIA_PASSWORD, - database=MARIA_DATABASE, - connect_timeout=30, + """Open a plaintext PyMySQL connection through the socat TLS tunnel. + + Retries while the server initialises: on first boot mariadbd builds the + system tables before it accepts client sessions, and ``wait_instance`` + only proves the unikernel booted. + """ + return retry_until_ready( + lambda: pymysql.connect( + host="127.0.0.1", + port=port, + user=MARIA_USER, + password=MARIA_PASSWORD, + database=MARIA_DATABASE, + connect_timeout=30, + ), + exceptions=pymysql.err.MySQLError, + description="mariadb", ) diff --git a/mcp-server-arxiv/Dockerfile b/mcp-server-arxiv/Dockerfile index 96c0907a..0354ae2b 100644 --- a/mcp-server-arxiv/Dockerfile +++ b/mcp-server-arxiv/Dockerfile @@ -1,16 +1,19 @@ -FROM python:3.11-slim AS build +# uv binary for the target architecture. +FROM --platform=linux/x86_64 ghcr.io/astral-sh/uv:0.9.14 AS uv + +FROM --platform=linux/x86_64 python:3.11-slim AS build WORKDIR /app COPY requirements.txt requirements.txt RUN pip install -r requirements.txt -FROM python:3.11-slim +FROM --platform=linux/x86_64 python:3.11-slim COPY --from=build /usr/local/lib/python3.11/site-packages /usr/local/lib/python3.11/site-packages COPY --from=build /usr/local/bin /usr/local/bin -COPY --from=ghcr.io/astral-sh/uv:0.9.14 /uv /uvx /bin/ +COPY --from=uv /uv /uvx /bin/ RUN uv tool install arxiv-mcp-server COPY ./server.py /src/server.py diff --git a/mcp-server-simple/Dockerfile b/mcp-server-simple/Dockerfile index dd4ea8b6..eda4349f 100644 --- a/mcp-server-simple/Dockerfile +++ b/mcp-server-simple/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.11-slim AS build +FROM --platform=linux/x86_64 python:3.11-slim AS build WORKDIR /app diff --git a/memcached1.6/Dockerfile b/memcached1.6/Dockerfile index 3f289342..66435fa9 100644 --- a/memcached1.6/Dockerfile +++ b/memcached1.6/Dockerfile @@ -1,4 +1,4 @@ -FROM memcached:1.6.23-bookworm AS build +FROM --platform=linux/x86_64 memcached:1.6.23-bookworm AS build FROM scratch diff --git a/memcached1.6/test_memcached1.6.py b/memcached1.6/test_memcached1.6.py index 02652e40..7615d890 100644 --- a/memcached1.6/test_memcached1.6.py +++ b/memcached1.6/test_memcached1.6.py @@ -21,24 +21,38 @@ import ssl from pymemcache.client.base import Client as MemcacheClient -from pymemcache.exceptions import MemcacheUnexpectedCloseError +from pymemcache.exceptions import MemcacheError, MemcacheUnexpectedCloseError +from _testlib.readiness import retry_until_ready from _testlib.unikraft import extract_instance_fqdn, extract_instance_name MEMCACHED_PORT = 11211 def _connect(host: str) -> MemcacheClient: - """Open a pymemcache connection to the instance over TLS.""" + """Open a pymemcache client to the instance over TLS, ready to use. + + pymemcache connects lazily, so constructing the client proves nothing; + ``version()`` is a cheap command that forces the connection, and is + therefore what we retry while the server finishes starting. + """ ctx = ssl.create_default_context() - return MemcacheClient( + client = MemcacheClient( (host, MEMCACHED_PORT), tls_context=ctx, connect_timeout=30, timeout=30, ) + retry_until_ready( + client.version, + exceptions=(OSError, MemcacheError), + description="memcached", + ) + + return client + def test_memcached(build_image, run_instance, wait_instance): """Build, deploy, and exercise a Memcached instance.""" diff --git a/minecraft/Dockerfile b/minecraft/Dockerfile index e4e0df05..96d8163e 100644 --- a/minecraft/Dockerfile +++ b/minecraft/Dockerfile @@ -1,4 +1,4 @@ -FROM itzg/minecraft-server:java25 +FROM --platform=linux/x86_64 itzg/minecraft-server:java25 RUN set -xe; \ apt-get -yqq update; \ diff --git a/minio/Dockerfile b/minio/Dockerfile index 20249f2c..784a31fa 100644 --- a/minio/Dockerfile +++ b/minio/Dockerfile @@ -1,13 +1,7 @@ -FROM --platform=linux/x86_64 golang:1.21.4-alpine3.17 AS build +FROM --platform=$BUILDPLATFORM golang:1.21.4-bookworm AS build -RUN set -xe; \ - apk --no-cache add \ - gcc \ - musl-dev \ - wget \ - ca-certificates \ - ; \ - update-ca-certificates; +ARG TARGETOS +ARG TARGETARCH ARG MINIO_VERSION=2024-02-04T22-36-13Z ARG MINIO_COMMIT=f225ca331203032888889c1c4a3f96ee3452b9f9 @@ -19,11 +13,26 @@ RUN set -xe; \ WORKDIR /minio +# Cross-compile natively on the build host. -linkmode external requires a C +# toolchain for the target architecture, so install a cross toolchain when +# the build and target architectures differ. RUN set -xe; \ - CGO_ENABLED=1 \ + if [ "$(dpkg --print-architecture)" != "${TARGETARCH}" ]; then \ + apt-get update; \ + apt-get install -y --no-install-recommends crossbuild-essential-${TARGETARCH}; \ + case "${TARGETARCH}" in \ + amd64) export CC=x86_64-linux-gnu-gcc ;; \ + arm64) export CC=aarch64-linux-gnu-gcc ;; \ + *) echo "unsupported TARGETARCH: ${TARGETARCH}" >&2; exit 1 ;; \ + esac; \ + fi; \ + COPYRIGHT_YEAR="${MINIO_VERSION%%-*}"; \ + SHORT_COMMIT_ID="$(echo "${MINIO_COMMIT}" | cut -c 1-12)"; \ + CGO_ENABLED=1 GOOS=${TARGETOS} GOARCH=${TARGETARCH} \ go build -v \ -buildmode=pie \ - -ldflags "-linkmode external -extldflags '-static-pie' -X github.com/minio/minio/cmd.Version=${MINIO_VERSION} -X github.com/minio/minio/cmd.CopyrightYear=${MINIO_VERSION:0:4} -X github.com/minio/minio/cmd.ReleaseTag=RELEASE.${MINIO_VERSION} -X github.com/minio/minio/cmd.CommitID=${MINIO_COMMIT} -X github.com/minio/minio/cmd.ShortCommitID=${MINIO_COMMIT:0:12} -X github.com/minio/minio/cmd.GOPATH=/go -X github.com/minio/minio/cmd.GOROOT=" \ + -ldflags "-linkmode external -extldflags '-static-pie' -X github.com/minio/minio/cmd.Version=${MINIO_VERSION} -X github.com/minio/minio/cmd.CopyrightYear=${COPYRIGHT_YEAR} -X github.com/minio/minio/cmd.ReleaseTag=RELEASE.${MINIO_VERSION} -X github.com/minio/minio/cmd.CommitID=${MINIO_COMMIT} -X github.com/minio/minio/cmd.ShortCommitID=${SHORT_COMMIT_ID} -X github.com/minio/minio/cmd.GOPATH=/go -X github.com/minio/minio/cmd.GOROOT=" \ + -tags netgo \ -o /usr/bin/minio \ . diff --git a/mongodb/Dockerfile b/mongodb/Dockerfile index 0af71513..f66c10a8 100644 --- a/mongodb/Dockerfile +++ b/mongodb/Dockerfile @@ -1,4 +1,4 @@ -FROM mongo:6.0.13 as build +FROM --platform=linux/x86_64 mongo:6.0.13 as build RUN mkdir /blank diff --git a/mongodb/test_mongodb.py b/mongodb/test_mongodb.py index c3254398..2e0d47df 100644 --- a/mongodb/test_mongodb.py +++ b/mongodb/test_mongodb.py @@ -15,23 +15,38 @@ from __future__ import annotations from pymongo import MongoClient +from pymongo.errors import PyMongoError -from _testlib.unikraft import extract_instance_fqdn +from _testlib.readiness import retry_until_ready +from _testlib.unikraft import extract_instance_fqdn, extract_instance_name MONGO_PORT = 27017 def _connect(host: str) -> MongoClient: - """Open a pymongo connection to the instance over TLS.""" + """Open a pymongo client to the instance over TLS, ready to use. + + MongoClient connects lazily, so constructing it proves nothing; a ping is + the first call that actually performs server selection, and is therefore + what we retry while mongod finishes starting. + """ uri = f"mongodb://{host}:{MONGO_PORT}/?tls=true&directConnection=true" - return MongoClient( + client = MongoClient( uri, serverSelectionTimeoutMS=30000, connectTimeoutMS=30000, ) + retry_until_ready( + lambda: client.admin.command("ping"), + exceptions=PyMongoError, + description="mongodb", + ) + + return client + -def test_mongodb(build_image, run_instance): +def test_mongodb(build_image, run_instance, wait_instance): """Build, deploy, and exercise a MongoDB instance.""" image = build_image("mongodb", "mongodb") @@ -44,6 +59,7 @@ def test_mongodb(build_image, run_instance): host = extract_instance_fqdn(instance) assert host, f"could not determine instance FQDN from: {instance!r}" + wait_instance(extract_instance_name(instance), "running") client = _connect(host) try: # ------------------------------------------------------------------ diff --git a/mysql/test_mysql.py b/mysql/test_mysql.py index ad9cd4d5..f28129d6 100644 --- a/mysql/test_mysql.py +++ b/mysql/test_mysql.py @@ -17,6 +17,7 @@ import pymysql +from _testlib.readiness import retry_until_ready from _testlib.unikraft import extract_instance_fqdn, extract_instance_name MYSQL_USER = "root" @@ -26,14 +27,23 @@ def _connect(port: int): - """Open a plaintext PyMySQL connection through the socat TLS tunnel.""" - return pymysql.connect( - host="127.0.0.1", - port=port, - user=MYSQL_USER, - password=MYSQL_PASSWORD, - database=MYSQL_DATABASE, - connect_timeout=30, + """Open a plaintext PyMySQL connection through the socat TLS tunnel. + + Retries while the server initialises: on first boot mysqld builds the + system tables before it accepts client sessions, and ``wait_instance`` + only proves the unikernel booted. + """ + return retry_until_ready( + lambda: pymysql.connect( + host="127.0.0.1", + port=port, + user=MYSQL_USER, + password=MYSQL_PASSWORD, + database=MYSQL_DATABASE, + connect_timeout=30, + ), + exceptions=pymysql.err.MySQLError, + description="mysql", ) diff --git a/neo4j/Dockerfile b/neo4j/Dockerfile index 1938bc0c..1a061394 100644 --- a/neo4j/Dockerfile +++ b/neo4j/Dockerfile @@ -1,4 +1,4 @@ -FROM neo4j:trixie +FROM --platform=linux/x86_64 neo4j:trixie COPY neo4j.conf /var/lib/neo4j/conf/neo4j.conf diff --git a/nginx-flask-mongo/README.md b/nginx-flask-mongo/README.md index 6aa6b02e..55d4bf65 100644 --- a/nginx-flask-mongo/README.md +++ b/nginx-flask-mongo/README.md @@ -154,7 +154,8 @@ or ## Flask Next, deploy the Flask backend. -It connects to MongoDB using the `MONGO_SERVER_URL` environment variable and is reached internally via `backend.internal`: +It connects to MongoDB using the `MONGO_SERVER_URL` environment variable and is reached internally via `backend.internal`. +If you change this domain, set `BACKEND_HOST` on the NGINX instance to the same value. **Using the unikraft CLI (Recommended)** ```bash title="unikraft" @@ -223,19 +224,22 @@ or ## NGINX Finally, deploy NGINX as the public-facing reverse proxy. -It forwards requests to the Flask backend at `backend.internal:9091`: +It forwards requests to the Flask backend at `backend.internal:9091` by default. +The backend domain can be changed with the `BACKEND_HOST` environment variable; +if you use a different value, make sure it matches the domain you assign to the +Flask instance. **Using the unikraft CLI (Recommended)** ```bash title="unikraft" unikraft build ./nginx --output /nginx:latest -unikraft run --scale-to-zero policy=on,cooldown-time=1000 --metro fra -p 443:80/tls+http -m 512M --image /nginx:latest +unikraft run --scale-to-zero policy=on,cooldown-time=1000 --metro fra -p 443:80/tls+http -m 512M --image /nginx:latest -e BACKEND_HOST=backend.internal ``` or **Using the legacy kraft CLI** ```bash title="kraft" -kraft cloud deploy --scale-to-zero on --scale-to-zero-cooldown 1s -p 443:80/tls+http -M 512Mi ./nginx +kraft cloud deploy --scale-to-zero on --scale-to-zero-cooldown 1s -p 443:80/tls+http -M 512Mi --env BACKEND_HOST=backend.internal ./nginx ``` The output shows the NGINX instance details including its public FQDN: diff --git a/nginx-flask-mongo/flask/Dockerfile b/nginx-flask-mongo/flask/Dockerfile index 2c358c6c..b2bdb144 100644 --- a/nginx-flask-mongo/flask/Dockerfile +++ b/nginx-flask-mongo/flask/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.12-bookworm AS builder +FROM --platform=linux/x86_64 python:3.12-bookworm AS builder WORKDIR /app @@ -7,7 +7,7 @@ RUN pip3 install -r requirements.txt --no-cache-dir COPY . /app -FROM python:3.12-alpine +FROM --platform=linux/x86_64 python:3.12-alpine WORKDIR /app diff --git a/nginx-flask-mongo/mongo/Dockerfile b/nginx-flask-mongo/mongo/Dockerfile index 72fffae6..181c4fb8 100644 --- a/nginx-flask-mongo/mongo/Dockerfile +++ b/nginx-flask-mongo/mongo/Dockerfile @@ -1,4 +1,4 @@ -FROM mongo:6.0.13 as build +FROM --platform=linux/x86_64 mongo:6.0.13 as build RUN mkdir /blank diff --git a/nginx-flask-mongo/nginx/Dockerfile b/nginx-flask-mongo/nginx/Dockerfile index 7a03f5bc..eab0fc51 100644 --- a/nginx-flask-mongo/nginx/Dockerfile +++ b/nginx-flask-mongo/nginx/Dockerfile @@ -1,3 +1,7 @@ -FROM nginx:1.29.0-alpine +FROM --platform=linux/x86_64 nginx:1.29.0-alpine -COPY ./nginx.conf /etc/nginx/nginx.conf +COPY ./nginx.conf /etc/nginx/nginx.conf.template +COPY ./entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh + +CMD ["/entrypoint.sh"] diff --git a/nginx-flask-mongo/nginx/Kraftfile b/nginx-flask-mongo/nginx/Kraftfile index fcd014cf..5b37dc00 100644 --- a/nginx-flask-mongo/nginx/Kraftfile +++ b/nginx-flask-mongo/nginx/Kraftfile @@ -11,4 +11,4 @@ rootfs: source: ./Dockerfile format: erofs -cmd: [ "nginx" ] +cmd: [ "/entrypoint.sh" ] diff --git a/nginx-flask-mongo/nginx/entrypoint.sh b/nginx-flask-mongo/nginx/entrypoint.sh new file mode 100755 index 00000000..dff5ea2e --- /dev/null +++ b/nginx-flask-mongo/nginx/entrypoint.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env sh +set -e + +# BACKEND_HOST defaults to backend.internal if not set. +BACKEND_HOST="${BACKEND_HOST:-backend.internal}" +export BACKEND_HOST + +# Substitute environment variables into the NGINX template. +envsubst '${BACKEND_HOST}' < /etc/nginx/nginx.conf.template > /etc/nginx/nginx.conf + +exec nginx diff --git a/nginx-flask-mongo/nginx/nginx.conf b/nginx-flask-mongo/nginx/nginx.conf index a61f5ecc..2da296ee 100644 --- a/nginx-flask-mongo/nginx/nginx.conf +++ b/nginx-flask-mongo/nginx/nginx.conf @@ -27,7 +27,7 @@ http { server_name 127.0.0.1; location / { - proxy_pass http://backend.internal:9091; + proxy_pass http://${BACKEND_HOST}:9091; } } -} \ No newline at end of file +} diff --git a/nginx-flask-mongo/test_nginx-flask-mongo.py b/nginx-flask-mongo/test_nginx-flask-mongo.py index 8442b4c1..79937218 100644 --- a/nginx-flask-mongo/test_nginx-flask-mongo.py +++ b/nginx-flask-mongo/test_nginx-flask-mongo.py @@ -4,7 +4,7 @@ 1. Create a volume for MongoDB data persistence. 2. Build and deploy MongoDB (internal, on ``mongo-{test_run_id}.internal``). -3. Build and deploy Flask backend (internal, on ``backend.internal``). +3. Build and deploy Flask backend (internal, on ``backend-{test_run_id}.internal``). 4. Build and deploy Nginx reverse proxy (public, on port 443:80/tls+http). 5. ``curl https://`` and assert "Hello from the MongoDB client!". """ @@ -53,18 +53,19 @@ def _cleanup_volume(): ) # 3. Build and deploy Flask backend. - # NOTE: domain must be "backend.internal" — hardcoded in nginx/nginx.conf. + backend_domain = f"backend-{test_run_id}.internal" flask_image = build_image("nginx-flask-mongo/flask", "nfm-flask") run_instance( flask_image, memory="1024M", - domain="backend.internal", + domain=backend_domain, env={"FLASK_SERVER_PORT": "9091", "MONGO_SERVER_URL": f"{mongo_domain}:27017"}, name=f"flask-{test_run_id}", ) # 4. Build and deploy Nginx reverse proxy. + # BACKEND_HOST tells NGINX which internal domain to proxy to. nginx_image = build_image("nginx-flask-mongo/nginx", "nfm-nginx") nginx_instance = run_instance( @@ -72,6 +73,7 @@ def _cleanup_volume(): publish=["443:80/tls+http"], memory="512M", name=f"nginx-{test_run_id}", + env={"BACKEND_HOST": backend_domain}, ) url = extract_instance_url(nginx_instance) diff --git a/nginx/Dockerfile b/nginx/Dockerfile index 4ba9fa97..ad6a5707 100644 --- a/nginx/Dockerfile +++ b/nginx/Dockerfile @@ -1,4 +1,4 @@ -FROM nginx:1.25.3-bookworm AS build +FROM --platform=linux/x86_64 nginx:1.25.3-bookworm AS build RUN set -xe; \ rm -f /var/log/nginx/access.log; \ diff --git a/node-code-execution/Dockerfile b/node-code-execution/Dockerfile index 243b2dd1..1d404b00 100644 --- a/node-code-execution/Dockerfile +++ b/node-code-execution/Dockerfile @@ -1,9 +1,20 @@ -FROM node:25-slim +# Install dependencies natively on the build host — they are pure JavaScript. +FROM --platform=$BUILDPLATFORM node:25-slim AS build WORKDIR /src COPY package*.json ./ RUN npm install --omit=dev +# Target-architecture runtime image (copy-only, nothing runs under emulation). +FROM --platform=linux/x86_64 node:25-slim + +WORKDIR /src + +# Bring over the package manifests and node_modules together — npm doesn't +# create node_modules when there is nothing to install, so copying it by name +# would fail. +COPY --from=build /src/ ./ + COPY ./server.ts ./ COPY ./wrapper.sh /wrapper.sh diff --git a/node-playwright-chromium/Dockerfile b/node-playwright-chromium/Dockerfile index 3253a9a5..44ce9ea4 100644 --- a/node-playwright-chromium/Dockerfile +++ b/node-playwright-chromium/Dockerfile @@ -1,4 +1,4 @@ -FROM debian:bookworm AS build +FROM --platform=linux/x86_64 debian:bookworm AS build ARG NODE_VERSION=22.8.0 diff --git a/node-playwright-firefox/Dockerfile b/node-playwright-firefox/Dockerfile index e7b1b0ea..533c3a71 100644 --- a/node-playwright-firefox/Dockerfile +++ b/node-playwright-firefox/Dockerfile @@ -1,4 +1,4 @@ -FROM debian:bookworm AS build +FROM --platform=linux/x86_64 debian:bookworm AS build ARG NODE_VERSION=22.8.0 diff --git a/node-playwright-webkit/Dockerfile b/node-playwright-webkit/Dockerfile index d59ca60d..9d012889 100644 --- a/node-playwright-webkit/Dockerfile +++ b/node-playwright-webkit/Dockerfile @@ -1,4 +1,4 @@ -FROM debian:bookworm AS build +FROM --platform=linux/x86_64 debian:bookworm AS build ARG NODE_VERSION=22.8.0 diff --git a/node18-agario/Dockerfile b/node18-agario/Dockerfile index 173b5faf..8b530591 100644 --- a/node18-agario/Dockerfile +++ b/node18-agario/Dockerfile @@ -1,4 +1,4 @@ -FROM node:18-alpine AS build +FROM --platform=linux/x86_64 node:18-alpine AS build RUN set -xe; \ mkdir -p /usr/src/app; \ diff --git a/node18-wingsio/Dockerfile b/node18-wingsio/Dockerfile index f2328846..6fdbcded 100644 --- a/node18-wingsio/Dockerfile +++ b/node18-wingsio/Dockerfile @@ -1,4 +1,4 @@ -FROM node:18-alpine AS build +FROM --platform=linux/x86_64 node:18-alpine AS build RUN set -xe; \ mkdir -p /usr/src/app; \ diff --git a/node21-websocket/Dockerfile b/node21-websocket/Dockerfile index 29e5fa59..710be8bc 100644 --- a/node21-websocket/Dockerfile +++ b/node21-websocket/Dockerfile @@ -1,22 +1,27 @@ -FROM node:21-alpine AS build +# Install dependencies natively on the build host — ws is pure JavaScript. +FROM --platform=$BUILDPLATFORM node:21-alpine AS build WORKDIR /usr/src COPY package.json . COPY package-lock.json . RUN npm install +# Node binary and system libraries for the target architecture (copy-only, +# nothing runs under emulation). +FROM --platform=linux/x86_64 node:21-alpine AS node + FROM scratch # Node binary -COPY --from=build /usr/local/bin/node /usr/bin/node +COPY --from=node /usr/local/bin/node /usr/bin/node # System libraries -COPY --from=build /lib/ld-musl-x86_64.so.1 /lib/ld-musl-x86_64.so.1 -COPY --from=build /usr/lib/libgcc_s.so.1 /usr/lib/libgcc_s.so.1 -COPY --from=build /usr/lib/libstdc++.so.6 /usr/lib/libstdc++.so.6 +COPY --from=node /lib/ld-musl-x86_64.so.1 /lib/ld-musl-x86_64.so.1 +COPY --from=node /usr/lib/libgcc_s.so.1 /usr/lib/libgcc_s.so.1 +COPY --from=node /usr/lib/libstdc++.so.6 /usr/lib/libstdc++.so.6 # Distribution configuration -COPY --from=build /etc/os-release /etc/os-release +COPY --from=node /etc/os-release /etc/os-release COPY --from=build /usr/src /usr/src COPY ./server.js /usr/src/server.js diff --git a/node24-karaoke/Dockerfile b/node24-karaoke/Dockerfile index 2915cad1..f13b62bc 100644 --- a/node24-karaoke/Dockerfile +++ b/node24-karaoke/Dockerfile @@ -1,4 +1,6 @@ -FROM node:24-bookworm-slim AS build +# Build the app natively on the build host — the client bundle is +# architecture-independent static files. +FROM --platform=$BUILDPLATFORM node:24-bookworm-slim AS build RUN set -xe; \ apt-get update; \ @@ -19,31 +21,35 @@ RUN set -xe; \ pnpm exec playwright install; \ pnpm build -# Install serve globally for use as static file server +# Install serve globally for use as static file server (pure JavaScript) RUN npm install -g serve +# Node binary and system libraries for the target architecture (copy-only, +# nothing runs under emulation). +FROM --platform=linux/x86_64 node:24-bookworm-slim AS runtime + FROM scratch # System binaries -COPY --from=build /usr/local/bin/node /usr/local/bin/node -COPY --from=build /bin/sh /bin/sh -COPY --from=build /usr/bin/env /usr/bin/env +COPY --from=runtime /usr/local/bin/node /usr/local/bin/node +COPY --from=runtime /bin/sh /bin/sh +COPY --from=runtime /usr/bin/env /usr/bin/env # serve (static file server) COPY --from=build /usr/local/lib/node_modules/serve /usr/local/lib/node_modules/serve # Required system libraries -COPY --from=build /lib/x86_64-linux-gnu/libdl.so.2 /lib/x86_64-linux-gnu/libdl.so.2 -COPY --from=build /lib/x86_64-linux-gnu/libstdc++.so.6 /lib/x86_64-linux-gnu/libstdc++.so.6 -COPY --from=build /lib/x86_64-linux-gnu/libm.so.6 /lib/x86_64-linux-gnu/libm.so.6 -COPY --from=build /lib/x86_64-linux-gnu/libgcc_s.so.1 /lib/x86_64-linux-gnu/libgcc_s.so.1 -COPY --from=build /lib/x86_64-linux-gnu/libpthread.so.0 /lib/x86_64-linux-gnu/libpthread.so.0 -COPY --from=build /lib/x86_64-linux-gnu/libc.so.6 /lib/x86_64-linux-gnu/libc.so.6 -COPY --from=build /lib64/ld-linux-x86-64.so.2 /lib64/ld-linux-x86-64.so.2 -COPY --from=build /etc/ld.so.cache /etc/ld.so.cache +COPY --from=runtime /lib/x86_64-linux-gnu/libdl.so.2 /lib/x86_64-linux-gnu/libdl.so.2 +COPY --from=runtime /lib/x86_64-linux-gnu/libstdc++.so.6 /lib/x86_64-linux-gnu/libstdc++.so.6 +COPY --from=runtime /lib/x86_64-linux-gnu/libm.so.6 /lib/x86_64-linux-gnu/libm.so.6 +COPY --from=runtime /lib/x86_64-linux-gnu/libgcc_s.so.1 /lib/x86_64-linux-gnu/libgcc_s.so.1 +COPY --from=runtime /lib/x86_64-linux-gnu/libpthread.so.0 /lib/x86_64-linux-gnu/libpthread.so.0 +COPY --from=runtime /lib/x86_64-linux-gnu/libc.so.6 /lib/x86_64-linux-gnu/libc.so.6 +COPY --from=runtime /lib64/ld-linux-x86-64.so.2 /lib64/ld-linux-x86-64.so.2 +COPY --from=runtime /etc/ld.so.cache /etc/ld.so.cache # Distribution configuration -COPY --from=build /etc/os-release /etc/os-release +COPY --from=runtime /etc/os-release /etc/os-release COPY --from=build /allkaraoke/build/client /wwwroot diff --git a/novnc-browser/Dockerfile b/novnc-browser/Dockerfile index 6d69883f..1f11a135 100644 --- a/novnc-browser/Dockerfile +++ b/novnc-browser/Dockerfile @@ -1,35 +1,77 @@ -FROM ubuntu:22.04 +# The rootfs is a full Ubuntu desktop, so the main package install must run +# under the target architecture (emulated on ARM hosts): package maintainer +# scripts execute x86_64 binaries. Everything that does not need target +# execution is kept out of that stage: noVNC/websockify are cloned natively +# (HTML/JS/Python only), and the Mozilla PPA is configured statically with a +# BuildKit-fetched key instead of add-apt-repository, which would drag the +# whole software-properties/python stack through the emulated build. +FROM --platform=$BUILDPLATFORM ubuntu:22.04 AS novnc-src + +RUN apt-get update && \ + apt-get -y install --no-install-recommends git ca-certificates && \ + git clone --depth 1 --branch v1.5.0 https://github.com/novnc/noVNC.git /opt/noVNC && \ + git clone --depth 1 --branch v0.12.0 https://github.com/novnc/websockify /opt/noVNC/utils/websockify && \ + ln -s /opt/noVNC/vnc.html /opt/noVNC/index.html + +FROM --platform=linux/x86_64 ubuntu:22.04 ENV DEBIAN_FRONTEND=noninteractive ENV DEBIAN_PRIORITY=high +# dpkg fsyncs after unpacking each file; under QEMU those syscalls dominate the +# emulated install time. This layer is ephemeral — only its installed files end +# up in the final rootfs — so disabling the fsyncs is safe and a sizeable win. +RUN echo 'force-unsafe-io' > /etc/dpkg/dpkg.cfg.d/02speedup + +# Mozilla team PPA public signing key, vendored from +# https://api.launchpad.net/devel/~mozillateam/+archive/ubuntu/ppa?ws.op=getSigningKeyData +# (fingerprint 738BEB9321D1AAEC13EA9391AEBDF4819BE21867). If the PPA ever +# rotates its key, apt fails signature verification until this file is +# refreshed from that endpoint. +COPY --chmod=644 mozillateam-ppa.asc /etc/apt/keyrings/mozillateam-ppa.asc + +# ca-certificates first (from the plain-http archive): the PPA is served +# over https, and without the CA store apt-get update skips it silently. RUN apt-get update && \ - apt-get -y upgrade && \ - apt-get -y install \ + apt-get -y install --no-install-recommends ca-certificates && \ + echo "deb [signed-by=/etc/apt/keyrings/mozillateam-ppa.asc] https://ppa.launchpadcontent.net/mozillateam/ppa/ubuntu jammy main" \ + > /etc/apt/sources.list.d/mozillateam-ppa.list && \ + apt-get update && \ + # --no-install-recommends keeps the emulated install lean: recommends for + # the desktop packages (docs, help, icon themes, extra fonts, GNOME bits) + # each drag in their own postinst/triggers under QEMU. Everything actually + # needed at runtime is listed explicitly below, and firefox-esr is folded + # into this one transaction so dpkg triggers run once instead of twice. + apt-get -y install --no-install-recommends \ + # Core desktop: virtual X server, window manager, panel, VNC server xvfb \ - xterm \ - xdotool \ - scrot \ - imagemagick \ mutter \ - x11vnc \ tint2 \ - git \ + x11vnc \ + # Invoked by image/*.sh: xdpyinfo (x11-utils) gates Xvfb readiness, + # xdotool waits on windows, netstat (net-tools) checks the VNC port + x11-utils \ + xdotool \ net-tools \ + scrot \ + # Demo apps wired into the tint2 taskbar launchers + xterm \ gedit \ - x11-apps \ xpaint \ - software-properties-common + # Previously satisfied by recommends, now required explicitly: a session + # bus for mutter/GTK apps, and base fonts so text isn't rendered as tofu + dbus-x11 \ + fonts-dejavu-core \ + fonts-liberation \ + # The browser this example exists to demonstrate + firefox-esr \ + # runs websockify; previously came in via software-properties-common + python3 \ + && \ + rm -rf /var/lib/apt/lists/* -# Install Firefox ESR -RUN add-apt-repository ppa:mozillateam/ppa && \ - apt-get install -y --no-install-recommends \ - firefox-esr - -# Install noVNC -RUN git clone --branch v1.5.0 https://github.com/novnc/noVNC.git /opt/noVNC && \ - git clone --branch v0.12.0 https://github.com/novnc/websockify /opt/noVNC/utils/websockify && \ - ln -s /opt/noVNC/vnc.html /opt/noVNC/index.html +# Install noVNC (fetched natively above; needs no target toolchain) +COPY --from=novnc-src /opt/noVNC /opt/noVNC COPY ./image / diff --git a/novnc-browser/image/tint2_startup.sh b/novnc-browser/image/tint2_startup.sh index 7fc957a5..d4c7d067 100755 --- a/novnc-browser/image/tint2_startup.sh +++ b/novnc-browser/image/tint2_startup.sh @@ -2,6 +2,19 @@ echo "starting tint2 on display :$DISPLAY_NUM ..." +# Verify the X display is still reachable. If Xvfb came up, passed its +# readiness check, and then exited (e.g. a rootfs artifact that is subtly +# wrong on emulated builds), tint2 would otherwise just print "could not +# open display!" and we would burn the full 30s timeout below with no clue. +if ! xdpyinfo >/dev/null 2>&1; then + echo "ERROR: display :$DISPLAY_NUM is not reachable - Xvfb appears to have exited after startup" >&2 + if [ -f /tmp/xvfb_stderr.log ]; then + echo "Xvfb stderr output:" >&2 + cat /tmp/xvfb_stderr.log >&2 + fi + exit 1 +fi + # Start tint2 and capture its stderr tint2 -c $HOME/.config/tint2/tint2rc 2>/tmp/tint2_stderr.log & diff --git a/novnc-browser/image/xvfb_startup.sh b/novnc-browser/image/xvfb_startup.sh index 45b80c7e..08f46701 100755 --- a/novnc-browser/image/xvfb_startup.sh +++ b/novnc-browser/image/xvfb_startup.sh @@ -34,8 +34,8 @@ if check_xvfb_running; then exit 0 fi -# Start Xvfb -Xvfb $DISPLAY -ac -screen 0 $RES_AND_DEPTH -retro -dpi $DPI -nolisten tcp -nolisten unix & +# Start Xvfb (capture stderr so a later crash is not silent) +Xvfb $DISPLAY -ac -screen 0 $RES_AND_DEPTH -retro -dpi $DPI -nolisten tcp -nolisten unix 2>/tmp/xvfb_stderr.log & XVFB_PID=$! # Wait for Xvfb to start @@ -44,6 +44,8 @@ if wait_for_xvfb; then echo "Xvfb PID: $XVFB_PID" else echo "Xvfb failed to start" + echo "Xvfb stderr output:" >&2 + cat /tmp/xvfb_stderr.log >&2 2>/dev/null || true # kill $XVFB_PID exit 1 fi diff --git a/novnc-browser/mozillateam-ppa.asc b/novnc-browser/mozillateam-ppa.asc new file mode 100644 index 00000000..97d514e5 --- /dev/null +++ b/novnc-browser/mozillateam-ppa.asc @@ -0,0 +1,29 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQINBGYov84BEADSrLhiWvqL3JJ3fTxjCGD4+viIUBS4eLSc7+Q7SyHm/wWfYNwT +EqEvMMM9brWQyC7xyE2JBlVk5/yYHkAQz3f8rbkv6ge3J8Z7G4ZwHziI45xJKJ0M +9SgJH24WlGxmbbFfK4SGFNlg9x1Z0m5liU3dUSfhvTQdmBNqwRCAjJLZSiS03IA0 +56V9r3ACejwpNiXzOnTsALZC2viszGiI854kqhUhFIJ/cnWKSbAcg6cy3ZAsne6K +vxJVPsdEl12gxU6zENZ/4a4DV1HkxIHtpbh1qub1lhpGR41ZBXv+SQhwuMLFSNeu +UjAAClC/g1pJ0gzI0ko1vcQFv+Q486jYY/kv+k4szzcB++nLILmYmgzOH0NEqT57 +XtdiBWhlb6oNfF/nYZAaToBU/QjtWXq3YImG2NiCUrCj9zAKHdGUsBU0FxN7HkVB +B8aF0VYwB0I2LRO4Af6Ry1cqMyCQnw3FVh0xw7Vz4gQ57acUYeAJpT68q8E2XcUx +riEP65/MBPoFlANLVMSrnsePEXmVzdysmXKnFVefeQ4E3dIDufXUIhrfmL1pMdTG +anhmDEjY7I3pQQQIaLpnNhhSDZKDSk9C/Ax/8gEUgnnmd6BwZxh8Q7oDXcm2tyeu +n2m9wCZI/eJI9P9G8ON8AkKvG4xFR+eqhowwzu7TLDr3feliG+UN+mJ8jwARAQAB +tB5MYXVuY2hwYWQgUFBBIGZvciBNb3ppbGxhIFRlYW2JAk4EEwEKADgWIQRzi+uT +IdGq7BPqk5GuvfSBm+IYZwUCZii/zgIbAwULCQgHAgYVCgkICwIEFgIDAQIeAQIX +gAAKCRCuvfSBm+IYZ38/D/46eEIyG7Gb65sxt3QnlIN0+90kUjz83QpCnIyALZDc +H2wPYBCMbyJFMG+rqVE8Yoh6WF0Rqy76LG+Y/xzO9eKIJGxVcSU75ifoq/M7pI1p +aiqA9T8QcFBmo83FFoPvnid67aqg/tFsHl+YF9rUxMZndGRE9Hk96lkH1Y2wHMEs +mAa582RELVEDDD2ellOPmQr69fRPa5IdJHkXjqGtoNQy5hAp49ofMLmeQ82d2OA+ +kpzgiuSw8Nh1VrMZludcUArSQDCHoXuiPG/7Wn9Vy6fvKkTQK3mCW8i5HgCa0qxe +vOKlDMz4virEEADMBs79iIyM6w1xm8JOD4734sgii2MPcQgmAlbu5LyBM5FfuO0u +rTMvZM0btSWQX3nIsxQ3far9MJvUT4nebhTo59cED+1EjkD14mReTHwtWt1aye/b +I8Rvor15RFiB8Ku6c41YmNKarSCzJDs4VEfsos4oMieEqA98J4ZOX67IT++ortcB +uXmDJgvzGWEeyVOMoc/4oDJHNQjJg9XRGy8b/J3AVhk2BE/CD4lKhX3hWGbufrQz +E8ENWuT4m3igQnBmOsrGlBPYIOKZvczQxri01vcKY95dKXb1jtnR9yR+JKgEP388 +1B/8dEohynhMnzEqR9TIMEEy9Y8RKZ+Jiy+/Lg2XGrChiLsouUetfMQww6BTK+++ +pw== +=tIux +-----END PGP PUBLIC KEY BLOCK----- diff --git a/openclaw/Dockerfile b/openclaw/Dockerfile index 5c56ef03..60d4afba 100644 --- a/openclaw/Dockerfile +++ b/openclaw/Dockerfile @@ -1,4 +1,4 @@ -FROM node:24-bookworm +FROM --platform=linux/x86_64 node:24-bookworm # Install sshd and other useful utilities RUN apt-get update -y \ diff --git a/opentelemetry-collector/Dockerfile b/opentelemetry-collector/Dockerfile index 1a983710..d6d79418 100644 --- a/opentelemetry-collector/Dockerfile +++ b/opentelemetry-collector/Dockerfile @@ -1,4 +1,4 @@ -FROM ghcr.io/open-telemetry/opentelemetry-collector-releases/opentelemetry-collector-contrib:0.95.0 AS upstream +FROM --platform=linux/x86_64 ghcr.io/open-telemetry/opentelemetry-collector-releases/opentelemetry-collector-contrib:0.95.0 AS upstream FROM scratch diff --git a/phoenix-postgres/phoenix/Dockerfile b/phoenix-postgres/phoenix/Dockerfile index 97fc05b3..977db986 100644 --- a/phoenix-postgres/phoenix/Dockerfile +++ b/phoenix-postgres/phoenix/Dockerfile @@ -11,6 +11,12 @@ # - https://pkgs.org/ - resource for finding needed packages # - Ex: docker.io/hexpm/elixir:1.17.3-erlang-27.1.2-debian-bookworm-20241202-slim # +# Compiled .beam files are architecture-independent and none of the app's +# deps ship NIFs, so the whole mix build runs natively on the build host. +# The only x86_64-specific pieces of the release are ERTS and the native +# parts of OTP's own applications (crypto NIF etc.); those are swapped in +# from the x86_64 variant of the same builder image, which is only pulled, +# never run. ARG ELIXIR_VERSION=1.17.3 ARG OTP_VERSION=27.1.2 ARG DEBIAN_VERSION=bookworm-20241202-slim @@ -18,11 +24,15 @@ ARG DEBIAN_VERSION=bookworm-20241202-slim ARG BUILDER_IMAGE="docker.io/hexpm/elixir:${ELIXIR_VERSION}-erlang-${OTP_VERSION}-debian-${DEBIAN_VERSION}" ARG RUNNER_IMAGE="docker.io/debian:${DEBIAN_VERSION}" -FROM ${BUILDER_IMAGE} AS builder +# x86_64 ERTS/OTP donor; no code from this stage is ever executed. +FROM --platform=linux/x86_64 ${BUILDER_IMAGE} AS otp-x86 -# install build dependencies +FROM --platform=$BUILDPLATFORM ${BUILDER_IMAGE} AS builder + +# install build dependencies ("file" is used to verify the assembled +# release contains only x86_64 native code) RUN apt-get update \ - && apt-get install -y --no-install-recommends build-essential git \ + && apt-get install -y --no-install-recommends build-essential git file \ && rm -rf /var/lib/apt/lists/* # prepare build dir @@ -46,6 +56,8 @@ RUN mkdir config COPY config/config.exs config/${MIX_ENV}.exs config/ RUN mix deps.compile +# esbuild/tailwind download binaries for the build host and only run at +# build time; the assets they emit are architecture-independent. RUN mix assets.setup COPY priv priv @@ -66,18 +78,78 @@ COPY config/runtime.exs config/ COPY rel rel RUN mix release +# Swap the release's ERTS and OTP applications for their x86_64 +# counterparts (same OTP build, so the .beam files are identical and only +# the native binaries/NIFs differ). mix release rewrites erts' bin/erl +# into a location-independent script, which must survive the swap. +# Anything not present in the donor OTP is a hex dep or the app itself, +# i.e. pure .beam code; the final `file` sweep proves no foreign-arch ELF +# slipped through (e.g. if a NIF dependency is ever added). +COPY --from=otp-x86 /usr/local/lib/erlang /otp-x86 + +RUN set -e; \ + rel="/app/_build/${MIX_ENV}/rel/myapp"; \ + ertsdir=$(cd "$rel" && echo erts-*); \ + test -d "/otp-x86/$ertsdir"; \ + cp "$rel/$ertsdir/bin/erl" /tmp/dyn-erl; \ + rm -rf "$rel/${ertsdir:?}"; \ + cp -a "/otp-x86/$ertsdir" "$rel/"; \ + cp /tmp/dyn-erl "$rel/$ertsdir/bin/erl"; \ + for d in "$rel"/lib/*; do \ + n=$(basename "$d"); \ + if [ -d "/otp-x86/lib/$n" ]; then \ + rm -rf "$d"; \ + cp -a "/otp-x86/lib/$n" "$rel/lib/"; \ + fi; \ + done; \ + if find "$rel" -type f -exec file {} + | grep ELF | grep -v 'x86-64'; then \ + echo "non-x86_64 ELF files remain in release" >&2; \ + exit 1; \ + fi + +# Runtime pieces for the final image, assembled natively: amd64 libraries +# are unpacked from prebuilt debs (no maintainer scripts run), the en_US +# locale is compiled with localedef (its output is architecture- +# independent), and the CA bundle is plain data. +FROM --platform=$BUILDPLATFORM debian:bookworm AS runtime-deps + +RUN set -xe; \ + dpkg --add-architecture amd64; \ + apt-get -yqq update; \ + apt-get -yqq install --no-install-recommends locales ca-certificates; \ + localedef -i en_US -c -f UTF-8 -A /usr/share/locale/locale.alias en_US.UTF-8; \ + mkdir -p /rootfs/usr/lib/locale /rootfs/etc/ssl/certs /rootfs/usr/share/ca-certificates; \ + cp /usr/lib/locale/locale-archive /rootfs/usr/lib/locale/; \ + cp -a /etc/ssl/certs/. /rootfs/etc/ssl/certs/; \ + cp -a /usr/share/ca-certificates/. /rootfs/usr/share/ca-certificates/; \ + cp /etc/ca-certificates.conf /rootfs/etc/; \ + mkdir /debs; \ + cd /debs; \ + apt-get -yqq download \ + libstdc++6:amd64 \ + libssl3:amd64 \ + libncursesw6:amd64 \ + libncurses6:amd64; \ + for deb in *.deb; do dpkg-deb -x "$deb" /rootfs; done; \ + rm -rf /rootfs/usr/share/doc /rootfs/usr/share/lintian; \ + # some debs (ncurses) still ship pre-usr-merge paths; the final image's + # /lib is a symlink to usr/lib, and COPY cannot overlay a directory + # onto a symlink, so fold the tree into the merged layout + for d in lib lib64 bin sbin; do \ + if [ -d "/rootfs/$d" ] && [ ! -L "/rootfs/$d" ]; then \ + mkdir -p "/rootfs/usr/$d"; \ + cp -a "/rootfs/$d/." "/rootfs/usr/$d/"; \ + rm -rf "/rootfs/${d:?}"; \ + fi; \ + done + # start a new build stage so that the final image will only contain # the compiled release and other runtime necessities -FROM ${RUNNER_IMAGE} AS final - -RUN apt-get update \ - && apt-get install -y --no-install-recommends libstdc++6 openssl libncurses5 locales ca-certificates \ - && rm -rf /var/lib/apt/lists/* +FROM --platform=linux/x86_64 ${RUNNER_IMAGE} AS final -# Set the locale -RUN sed -i '/en_US.UTF-8/s/^# //g' /etc/locale.gen \ - && locale-gen +COPY --from=runtime-deps /rootfs/ / +# Set the locale (compiled into the locale-archive copied above) ENV LANG=en_US.UTF-8 ENV LANGUAGE=en_US:en ENV LC_ALL=en_US.UTF-8 diff --git a/phoenix-postgres/postgres/Dockerfile b/phoenix-postgres/postgres/Dockerfile index fdb73804..3ffee372 100644 --- a/phoenix-postgres/postgres/Dockerfile +++ b/phoenix-postgres/postgres/Dockerfile @@ -1,14 +1,34 @@ -FROM postgres:16.2-alpine AS base +# The final image is the prebuilt (x86_64) official postgres image, which is +# only pulled, never run during the build. The scale-to-zero extension is a +# single-file module compiled natively with the amd64 cross toolchain +# against the server headers shipped inside that image, so no build step +# executes x86_64 code on an ARM host. +FROM --platform=linux/x86_64 postgres:16.2-alpine AS base + +FROM --platform=$BUILDPLATFORM debian:bookworm AS ext-build + +RUN set -xe; \ + apt-get -yqq update; \ + apt-get -yqq install --no-install-recommends crossbuild-essential-amd64 unzip -FROM base AS ext-build -RUN apk add --no-cache gcc make musl-dev llvm15-dev clang15 unzip ADD https://github.com/kraftcloud/pg_ukc_scaletozero/archive/refs/heads/stable.zip /tmp/src.zip + RUN mkdir /src && \ unzip /tmp/src.zip -d /tmp/extract && \ mv /tmp/extract/pg_ukc_scaletozero-*/* /src + +COPY --from=base /usr/local/include/postgresql/server /pg-include/server + +# The target server is musl-libc while the cross toolchain is glibc, so the +# module is linked -nostdlib: no glibc NEEDED entry is recorded (Alpine +# could not satisfy it), and its libc symbol references (open, errno, ...) +# resolve from the server process at load time. This replaces the PGXS +# build, which would run pg_config, an x86_64 binary; PGXS's bitcode/LLVM +# output was never shipped anyway. RUN cd /src && \ - make && \ - make install DESTDIR=/out + x86_64-linux-gnu-gcc -shared -fPIC -nostdlib -fno-stack-protector -s -Os \ + -I/pg-include/server \ + -o pg_ukc_scaletozero.so pg_ukc_scaletozero.c FROM base -COPY --from=ext-build /out/usr/local/lib/postgresql/pg_ukc_scaletozero.so /usr/local/lib/postgresql/pg_ukc_scaletozero.so +COPY --from=ext-build /src/pg_ukc_scaletozero.so /usr/local/lib/postgresql/pg_ukc_scaletozero.so diff --git a/postgres/Dockerfile b/postgres/Dockerfile index 0b3284ef..8063ff76 100644 --- a/postgres/Dockerfile +++ b/postgres/Dockerfile @@ -1,45 +1,59 @@ -FROM alpine:3.20 AS base +# Postgres must be compiled from source (allow-root.patch rules out prebuilt +# binaries), but no build step needs to *execute* x86_64 code, so every stage +# runs natively on the build host: the compiler is Debian's packaged amd64 +# cross toolchain (on x86_64 hosts the same `x86_64-linux-gnu-gcc` name is +# provided by native gcc), and the libraries Postgres builds against and +# ships with are prebuilt amd64 Debian packages unpacked by native dpkg via +# multiarch. ### -### Build Postgres +### Cross-build Postgres ### -FROM base AS pg-build +FROM --platform=$BUILDPLATFORM debian:bookworm AS pg-build ENV PG_VERSION=16.4 -RUN apk add --no-cache --virtual .build-deps \ - coreutils \ - g++ \ - gcc \ +RUN set -xe; \ + dpkg --add-architecture amd64; \ + apt-get -yqq update; \ + apt-get -yqq install --no-install-recommends \ + # x86_64 cross toolchain; resolves to native gcc on x86_64 hosts + crossbuild-essential-amd64 \ + libc6-dev:amd64 \ + # prebuilt x86_64 libraries Postgres links against + libicu-dev:amd64 \ + libxml2-dev:amd64 \ + libxslt1-dev:amd64 \ + liblz4-dev:amd64 \ + uuid-dev:amd64 \ + libedit-dev:amd64 \ + # build tools, all run natively make \ - tar \ + pkg-config \ + bison \ + flex \ + patch \ bzip2 \ - libxml2-dev \ - libxslt-dev \ - libedit-dev \ - zlib-dev \ - lz4-dev \ - zstd-dev \ - e2fsprogs-dev \ - linux-headers \ unzip \ - git \ - icu-dev \ ; \ - mkdir -p /usr/src; \ - mv /lib/libssl.so.3 /lib/libssl.so.3.orig + mkdir -p /usr/src ADD https://ftp.postgresql.org/pub/source/v$PG_VERSION/postgresql-$PG_VERSION.tar.bz2 /tmp/postgresql-$PG_VERSION.tar.bz2 -RUN tar -xvjf /tmp/postgresql-$PG_VERSION.tar.bz2 -C /usr/src +RUN tar -xjf /tmp/postgresql-$PG_VERSION.tar.bz2 -C /usr/src ADD allow-root.patch /usr/src/postgresql-$PG_VERSION/ +# --host makes this a cross build on ARM hosts; on x86_64 hosts it is the +# native triplet and configure behaves as before. PKG_CONFIG_LIBDIR points +# pkg-config at the amd64 multiarch .pc files so ICU resolves for the +# target, not the build host. RUN set -e; \ cd /usr/src/postgresql-$PG_VERSION; \ - git apply allow-root.patch; \ + patch -p1 < allow-root.patch; \ export CFLAGS="-s -Os -Wl,--strip-all"; \ - export CXXFLAGS="-s -Os -Wl,--strip-all"; \ + export PKG_CONFIG_LIBDIR=/usr/lib/x86_64-linux-gnu/pkgconfig:/usr/lib/pkgconfig:/usr/share/pkgconfig; \ ./configure \ + --host=x86_64-linux-gnu \ --enable-option-checking=fatal \ --enable-integer-datetimes \ --enable-thread-safety \ @@ -48,8 +62,6 @@ RUN set -e; \ --with-pgport=5432 \ --with-system-tzdata=/usr/share/zoneinfo \ --prefix=/usr/local \ - --with-includes=/usr/local/include \ - --with-libraries=/usr/local/lib \ --with-icu \ --with-libxml \ --with-libxslt \ @@ -57,88 +69,120 @@ RUN set -e; \ --with-lz4 \ ; \ make -j "$(nproc)" world-bin; \ - make install-world-bin DESTDIR=/usr/src/postgresql/dist; \ - make install + make install-world-bin DESTDIR=/usr/src/postgresql/dist -# Build pg_ukc_scaletozero plugin +# Build pg_ukc_scaletozero plugin. It is a single-file PGXS module (one +# shared object, no SQL scripts), compiled directly against the Postgres +# build tree because PGXS would invoke pg_config, an x86_64 binary. ADD https://github.com/unikraft-cloud/pg_ukc_scaletozero/archive/refs/heads/stable.zip /tmp/pg_ukc_scaletozero.zip RUN mkdir /usr/src/pg_ukc_scaletozero; \ unzip /tmp/pg_ukc_scaletozero.zip -d /tmp/extract; \ mv /tmp/extract/pg_ukc_scaletozero-*/* /usr/src/pg_ukc_scaletozero -RUN cd /usr/src/pg_ukc_scaletozero; \ - export CFLAGS="-s -Os -Wl,--strip-all"; \ - make; \ - make install DESTDIR=/usr/src/postgresql/dist +RUN set -e; \ + cd /usr/src/pg_ukc_scaletozero; \ + x86_64-linux-gnu-gcc -s -Os -fPIC -shared \ + -I/usr/src/postgresql-$PG_VERSION/src/include \ + -o pg_ukc_scaletozero.so pg_ukc_scaletozero.c; \ + install -D -m 755 pg_ukc_scaletozero.so \ + /usr/src/postgresql/dist/usr/local/lib/postgresql/pg_ukc_scaletozero.so ## -## Retrieve dependencies +## Retrieve dependencies as prebuilt x86_64 packages ## -FROM base AS base-deps - -RUN apk add --no-cache \ - libuuid \ - libxml2 \ - libxslt \ - lz4-libs \ - tzdata \ - libgcc \ - libstdc++ \ - icu-libs \ - bash \ - libedit \ - openssl \ - krb5-libs \ - libldap \ - cyrus-sasl \ - libcom_err \ - keyutils-libs \ - ; +FROM --platform=$BUILDPLATFORM debian:bookworm AS runtime-deps + +# dpkg-deb -x unpacks the amd64 debs without running any of their +# (x86_64) maintainer scripts. +RUN set -xe; \ + dpkg --add-architecture amd64; \ + apt-get -yqq update; \ + apt-get -yqq install --no-install-recommends locales; \ + mkdir /debs; \ + cd /debs; \ + apt-get -yqq download \ + libc6:amd64 \ + libicu72:amd64 \ + libxml2:amd64 \ + libxslt1.1:amd64 \ + liblz4-1:amd64 \ + libuuid1:amd64 \ + libedit2:amd64 \ + libbsd0:amd64 \ + libmd0:amd64 \ + libtinfo6:amd64 \ + zlib1g:amd64 \ + liblzma5:amd64 \ + libstdc++6:amd64 \ + libgcc-s1:amd64 \ + bash:amd64 \ + tzdata; \ + for deb in *.deb; do dpkg-deb -x "$deb" /rootfs; done + +# wrapper.sh runs initdb with LANG=en_US.utf8, which glibc only accepts if +# the compiled locale exists (musl faked it; glibc does not). Compiled +# glibc locales are architecture-independent, so the native localedef +# output is valid for the x86_64 rootfs. +RUN set -xe; \ + localedef -i en_US -c -f UTF-8 -A /usr/share/locale/locale.alias en_US.UTF-8; \ + mkdir -p /rootfs/usr/lib/locale; \ + cp /usr/lib/locale/locale-archive /rootfs/usr/lib/locale/ ## ## Build root file system ## -FROM busybox +# busybox:musl is a statically linked musl build, so its applets (including +# the /usr/bin/env that runs wrapper.sh's shebang, plus mkdir/chmod/ls/cat) +# carry no glibc dependency. The default busybox:latest is glibc and ships +# its own libc.so.6; the glibc COPYs below overwrite that path with bookworm's +# older libc (2.36), which would leave every busybox applet demanding a +# GLIBC_2.38 the bundled libc no longer provides. Keeping busybox on musl +# lets it coexist with the glibc that Postgres and bash are bundled against. +FROM --platform=linux/x86_64 busybox:musl + +# Dynamic loader at the ELF interpreter path. /lib/x86_64-linux-gnu and +# /usr/lib/x86_64-linux-gnu are its compiled-in search paths, so no +# ld.so.cache is needed. The deb's own /lib64 entry is an absolute symlink +# that dangles inside /rootfs, so copy the real file. +COPY --from=runtime-deps /rootfs/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 /lib64/ld-linux-x86-64.so.2 # Libraries -COPY --from=base-deps /lib/ld-musl-x86_64.so.1 /lib/ -COPY --from=base-deps /lib/libz.so.1 /lib/ -COPY --from=base-deps /lib/libuuid.so.1 /lib/ -COPY --from=base-deps /usr/lib/libgcc_s.so.1 /usr/lib/ -COPY --from=base-deps /usr/lib/libstdc++.so.6 /usr/lib/ -COPY --from=base-deps /usr/lib/liblz4.so.1 /usr/lib/ -COPY --from=base-deps /usr/lib/liblzma.so.5 /usr/lib/ -COPY --from=base-deps /usr/lib/libxml2.so.2 /usr/lib/ -COPY --from=base-deps /usr/lib/libxslt.so.1 /usr/lib/ -COPY --from=base-deps /usr/lib/libncursesw.so.6 /usr/lib/ -COPY --from=base-deps /usr/lib/libreadline.so.8 /usr/lib/ -COPY --from=base-deps /usr/lib/libedit.so.0 /usr/lib/ -COPY --from=base-deps /lib/libssl.so.3 /lib/ -COPY --from=base-deps /lib/libcrypto.so.3 /lib/ -COPY --from=base-deps /usr/lib/libgssapi_krb5.so.2 /usr/lib/ -COPY --from=base-deps /usr/lib/libkrb5.so.3 /usr/lib/ -COPY --from=base-deps /usr/lib/libk5crypto.so.3 /usr/lib/ -COPY --from=base-deps /usr/lib/libkrb5support.so.0 /usr/lib/ -COPY --from=base-deps /usr/lib/libldap.so.2 /usr/lib/ -COPY --from=base-deps /usr/lib/liblber.so.2 /usr/lib/ -COPY --from=base-deps /usr/lib/libsasl2.so.3 /usr/lib/ -COPY --from=base-deps /lib/libcom_err.so.2 /lib/ -COPY --from=base-deps /usr/lib/libkeyutils.so.1 /usr/lib/ -COPY --from=base-deps /usr/lib/libicudata.so.74 /usr/lib/ -COPY --from=base-deps /usr/lib/libicuuc.so.74 /usr/lib/ -COPY --from=base-deps /usr/lib/libicui18n.so.74 /usr/lib/ +COPY --from=runtime-deps /rootfs/lib/x86_64-linux-gnu/libc.so.6 \ + /rootfs/lib/x86_64-linux-gnu/libm.so.6 \ + /rootfs/lib/x86_64-linux-gnu/libgcc_s.so.1 \ + /rootfs/lib/x86_64-linux-gnu/libz.so.1 \ + /rootfs/lib/x86_64-linux-gnu/liblzma.so.5 \ + /rootfs/lib/x86_64-linux-gnu/libtinfo.so.6 \ + /lib/x86_64-linux-gnu/ + +COPY --from=runtime-deps /rootfs/usr/lib/x86_64-linux-gnu/libicudata.so.72 \ + /rootfs/usr/lib/x86_64-linux-gnu/libicuuc.so.72 \ + /rootfs/usr/lib/x86_64-linux-gnu/libicui18n.so.72 \ + /rootfs/usr/lib/x86_64-linux-gnu/libstdc++.so.6 \ + /rootfs/usr/lib/x86_64-linux-gnu/libxml2.so.2 \ + /rootfs/usr/lib/x86_64-linux-gnu/libxslt.so.1 \ + /rootfs/usr/lib/x86_64-linux-gnu/liblz4.so.1 \ + /rootfs/usr/lib/x86_64-linux-gnu/libuuid.so.1 \ + /rootfs/usr/lib/x86_64-linux-gnu/libedit.so.2 \ + /rootfs/usr/lib/x86_64-linux-gnu/libbsd.so.0 \ + /rootfs/usr/lib/x86_64-linux-gnu/libmd.so.0 \ + /usr/lib/x86_64-linux-gnu/ # Data files -COPY --from=base-deps /usr/share/icu /usr/share/icu -COPY --from=base-deps /usr/share/zoneinfo /usr/share/zoneinfo +COPY --from=runtime-deps /rootfs/usr/share/zoneinfo /usr/share/zoneinfo +COPY --from=runtime-deps /rootfs/usr/lib/locale /usr/lib/locale -# Bash -COPY --from=base-deps /bin/bash /bin/bash +# Bash, for wrapper.sh +COPY --from=runtime-deps /rootfs/bin/bash /bin/bash # PostgreSQL binaries COPY --from=pg-build /usr/src/postgresql/dist/ / +# glibc's loader does not search /usr/local/lib (musl's did), so libpq must +# also live in a searched directory for psql and friends. +COPY --from=pg-build /usr/src/postgresql/dist/usr/local/lib/libpq.so.5 /usr/lib/x86_64-linux-gnu/ + # Configuration and runtime files COPY fs/ / diff --git a/postgres/test_postgres.py b/postgres/test_postgres.py index e9954162..b15b0978 100644 --- a/postgres/test_postgres.py +++ b/postgres/test_postgres.py @@ -14,6 +14,7 @@ import psycopg2 +from _testlib.readiness import retry_until_ready from _testlib.unikraft import extract_instance_fqdn, extract_instance_name # The README deploys with POSTGRES_PASSWORD=unikraft. @@ -24,15 +25,26 @@ def _connect(host: str): - """Open a psycopg2 connection to the instance over TLS.""" - return psycopg2.connect( - host=host, - port=PG_PORT, - user=PG_USER, - password=PG_PASSWORD, - dbname=PG_DATABASE, - sslmode="require", - connect_timeout=30, + """Open a psycopg2 connection to the instance over TLS. + + Retries while the server finishes starting up. ``wait_instance`` only + proves the unikernel booted; with no volume attached, postgres still has + to run ``initdb`` on every boot, and answers connections with "FATAL: the + database system is starting up" until that completes. ``connect_timeout`` + does not cover this — the connection succeeds and the server rejects it. + """ + return retry_until_ready( + lambda: psycopg2.connect( + host=host, + port=PG_PORT, + user=PG_USER, + password=PG_PASSWORD, + dbname=PG_DATABASE, + sslmode="require", + connect_timeout=30, + ), + exceptions=psycopg2.OperationalError, + description="postgres", ) diff --git a/python-playwright-chromium/Dockerfile b/python-playwright-chromium/Dockerfile index 6bd723fb..10edcd53 100644 --- a/python-playwright-chromium/Dockerfile +++ b/python-playwright-chromium/Dockerfile @@ -1,215 +1,272 @@ -FROM debian:bookworm AS build +# Everything in the final rootfs is a prebuilt x86_64 artifact, so each +# stage fetches and unpacks with native tools regardless of build host: +# Debian packages are unpacked by native dpkg via multiarch, Python +# wheels are downloaded for x86_64 with pip --platform, and Playwright's +# headless Chromium build is downloaded straight from its CDN. + +# x86_64 Debian packages, unpacked natively via dpkg multiarch. +FROM --platform=$BUILDPLATFORM debian:bookworm AS debs + +RUN set -xe; \ + dpkg --add-architecture amd64; \ + apt-get -yqq update; \ + apt-get -yqq install --no-install-recommends \ + # Chromium runtime dependencies + libasound2:amd64 \ + libatk-bridge2.0-0:amd64 \ + libatk1.0-0:amd64 \ + libatspi2.0-0:amd64 \ + libcairo2:amd64 \ + libcups2:amd64 \ + libdbus-1-3:amd64 \ + libdrm2:amd64 \ + libfontconfig1:amd64 \ + libgbm1:amd64 \ + libglib2.0-0:amd64 \ + libnspr4:amd64 \ + libnss3:amd64 \ + libpango-1.0-0:amd64 \ + libstdc++6:amd64 \ + libudev1:amd64 \ + libx11-6:amd64 \ + libxcb1:amd64 \ + libxcomposite1:amd64 \ + libxdamage1:amd64 \ + libxext6:amd64 \ + libxfixes3:amd64 \ + libxi6:amd64 \ + libxkbcommon0:amd64 \ + libxrandr2:amd64 \ + fonts-dejavu-core \ + ca-certificates \ + # Python runtime, and ps which Playwright uses at runtime + python3:amd64 \ + procps:amd64; \ + # python-is-python3 is arch:all, so apt resolves its python3 dependency + # against the build host's native arch, which cannot coexist with + # python3:amd64; it only ships the /usr/bin/python symlink, so create + # that directly + ln -s python3 /usr/bin/python; \ + # dash:amd64 conflicts with the build host's essential dash, so + # unpack it manually to provide the rootfs /bin/sh + cd /tmp; \ + apt-get -q download dash:amd64; \ + dpkg-deb -x dash_*.deb /dash-amd64; \ + mkdir /home/tmp + +# x86_64 Python wheels and Chromium, fetched by native python/curl. +# debian:bookworm has the same python3.11 the rootfs gets from apt. +FROM --platform=$BUILDPLATFORM debian:bookworm AS pyapps RUN set -xe; \ apt-get -yqq update; \ - apt-get -yqq install \ - libcups2 \ - libnss3 \ - libatk1.0-0 \ - libnspr4 \ - libpango1.0-0 \ - libasound2 \ - libatspi2.0-0 \ - libxdamage1 \ - libatk-bridge2.0-0 \ - libxkbcommon0 \ - libdrm2 \ - libxcomposite1 \ - libxfixes3 \ - libxrandr2 \ - libgbm1; \ - apt-get -yqq install \ + apt-get -yqq install --no-install-recommends \ python3 \ - python-is-python3 \ python3-pip \ - python3-venv \ - strace \ - ; + ca-certificates \ + curl \ + unzip WORKDIR /app COPY ./requirements.txt . + +# A venv would buy nothing in a single-workload rootfs: packages go +# straight into the path Debian's python already searches, and the CLI +# scripts pip generates get the /usr/bin/python3 shebang that matches +# the interpreter shipped in the final image. RUN set -xe; \ - python -m venv .venv; \ - . .venv/bin/activate; \ - pip install -r requirements.txt; \ - pip install "fastapi[standard]"; \ - playwright install chromium \ - ; + pip3 install --break-system-packages --no-cache-dir --only-binary=:all: \ + --platform manylinux1_x86_64 \ + --platform manylinux2010_x86_64 \ + --platform manylinux2014_x86_64 \ + --platform manylinux_2_17_x86_64 \ + --platform manylinux_2_24_x86_64 \ + --platform manylinux_2_28_x86_64 \ + --platform manylinux_2_31_x86_64 \ + --platform manylinux_2_34_x86_64 \ + --platform manylinux_2_35_x86_64 \ + --python-version 311 --implementation cp --abi cp311 \ + --target /x86-dist-packages \ + -r requirements.txt; \ + mkdir /x86-bin; \ + mv /x86-dist-packages/bin/* /x86-bin/; \ + # bytecode is arch-independent; precompile since the rootfs is read-only + python3 -m compileall -q /x86-dist-packages || true -RUN mkdir /home/tmp +# main.py only launches headless Chromium, so only the headless-shell +# build is installed. URL layout matches what `playwright install +# --dry-run chromium-headless-shell` prints for the pinned version. +RUN set -xe; \ + BROWSERS=/x86-dist-packages/playwright/driver/package/browsers.json; \ + JSONQ="import json; b = [b for b in json.load(open('$BROWSERS'))['browsers'] if b['name'] == 'chromium-headless-shell'][0]"; \ + REVISION=$(python3 -c "$JSONQ; print(b['revision'])"); \ + VERSION=$(python3 -c "$JSONQ; print(b['browserVersion'])"); \ + DEST=/root/.cache/ms-playwright/chromium_headless_shell-$REVISION; \ + mkdir -p $DEST; \ + curl -fsSL -o /tmp/headless-shell.zip \ + "https://cdn.playwright.dev/builds/cft/$VERSION/linux64/chrome-headless-shell-linux64.zip"; \ + unzip -q /tmp/headless-shell.zip -d $DEST; \ + rm /tmp/headless-shell.zip; \ + touch $DEST/INSTALLATION_COMPLETE FROM scratch # Create required directories. -COPY --from=build /home/tmp /tmp +COPY --from=debs /home/tmp /tmp # Chrome binary -COPY --from=build /root/.cache/ms-playwright /root/.cache/ms-playwright +COPY --from=pyapps /root/.cache/ms-playwright /root/.cache/ms-playwright # Chrome libraries -COPY --from=build /lib/x86_64-linux-gnu/libdl.so.2 \ - /lib/x86_64-linux-gnu/libpthread.so.0 \ - /lib/x86_64-linux-gnu/libglib-2.0.so.0 \ - /lib/x86_64-linux-gnu/libgobject-2.0.so.0 \ - /lib/x86_64-linux-gnu/libnss3.so \ - /lib/x86_64-linux-gnu/libnssutil3.so \ - /lib/x86_64-linux-gnu/libsmime3.so \ - /lib/x86_64-linux-gnu/libnspr4.so \ - /lib/x86_64-linux-gnu/libdbus-1.so.3 \ - /lib/x86_64-linux-gnu/libatk-1.0.so.0 \ - /lib/x86_64-linux-gnu/libatk-bridge-2.0.so.0 \ - /lib/x86_64-linux-gnu/libcups.so.2 \ - /lib/x86_64-linux-gnu/libgio-2.0.so.0 \ - /lib/x86_64-linux-gnu/libdrm.so.2 \ - /lib/x86_64-linux-gnu/libexpat.so.1 \ - /lib/x86_64-linux-gnu/libxcb.so.1 \ - /lib/x86_64-linux-gnu/libxkbcommon.so.0 \ - /lib/x86_64-linux-gnu/libatspi.so.0 \ - /lib/x86_64-linux-gnu/libX11.so.6 \ - /lib/x86_64-linux-gnu/libXcomposite.so.1 \ - /lib/x86_64-linux-gnu/libXdamage.so.1 \ - /lib/x86_64-linux-gnu/libXext.so.6 \ - /lib/x86_64-linux-gnu/libXfixes.so.3 \ - /lib/x86_64-linux-gnu/libXrandr.so.2 \ - /lib/x86_64-linux-gnu/libgbm.so.1 \ - /lib/x86_64-linux-gnu/libpango-1.0.so.0 \ - /lib/x86_64-linux-gnu/libcairo.so.2 \ - /lib/x86_64-linux-gnu/libasound.so.2 \ - /lib/x86_64-linux-gnu/libm.so.6 \ - /lib/x86_64-linux-gnu/libgcc_s.so.1 \ - /lib/x86_64-linux-gnu/libc.so.6 \ - /lib/x86_64-linux-gnu/libpcre2-8.so.0 \ - /lib/x86_64-linux-gnu/libffi.so.8 \ - /lib/x86_64-linux-gnu/libplc4.so \ - /lib/x86_64-linux-gnu/libplds4.so \ - /lib/x86_64-linux-gnu/libsystemd.so.0 \ - /lib/x86_64-linux-gnu/libgssapi_krb5.so.2 \ - /lib/x86_64-linux-gnu/libavahi-common.so.3 \ - /lib/x86_64-linux-gnu/libavahi-client.so.3 \ - /lib/x86_64-linux-gnu/libgnutls.so.30 \ - /lib/x86_64-linux-gnu/libz.so.1 \ - /lib/x86_64-linux-gnu/libgmodule-2.0.so.0 \ - /lib/x86_64-linux-gnu/libmount.so.1 \ - /lib/x86_64-linux-gnu/libselinux.so.1 \ - /lib/x86_64-linux-gnu/libXau.so.6 \ - /lib/x86_64-linux-gnu/libXdmcp.so.6 \ - /lib/x86_64-linux-gnu/libXi.so.6 \ - /lib/x86_64-linux-gnu/libXrender.so.1 \ - /lib/x86_64-linux-gnu/libwayland-server.so.0 \ - /lib/x86_64-linux-gnu/libfribidi.so.0 \ - /lib/x86_64-linux-gnu/libthai.so.0 \ - /lib/x86_64-linux-gnu/libharfbuzz.so.0 \ - /lib/x86_64-linux-gnu/libpixman-1.so.0 \ - /lib/x86_64-linux-gnu/libfontconfig.so.1 \ - /lib/x86_64-linux-gnu/libfreetype.so.6 \ - /lib/x86_64-linux-gnu/libpng16.so.16 \ - /lib/x86_64-linux-gnu/libxcb-shm.so.0 \ - /lib/x86_64-linux-gnu/libxcb-render.so.0 \ - /lib/x86_64-linux-gnu/libcap.so.2 \ - /lib/x86_64-linux-gnu/libgcrypt.so.20 \ - /lib/x86_64-linux-gnu/liblzma.so.5 \ - /lib/x86_64-linux-gnu/libzstd.so.1 \ - /lib/x86_64-linux-gnu/liblz4.so.1 \ - /lib/x86_64-linux-gnu/libkrb5.so.3 \ - /lib/x86_64-linux-gnu/libk5crypto.so.3 \ - /lib/x86_64-linux-gnu/libcom_err.so.2 \ - /lib/x86_64-linux-gnu/libkrb5support.so.0 \ - /lib/x86_64-linux-gnu/libp11-kit.so.0 \ - /lib/x86_64-linux-gnu/libidn2.so.0 \ - /lib/x86_64-linux-gnu/libunistring.so.2 \ - /lib/x86_64-linux-gnu/libtasn1.so.6 \ - /lib/x86_64-linux-gnu/libnettle.so.8 \ - /lib/x86_64-linux-gnu/libhogweed.so.6 \ - /lib/x86_64-linux-gnu/libgmp.so.10 \ - /lib/x86_64-linux-gnu/libblkid.so.1 \ - /lib/x86_64-linux-gnu/libbsd.so.0 \ - /lib/x86_64-linux-gnu/libdatrie.so.1 \ - /lib/x86_64-linux-gnu/libgraphite2.so.3 \ - /lib/x86_64-linux-gnu/libbrotlidec.so.1 \ - /lib/x86_64-linux-gnu/libgpg-error.so.0 \ - /lib/x86_64-linux-gnu/libkeyutils.so.1 \ - /lib/x86_64-linux-gnu/libresolv.so.2 \ - /lib/x86_64-linux-gnu/libmd.so.0 \ - /lib/x86_64-linux-gnu/libbrotlicommon.so.1 \ - /lib/x86_64-linux-gnu/ +COPY --from=debs /lib/x86_64-linux-gnu/libdl.so.2 \ + /lib/x86_64-linux-gnu/libpthread.so.0 \ + /lib/x86_64-linux-gnu/libglib-2.0.so.0 \ + /lib/x86_64-linux-gnu/libgobject-2.0.so.0 \ + /lib/x86_64-linux-gnu/libnss3.so \ + /lib/x86_64-linux-gnu/libnssutil3.so \ + /lib/x86_64-linux-gnu/libsmime3.so \ + /lib/x86_64-linux-gnu/libnspr4.so \ + /lib/x86_64-linux-gnu/libdbus-1.so.3 \ + /lib/x86_64-linux-gnu/libatk-1.0.so.0 \ + /lib/x86_64-linux-gnu/libatk-bridge-2.0.so.0 \ + /lib/x86_64-linux-gnu/libcups.so.2 \ + /lib/x86_64-linux-gnu/libgio-2.0.so.0 \ + /lib/x86_64-linux-gnu/libdrm.so.2 \ + /lib/x86_64-linux-gnu/libexpat.so.1 \ + /lib/x86_64-linux-gnu/libxcb.so.1 \ + /lib/x86_64-linux-gnu/libxkbcommon.so.0 \ + /lib/x86_64-linux-gnu/libatspi.so.0 \ + /lib/x86_64-linux-gnu/libX11.so.6 \ + /lib/x86_64-linux-gnu/libXcomposite.so.1 \ + /lib/x86_64-linux-gnu/libXdamage.so.1 \ + /lib/x86_64-linux-gnu/libXext.so.6 \ + /lib/x86_64-linux-gnu/libXfixes.so.3 \ + /lib/x86_64-linux-gnu/libXrandr.so.2 \ + /lib/x86_64-linux-gnu/libgbm.so.1 \ + /lib/x86_64-linux-gnu/libpango-1.0.so.0 \ + /lib/x86_64-linux-gnu/libcairo.so.2 \ + /lib/x86_64-linux-gnu/libasound.so.2 \ + /lib/x86_64-linux-gnu/libm.so.6 \ + /lib/x86_64-linux-gnu/libgcc_s.so.1 \ + /lib/x86_64-linux-gnu/libc.so.6 \ + /lib/x86_64-linux-gnu/libpcre2-8.so.0 \ + /lib/x86_64-linux-gnu/libffi.so.8 \ + /lib/x86_64-linux-gnu/libplc4.so \ + /lib/x86_64-linux-gnu/libplds4.so \ + /lib/x86_64-linux-gnu/libsystemd.so.0 \ + /lib/x86_64-linux-gnu/libgssapi_krb5.so.2 \ + /lib/x86_64-linux-gnu/libavahi-common.so.3 \ + /lib/x86_64-linux-gnu/libavahi-client.so.3 \ + /lib/x86_64-linux-gnu/libgnutls.so.30 \ + /lib/x86_64-linux-gnu/libz.so.1 \ + /lib/x86_64-linux-gnu/libgmodule-2.0.so.0 \ + /lib/x86_64-linux-gnu/libmount.so.1 \ + /lib/x86_64-linux-gnu/libselinux.so.1 \ + /lib/x86_64-linux-gnu/libXau.so.6 \ + /lib/x86_64-linux-gnu/libXdmcp.so.6 \ + /lib/x86_64-linux-gnu/libXi.so.6 \ + /lib/x86_64-linux-gnu/libXrender.so.1 \ + /lib/x86_64-linux-gnu/libwayland-server.so.0 \ + /lib/x86_64-linux-gnu/libfribidi.so.0 \ + /lib/x86_64-linux-gnu/libthai.so.0 \ + /lib/x86_64-linux-gnu/libharfbuzz.so.0 \ + /lib/x86_64-linux-gnu/libpixman-1.so.0 \ + /lib/x86_64-linux-gnu/libfontconfig.so.1 \ + /lib/x86_64-linux-gnu/libfreetype.so.6 \ + /lib/x86_64-linux-gnu/libpng16.so.16 \ + /lib/x86_64-linux-gnu/libxcb-shm.so.0 \ + /lib/x86_64-linux-gnu/libxcb-render.so.0 \ + /lib/x86_64-linux-gnu/libcap.so.2 \ + /lib/x86_64-linux-gnu/libgcrypt.so.20 \ + /lib/x86_64-linux-gnu/liblzma.so.5 \ + /lib/x86_64-linux-gnu/libzstd.so.1 \ + /lib/x86_64-linux-gnu/liblz4.so.1 \ + /lib/x86_64-linux-gnu/libkrb5.so.3 \ + /lib/x86_64-linux-gnu/libk5crypto.so.3 \ + /lib/x86_64-linux-gnu/libcom_err.so.2 \ + /lib/x86_64-linux-gnu/libkrb5support.so.0 \ + /lib/x86_64-linux-gnu/libp11-kit.so.0 \ + /lib/x86_64-linux-gnu/libidn2.so.0 \ + /lib/x86_64-linux-gnu/libunistring.so.2 \ + /lib/x86_64-linux-gnu/libtasn1.so.6 \ + /lib/x86_64-linux-gnu/libnettle.so.8 \ + /lib/x86_64-linux-gnu/libhogweed.so.6 \ + /lib/x86_64-linux-gnu/libgmp.so.10 \ + /lib/x86_64-linux-gnu/libblkid.so.1 \ + /lib/x86_64-linux-gnu/libbsd.so.0 \ + /lib/x86_64-linux-gnu/libdatrie.so.1 \ + /lib/x86_64-linux-gnu/libgraphite2.so.3 \ + /lib/x86_64-linux-gnu/libbrotlidec.so.1 \ + /lib/x86_64-linux-gnu/libgpg-error.so.0 \ + /lib/x86_64-linux-gnu/libkeyutils.so.1 \ + /lib/x86_64-linux-gnu/libresolv.so.2 \ + /lib/x86_64-linux-gnu/libmd.so.0 \ + /lib/x86_64-linux-gnu/libbrotlicommon.so.1 \ + /lib/x86_64-linux-gnu/ # Other Chrome-related libraries -COPY --from=build /usr/lib/x86_64-linux-gnu/libsoftokn3.so \ - /usr/lib/x86_64-linux-gnu/libsqlite3.so.0 \ - /usr/lib/x86_64-linux-gnu/libudev.so.1 \ - /usr/lib/x86_64-linux-gnu/libfreebl3.so \ - /usr/lib/x86_64-linux-gnu/libfreeblpriv3.so \ - /usr/lib/x86_64-linux-gnu/libudev.so.1.7.5 \ - /usr/lib/x86_64-linux-gnu/ +COPY --from=debs /usr/lib/x86_64-linux-gnu/libsoftokn3.so \ + /usr/lib/x86_64-linux-gnu/libsqlite3.so.0 \ + /usr/lib/x86_64-linux-gnu/libudev.so.1 \ + /usr/lib/x86_64-linux-gnu/libfreebl3.so \ + /usr/lib/x86_64-linux-gnu/libfreeblpriv3.so \ + /usr/lib/x86_64-linux-gnu/ # Python binary -COPY --from=build /usr/bin/python3 /usr/bin/python3 -COPY --from=build /usr/bin/python /usr/bin/python +COPY --from=debs /usr/bin/python3 /usr/bin/python3 +COPY --from=debs /usr/bin/python /usr/bin/python # Python libraries -COPY --from=build /lib/x86_64-linux-gnu/libm.so.6 \ - /lib/x86_64-linux-gnu/libz.so.1 \ - /lib/x86_64-linux-gnu/libexpat.so.1 \ - /lib/x86_64-linux-gnu/libc.so.6 \ - /lib/x86_64-linux-gnu/ +COPY --from=debs /lib/x86_64-linux-gnu/libm.so.6 \ + /lib/x86_64-linux-gnu/libz.so.1 \ + /lib/x86_64-linux-gnu/libexpat.so.1 \ + /lib/x86_64-linux-gnu/libc.so.6 \ + /lib/x86_64-linux-gnu/ -COPY --from=build /usr/lib/python3 /usr/lib/python3 -COPY --from=build /usr/lib/python3.11 /usr/lib/python3.11 +COPY --from=debs /usr/lib/python3.11 /usr/lib/python3.11 -# Python PIP packages -COPY --from=build /app/.venv /app/.venv +# Python packages and their CLI entry points +COPY --from=pyapps /x86-dist-packages /usr/lib/python3/dist-packages +COPY --from=pyapps /x86-bin /usr/bin # System libraries -COPY --from=build /lib/x86_64-linux-gnu/libc.so.6 \ - /lib/x86_64-linux-gnu/libz.so.1 \ - /lib/x86_64-linux-gnu/libbrotlidec.so.1 \ - /lib/x86_64-linux-gnu/libbrotlienc.so.1 \ - /lib/x86_64-linux-gnu/libcrypto.so.3 \ - /lib/x86_64-linux-gnu/libssl.so.3 \ - /lib/x86_64-linux-gnu/libicui18n.so.72 \ - /lib/x86_64-linux-gnu/libicuuc.so.72 \ - /lib/x86_64-linux-gnu/libstdc++.so.6 \ - /lib/x86_64-linux-gnu/libm.so.6 \ - /lib/x86_64-linux-gnu/libgcc_s.so.1 \ - /lib/x86_64-linux-gnu/libpthread.so.0 \ - /lib/x86_64-linux-gnu/libdl.so.2 \ - /lib/x86_64-linux-gnu/libbrotlicommon.so.1 \ - /lib/x86_64-linux-gnu/libicudata.so.72 \ - /lib/x86_64-linux-gnu/librt.so.1 \ - /lib/x86_64-linux-gnu/libtinfo.so.6 \ - /lib/x86_64-linux-gnu/libproc2.so.0 \ - /lib/x86_64-linux-gnu/ - -COPY --from=build /lib64/ld-linux-x86-64.so.2 /lib64/ld-linux-x86-64.so.2 -COPY --from=build /etc/ld.so.cache /etc/ld.so.cache - -# Dbus and system files -COPY --from=build /usr/lib/dbus-1.0 /usr/lib/dbus-1.0 -COPY --from=build /usr/lib/systemd /usr/lib/systemd -COPY --from=build /usr/lib/tmpfiles.d /usr/lib/tmpfiles.d -COPY --from=build /usr/lib/sysusers.d /usr/lib/sysusers.d -COPY --from=build /usr/lib/sysctl.d /usr/lib/sysctl.d +COPY --from=debs /lib/x86_64-linux-gnu/libc.so.6 \ + /lib/x86_64-linux-gnu/libz.so.1 \ + /lib/x86_64-linux-gnu/libbrotlidec.so.1 \ + /lib/x86_64-linux-gnu/libbrotlienc.so.1 \ + /lib/x86_64-linux-gnu/libcrypto.so.3 \ + /lib/x86_64-linux-gnu/libssl.so.3 \ + /lib/x86_64-linux-gnu/libstdc++.so.6 \ + /lib/x86_64-linux-gnu/libm.so.6 \ + /lib/x86_64-linux-gnu/libgcc_s.so.1 \ + /lib/x86_64-linux-gnu/libpthread.so.0 \ + /lib/x86_64-linux-gnu/libdl.so.2 \ + /lib/x86_64-linux-gnu/libbrotlicommon.so.1 \ + /lib/x86_64-linux-gnu/librt.so.1 \ + /lib/x86_64-linux-gnu/libtinfo.so.6 \ + /lib/x86_64-linux-gnu/libproc2.so.0 \ + /lib/x86_64-linux-gnu/ + +COPY --from=debs /lib64/ld-linux-x86-64.so.2 /lib64/ld-linux-x86-64.so.2 +COPY --from=debs /etc/ld.so.cache /etc/ld.so.cache # Data files -COPY --from=build /usr/share/fonts /usr/share/fonts +COPY --from=debs /usr/share/fonts /usr/share/fonts -COPY --from=build /run /run +COPY --from=debs /run /run # Distro definition -COPY --from=build /etc/os-release /etc/os-release -COPY --from=build /usr/lib/os-release /usr/lib/os-release +COPY --from=debs /etc/os-release /etc/os-release +COPY --from=debs /usr/lib/os-release /usr/lib/os-release # Configuration files -COPY --from=build /etc /etc - -# Node modules, including Puppeteer and application -COPY --from=build /app /app +COPY --from=debs /etc /etc # Required by wrapper script -COPY --from=build /bin/sh /bin/sh +COPY --from=debs /dash-amd64/bin/dash /bin/sh # Required by Playwright / Chrome -COPY --from=build /usr/bin/ps /usr/bin/ps +COPY --from=debs /usr/bin/ps /usr/bin/ps # Actual server implementation COPY ./main.py /app/main.py diff --git a/python-playwright-chromium/requirements.txt b/python-playwright-chromium/requirements.txt index 3a920b28..9f380d52 100644 --- a/python-playwright-chromium/requirements.txt +++ b/python-playwright-chromium/requirements.txt @@ -1,2 +1,2 @@ -pytest-playwright == 0.5.2 -fastapi == 0.114.1 +playwright == 1.61.0 +fastapi[standard] == 0.114.1 diff --git a/python-playwright-chromium/wrapper.sh b/python-playwright-chromium/wrapper.sh index 697748bd..8875298f 100755 --- a/python-playwright-chromium/wrapper.sh +++ b/python-playwright-chromium/wrapper.sh @@ -3,6 +3,6 @@ set -e export HOME=/root +export PATH=/usr/bin:/bin cd /app -. .venv/bin/activate exec "$@" diff --git a/redis7.2/Dockerfile b/redis7.2/Dockerfile index 6e48acc0..034c5a83 100644 --- a/redis7.2/Dockerfile +++ b/redis7.2/Dockerfile @@ -1,4 +1,4 @@ -FROM redis:7.2.2-bookworm AS build +FROM --platform=linux/x86_64 redis:7.2.2-bookworm AS build FROM scratch diff --git a/redis7.2/test_redis7.2.py b/redis7.2/test_redis7.2.py index a745f55f..efbbed86 100644 --- a/redis7.2/test_redis7.2.py +++ b/redis7.2/test_redis7.2.py @@ -14,14 +14,20 @@ import redis -from _testlib.unikraft import extract_instance_fqdn +from _testlib.readiness import retry_until_ready +from _testlib.unikraft import extract_instance_fqdn, extract_instance_name REDIS_PORT = 6379 def _connect(host: str) -> redis.Redis: - """Open a redis-py connection to the instance over TLS.""" - return redis.Redis( + """Open a redis-py client to the instance over TLS, ready to use. + + redis-py connects lazily, so constructing the client proves nothing; PING + is the first call that actually touches the network, and is therefore what + we retry while the server finishes starting. + """ + client = redis.Redis( host=host, port=REDIS_PORT, ssl=True, @@ -29,8 +35,16 @@ def _connect(host: str) -> redis.Redis: decode_responses=True, ) + retry_until_ready( + client.ping, + exceptions=(redis.ConnectionError, redis.TimeoutError), + description="redis", + ) + + return client + -def test_redis(build_image, run_instance): +def test_redis(build_image, run_instance, wait_instance): """Build, deploy, and exercise a Redis instance.""" image = build_image("redis7.2", "redis72") @@ -43,6 +57,7 @@ def test_redis(build_image, run_instance): host = extract_instance_fqdn(instance) assert host, f"could not determine instance FQDN from: {instance!r}" + wait_instance(extract_instance_name(instance), "running") r = _connect(host) try: # ------------------------------------------------------------------ diff --git a/ruby3.2-rails/.dockerignore b/ruby3.2-rails/.dockerignore index a75bcc4f..4e308a17 100644 --- a/ruby3.2-rails/.dockerignore +++ b/ruby3.2-rails/.dockerignore @@ -2,3 +2,4 @@ /Dockerfile /Kraftfile /README.md +/test_ruby3.2-rails.py diff --git a/ruby3.2-rails/Dockerfile b/ruby3.2-rails/Dockerfile index a67f7e56..235cca76 100644 --- a/ruby3.2-rails/Dockerfile +++ b/ruby3.2-rails/Dockerfile @@ -1,13 +1,72 @@ -FROM ruby:3.2.2-bookworm AS build +# The generated app tree is architecture-independent text, so this stage +# runs natively on whatever the build host is. The gems it installs are +# only needed to run the rails generators and are discarded. +FROM --platform=$BUILDPLATFORM ruby:3.2.2-bookworm AS appgen -RUN gem install rails -RUN rails new app +ARG RAILS_VERSION=8.1.3 +RUN gem install rails -v "$RAILS_VERSION" + +# --skip-bundle: `rails new` would otherwise run a full `bundle install` right +# here, natively compiling Puma and nio4r for the build host — only for the +# next instruction to drop Puma from the Gemfile again. Skipping it means the +# app bundle is resolved and installed exactly once, below. +RUN rails new app --minimal --skip-bundle WORKDIR /app + +# Puma ships a C extension (its HTTP/1.1 parser) and pulls in nio4r, another +# C extension; both would otherwise compile from source in the x86_64 `build` +# stage — under QEMU emulation on non-amd64 hosts. WEBrick is pure Ruby, so +# swapping it in removes the only meaningful native compilation left in the +# emulated bundle install. With Puma gone, `rails server` (the Kraftfile cmd) +# selects WEBrick — the only Rack server gem left in the bundle — via Rack's +# handler auto-detection; config/puma.rb is now dead config and is dropped. +# WEBrick is single-threaded and not a production server, which is acceptable +# for this hello-world example. +# +# This is a plain text edit rather than `bundle remove` / `bundle add` because +# those resolve (and `bundle add` installs) on their own, which is exactly the +# duplicated work --skip-bundle just removed. The grep guard fails the build +# loudly if a future Rails changes the generated Gemfile format and the +# substitution silently matches nothing, leaving Puma in the bundle. +RUN sed -i '/^[[:space:]]*gem "puma"/d' Gemfile && \ + ! grep -qE '^[[:space:]]*gem .puma.' Gemfile && \ + printf '\ngem "webrick"\n' >> Gemfile && \ + rm -f config/puma.rb + +# The rootfs targets x86_64 regardless of build host, so the lockfile has to +# cover that platform. Resolving with --add-platform *first* writes a lockfile +# for both platforms in a single resolve; the install below then works from +# that lockfile rather than resolving the graph a second time. +RUN bundle lock --add-platform x86_64-linux + +# The only gem install in this stage. It has to precede `rails generate`, +# which boots the app and therefore needs the gems present. +RUN bundle install --jobs 4 + +# `bundle install` resolves for the build host's own platform, so it is what +# would drop x86_64-linux from the lockfile if bundler ever pruned it. That +# would otherwise surface much later — as "your bundle only supports platforms +# [...]" from the emulated x86_64 stage. Fail here, where the cause is obvious. +RUN grep -q 'x86_64-linux' Gemfile.lock || \ + { echo 'ERROR: x86_64-linux was pruned from Gemfile.lock' >&2; exit 1; } + RUN rails generate controller hello COPY . /app/ +# Arch-specific caches must not leak into the rootfs. +RUN rm -rf tmp/cache log/* + +# Gem native extensions must be compiled for x86_64, which no Ruby tooling +# can cross-compile, so this is the one stage that has to execute x86_64 +# binaries (under QEMU emulation on other hosts). +FROM --platform=linux/x86_64 ruby:3.2.2-bookworm AS build + +WORKDIR /app +COPY --from=appgen /app/Gemfile /app/Gemfile.lock ./ +RUN bundle install --jobs 4 + FROM scratch # Ruby binary @@ -44,5 +103,5 @@ COPY --from=build /lib/x86_64-linux-gnu/libyaml-0.so.2 /lib/x86_64-linux-gnu/lib # Timezone information COPY --from=build /usr/share/zoneinfo /usr/share/zoneinfo -# Rails application -COPY --from=build /app /app +# Rails application (arch-independent, generated in the native stage) +COPY --from=appgen /app /app diff --git a/ruby3.2-rails/README.md b/ruby3.2-rails/README.md index 1ed0869c..b041704f 100644 --- a/ruby3.2-rails/README.md +++ b/ruby3.2-rails/README.md @@ -193,7 +193,7 @@ Lines in the `Kraftfile` have the following roles: `source: ./Dockerfile` means the filesystem is built using the `Dockerfile`. `format: erofs` means the filesystem type is [EROFS](https://erofs.docs.kernel.org/). -* `cmd: ["/usr/bin/ruby", "/app/bin/rails", "server", "-b", "0.0.0.0"]`: Use `/usr/bin/ruby /app/bin/rails server -b 0.0.0.0` as the starting command of the instance. +* `cmd: ["/usr/bin/ruby", "/app/bin/rails", "server", "-u", "webrick", "-b", "0.0.0.0"]`: Use `/usr/bin/ruby /app/bin/rails server -u webrick -b 0.0.0.0` as the starting command of the instance. Lines in the `Dockerfile` have the following roles: diff --git a/skipper0.18/Dockerfile b/skipper0.18/Dockerfile index 41cdd49d..e4de2327 100644 --- a/skipper0.18/Dockerfile +++ b/skipper0.18/Dockerfile @@ -1,4 +1,7 @@ -FROM golang:1.21.4-bookworm AS build +FROM --platform=$BUILDPLATFORM golang:1.21.4-bookworm AS build + +ARG TARGETOS +ARG TARGETARCH RUN set -xe; apt-get update ; \ apt-get install -y --no-install-recommends \ @@ -14,8 +17,20 @@ RUN set -xe; \ WORKDIR /skipper-0.18.51 +# Cross-compile natively on the build host. -linkmode external requires a C +# toolchain for the target architecture, so install a cross toolchain when +# the build and target architectures differ. RUN set -xe; \ - CGO_ENABLED=1 \ + if [ "$(dpkg --print-architecture)" != "${TARGETARCH}" ]; then \ + apt-get update; \ + apt-get install -y --no-install-recommends crossbuild-essential-${TARGETARCH}; \ + case "${TARGETARCH}" in \ + amd64) export CC=x86_64-linux-gnu-gcc ;; \ + arm64) export CC=aarch64-linux-gnu-gcc ;; \ + *) echo "unsupported TARGETARCH: ${TARGETARCH}" >&2; exit 1 ;; \ + esac; \ + fi; \ + CGO_ENABLED=1 GOOS=${TARGETOS} GOARCH=${TARGETARCH} \ go build \ -buildmode=pie \ -ldflags "-linkmode external -extldflags '-static-pie'" \ diff --git a/spin-wagi-http/Dockerfile b/spin-wagi-http/Dockerfile index a0699375..5ed34109 100644 --- a/spin-wagi-http/Dockerfile +++ b/spin-wagi-http/Dockerfile @@ -1,4 +1,8 @@ -FROM rust:1.95.0-bookworm AS spin-devenv +# Build the wasm components natively on the build host — wasm is +# architecture-independent. spin and the WASI SDK run on the build host, so +# fetch them for its architecture; the amd64 spin binary that ships in the +# final image is downloaded separately. +FROM --platform=$BUILDPLATFORM rust:1.95.0-bookworm AS spin-devenv ARG SPIN_VERSION=4.0.0 @@ -12,18 +16,32 @@ RUN set -xe; \ wget; RUN set -xe; \ - wget -q -O spin.tar.gz "https://github.com/fermyon/spin/releases/download/v${SPIN_VERSION}/spin-v${SPIN_VERSION}-linux-amd64.tar.gz"; \ + case "$(dpkg --print-architecture)" in \ + amd64) SPIN_ARCH=amd64 ;; \ + arm64) SPIN_ARCH=aarch64 ;; \ + *) echo "unsupported build architecture" >&2; exit 1 ;; \ + esac; \ + wget -q -O spin.tar.gz "https://github.com/fermyon/spin/releases/download/v${SPIN_VERSION}/spin-v${SPIN_VERSION}-linux-${SPIN_ARCH}.tar.gz"; \ tar xzvf ./spin.tar.gz; \ mv ./spin /usr/bin/spin; \ rm -rf spin.tar.gz; \ + wget -q -O spin-amd64.tar.gz "https://github.com/fermyon/spin/releases/download/v${SPIN_VERSION}/spin-v${SPIN_VERSION}-linux-amd64.tar.gz"; \ + mkdir -p /spin-dist; \ + tar xzf ./spin-amd64.tar.gz -C /spin-dist; \ + rm -rf spin-amd64.tar.gz; \ rustup target add wasm32-wasip1 -ARG WASI_SDK_VERSION=21 +ARG WASI_SDK_VERSION=25 WORKDIR /wasi RUN set -xe; \ - wget -q -O wasi-sdk.tar.gz "https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-${WASI_SDK_VERSION}/wasi-sdk-${WASI_SDK_VERSION}.0-linux.tar.gz"; \ + case "$(dpkg --print-architecture)" in \ + amd64) WASI_SDK_ARCH=x86_64 ;; \ + arm64) WASI_SDK_ARCH=arm64 ;; \ + *) echo "unsupported build architecture" >&2; exit 1 ;; \ + esac; \ + wget -q -O wasi-sdk.tar.gz "https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-${WASI_SDK_VERSION}/wasi-sdk-${WASI_SDK_VERSION}.0-${WASI_SDK_ARCH}-linux.tar.gz"; \ mkdir -p /opt/wasi-sdk; \ tar xzvf ./wasi-sdk.tar.gz --strip-components 1 -C /opt/wasi-sdk/; \ rm -rf ./wasi-sdk.tar.gz; @@ -38,20 +56,24 @@ WORKDIR /app RUN spin build +# x86_64 system libraries for the final image (copy-only, nothing runs under +# emulation). +FROM --platform=linux/x86_64 debian:bookworm-slim AS libs + FROM scratch # Binary executable -COPY --from=build /usr/bin/spin /usr/bin/spin +COPY --from=build /spin-dist/spin /usr/bin/spin # System libraries -COPY --from=build /lib/x86_64-linux-gnu/libstdc++.so.6 /lib/x86_64-linux-gnu/libstdc++.so.6 -COPY --from=build /lib/x86_64-linux-gnu/libgcc_s.so.1 /lib/x86_64-linux-gnu/libgcc_s.so.1 -COPY --from=build /lib/x86_64-linux-gnu/librt.so.1 /lib/x86_64-linux-gnu/librt.so.1 -COPY --from=build /lib/x86_64-linux-gnu/libpthread.so.0 /lib/x86_64-linux-gnu/libpthread.so.0 -COPY --from=build /lib/x86_64-linux-gnu/libm.so.6 /lib/x86_64-linux-gnu/libm.so.6 -COPY --from=build /lib/x86_64-linux-gnu/libdl.so.2 /lib/x86_64-linux-gnu/libdl.so.2 -COPY --from=build /lib/x86_64-linux-gnu/libc.so.6 /lib/x86_64-linux-gnu/libc.so.6 -COPY --from=build /lib64/ld-linux-x86-64.so.2 /lib64/ld-linux-x86-64.so.2 +COPY --from=libs /lib/x86_64-linux-gnu/libstdc++.so.6 /lib/x86_64-linux-gnu/libstdc++.so.6 +COPY --from=libs /lib/x86_64-linux-gnu/libgcc_s.so.1 /lib/x86_64-linux-gnu/libgcc_s.so.1 +COPY --from=libs /lib/x86_64-linux-gnu/librt.so.1 /lib/x86_64-linux-gnu/librt.so.1 +COPY --from=libs /lib/x86_64-linux-gnu/libpthread.so.0 /lib/x86_64-linux-gnu/libpthread.so.0 +COPY --from=libs /lib/x86_64-linux-gnu/libm.so.6 /lib/x86_64-linux-gnu/libm.so.6 +COPY --from=libs /lib/x86_64-linux-gnu/libdl.so.2 /lib/x86_64-linux-gnu/libdl.so.2 +COPY --from=libs /lib/x86_64-linux-gnu/libc.so.6 /lib/x86_64-linux-gnu/libc.so.6 +COPY --from=libs /lib64/ld-linux-x86-64.so.2 /lib64/ld-linux-x86-64.so.2 # CA certificates required by Spin for TLS initialization COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt diff --git a/traefik/Dockerfile b/traefik/Dockerfile index 9906affc..32336ea0 100644 --- a/traefik/Dockerfile +++ b/traefik/Dockerfile @@ -1,4 +1,4 @@ -FROM node:14.16 AS build-frontend +FROM --platform=$BUILDPLATFORM node:14.16 AS build-frontend WORKDIR / @@ -8,18 +8,15 @@ RUN set -xe; \ WORKDIR /traefik-2.10.6/webui -# Build traefik frontend +# Build traefik frontend (architecture-independent) RUN set -xe; \ yarn install; \ yarn build -FROM golang:1.21.4-alpine3.17 AS build-backend +FROM --platform=$BUILDPLATFORM golang:1.21.4-bookworm AS build-backend -RUN set -xe; \ - apk --no-cache add \ - gcc \ - musl-dev \ - ; +ARG TARGETOS +ARG TARGETARCH COPY --from=build-frontend /traefik-2.10.6 /traefik @@ -28,11 +25,24 @@ WORKDIR /traefik ENV TRAEFIK_CODENAME=saintmarcelin ENV TRAEFIK_VERSION=2.10.6 +# Cross-compile natively on the build host. -linkmode external requires a C +# toolchain for the target architecture, so install a cross toolchain when +# the build and target architectures differ. RUN set -xe; \ - CGO_ENABLED=1 \ + if [ "$(dpkg --print-architecture)" != "${TARGETARCH}" ]; then \ + apt-get update; \ + apt-get install -y --no-install-recommends crossbuild-essential-${TARGETARCH}; \ + case "${TARGETARCH}" in \ + amd64) export CC=x86_64-linux-gnu-gcc ;; \ + arm64) export CC=aarch64-linux-gnu-gcc ;; \ + *) echo "unsupported TARGETARCH: ${TARGETARCH}" >&2; exit 1 ;; \ + esac; \ + fi; \ + CGO_ENABLED=1 GOOS=${TARGETOS} GOARCH=${TARGETARCH} \ go build -v \ -buildmode=pie \ -ldflags "-linkmode external -extldflags '-static-pie' -X 'github.com/traefik/traefik/v2/pkg/version.Version=${TRAEFIK_VERSION}' -X 'github.com/traefik/traefik/v2/pkg/version.Codename=${TRAEFIK_CODENAME}' -X 'github.com/traefik/traefik/v2/pkg/version.BuildDate=$(date)'" \ + -tags netgo \ -o /usr/bin/traefik \ ./cmd/traefik diff --git a/tyk/README.md b/tyk/README.md index a7562674..43556758 100644 --- a/tyk/README.md +++ b/tyk/README.md @@ -43,7 +43,10 @@ export UKC_METRO=fra The `REDIS_PASSWORD` environment variable sets the Redis `requirepass` directive. If not provided, it defaults to `unikraft`. -Build and deploy the Redis instance (used internally by Tyk): +Build and deploy the Redis instance (used internally by Tyk). +The internal domain is whatever you pass via `--domain` (this guide uses +`tyk-redis.internal`); Tyk learns about it through the `TYK_GW_STORAGE_HOST` +environment variable, so set that to the same value when deploying Tyk. **Using the unikraft CLI (Recommended)** ```bash title="unikraft" @@ -110,19 +113,22 @@ or ## Tyk -Build and deploy the Tyk instance: +Build and deploy the Tyk instance. +The `TYK_GW_STORAGE_HOST` environment variable tells Tyk which Redis host to +use; `tyk.conf` deliberately leaves `storage.host` empty, so this variable is +required and must match the internal domain you gave the Redis instance. **Using the unikraft CLI (Recommended)** ```bash title="unikraft" unikraft build ./tyk --output /tyk:latest -unikraft run --scale-to-zero policy=on,cooldown-time=1000 --metro fra -p 443:8080/tls+http -m 256M --image /tyk:latest -e TYK_GW_STORAGE_PASSWORD=unikraft +unikraft run --scale-to-zero policy=on,cooldown-time=1000 --metro fra -p 443:8080/tls+http -m 256M --image /tyk:latest -e TYK_GW_STORAGE_PASSWORD=unikraft -e TYK_GW_STORAGE_HOST=tyk-redis.internal ``` or **Using the legacy kraft CLI** ```bash title="kraft" -kraft cloud deploy --scale-to-zero on --scale-to-zero-cooldown 1s -p 443:8080/tls+http -M 256Mi --env TYK_GW_STORAGE_PASSWORD=unikraft ./tyk/ +kraft cloud deploy --scale-to-zero on --scale-to-zero-cooldown 1s -p 443:8080/tls+http -M 256Mi --env TYK_GW_STORAGE_PASSWORD=unikraft --env TYK_GW_STORAGE_HOST=tyk-redis.internal ./tyk/ ``` Make sure to replace `` with your username / org-name in the unikraft CLI commands above. diff --git a/tyk/redis/Dockerfile b/tyk/redis/Dockerfile index 87bbb1a0..4d9f5473 100644 --- a/tyk/redis/Dockerfile +++ b/tyk/redis/Dockerfile @@ -1,4 +1,4 @@ -FROM redis:7.2.2-bookworm AS build +FROM --platform=linux/x86_64 redis:7.2.2-bookworm AS build FROM scratch diff --git a/tyk/test_tyk.py b/tyk/test_tyk.py index a83e5a47..7e6f65d6 100644 --- a/tyk/test_tyk.py +++ b/tyk/test_tyk.py @@ -2,7 +2,7 @@ Mirrors the manual steps from ``tyk/README.md``: -1. Build and deploy the Redis instance (internal, on ``tyk-redis.internal``, +1. Build and deploy the Redis instance (internal, on a configurable domain, with REDIS_PASSWORD=unikraft). 2. Build and deploy the Tyk API gateway (public, on port 443:8080/tls+http). 3. ``curl https:///hello`` and assert Tyk health response with @@ -11,6 +11,8 @@ from __future__ import annotations +import os + from _testlib.unikraft import extract_instance_name, extract_instance_url @@ -18,28 +20,37 @@ def test_tyk_hello(build_image, run_instance, http, wait_instance, test_run_id): + # Internal domain for the Redis instance. Unique per test run so parallel + # CI runs (e.g. matrix legs sharing a metro) don't collide on the same + # domain; set TYK_REDIS_DOMAIN to override. + redis_domain = os.environ.get("TYK_REDIS_DOMAIN") or f"tyk-redis-{test_run_id}.internal" + # 1. Build and deploy internal Redis instance. - # NOTE: domain must be "tyk-redis.internal" — hardcoded in tyk/rootfs/etc/tyk.conf. redis_image = build_image("tyk/redis", "tyk-redis") run_instance( redis_image, memory="256M", - domain="tyk-redis.internal", + domain=redis_domain, scale_to_zero={"policy": "idle", "cooldown-time": "1000", "stateful": "true"}, env={"REDIS_PASSWORD": REDIS_PASSWORD}, name=f"redis-{test_run_id}", ) # 2. Build and deploy the Tyk gateway. + # TYK_GW_STORAGE_HOST tells Tyk which Redis host to use; tyk.conf leaves + # storage.host empty, so this variable is required. tyk_image = build_image("tyk/tyk", "tyk-gw") tyk_instance = run_instance( tyk_image, publish=["443:8080/tls+http"], memory="256M", - env={"TYK_GW_STORAGE_PASSWORD": REDIS_PASSWORD}, name=f"tyk-{test_run_id}", + env={ + "TYK_GW_STORAGE_PASSWORD": REDIS_PASSWORD, + "TYK_GW_STORAGE_HOST": redis_domain, + }, ) url = extract_instance_url(tyk_instance) diff --git a/tyk/tyk/Dockerfile b/tyk/tyk/Dockerfile index a8b31520..ba3fb9d2 100644 --- a/tyk/tyk/Dockerfile +++ b/tyk/tyk/Dockerfile @@ -1,16 +1,30 @@ -FROM --platform=linux/x86_64 golang:1.21.3-bookworm AS build +FROM --platform=$BUILDPLATFORM golang:1.21.3-bookworm AS build ARG TYK_VERSION=5.2.6 +ARG TARGETOS +ARG TARGETARCH WORKDIR /tyk +# Cross-compile natively on the build host. -linkmode external requires a C +# toolchain for the target architecture, so install a cross toolchain when +# the build and target architectures differ. RUN --mount=type=cache,target=/root/go/pkg/mod \ --mount=type=cache,target=/root/.cache/go-build \ set -xe; \ apt-get update; \ apt-get install -y --no-install-recommends \ git; \ + if [ "$(dpkg --print-architecture)" != "${TARGETARCH}" ]; then \ + apt-get install -y --no-install-recommends crossbuild-essential-${TARGETARCH}; \ + case "${TARGETARCH}" in \ + amd64) export CC=x86_64-linux-gnu-gcc ;; \ + arm64) export CC=aarch64-linux-gnu-gcc ;; \ + *) echo "unsupported TARGETARCH: ${TARGETARCH}" >&2; exit 1 ;; \ + esac; \ + fi; \ git clone --depth=1 --branch v${TYK_VERSION} https://github.com/TykTechnologies/tyk /tyk; \ + CGO_ENABLED=1 GOOS=${TARGETOS} GOARCH=${TARGETARCH} \ go build \ -buildmode=pie \ -ldflags "-linkmode external -extldflags -static-pie" \ diff --git a/tyk/tyk/rootfs/etc/tyk.conf b/tyk/tyk/rootfs/etc/tyk.conf index 5482a814..904b5d8e 100644 --- a/tyk/tyk/rootfs/etc/tyk.conf +++ b/tyk/tyk/rootfs/etc/tyk.conf @@ -8,7 +8,7 @@ "middleware_path": "/opt/tyk-gateway/middleware", "storage": { "type": "redis", - "host": "tyk-redis.internal", + "host": "", "port": 6379, "username": "", "database": 0, diff --git a/visual-studio-code-server/Dockerfile b/visual-studio-code-server/Dockerfile index fcf5d9ee..16b0efe7 100644 --- a/visual-studio-code-server/Dockerfile +++ b/visual-studio-code-server/Dockerfile @@ -1,3 +1,5 @@ -FROM ubuntu:latest +FROM --platform=linux/x86_64 lscr.io/linuxserver/code-server:4.103.2 AS code-server -COPY --from=lscr.io/linuxserver/code-server:4.103.2 /app /app +FROM --platform=linux/x86_64 ubuntu:latest + +COPY --from=code-server /app /app diff --git a/vsftpd/Dockerfile b/vsftpd/Dockerfile index b0467d06..f0f7967c 100644 --- a/vsftpd/Dockerfile +++ b/vsftpd/Dockerfile @@ -1,4 +1,4 @@ -FROM debian:bookworm AS build +FROM --platform=linux/x86_64 debian:bookworm AS build # Install packages. RUN set -xe; \ diff --git a/wazero-import-go/Dockerfile b/wazero-import-go/Dockerfile index 0803b6c7..68559a6b 100644 --- a/wazero-import-go/Dockerfile +++ b/wazero-import-go/Dockerfile @@ -1,4 +1,7 @@ -FROM golang:1.21.6-bookworm AS build +FROM --platform=$BUILDPLATFORM golang:1.21.6-bookworm AS build + +ARG TARGETOS +ARG TARGETARCH WORKDIR /src @@ -7,7 +10,20 @@ COPY ./go.mod /src/go.mod COPY ./go.sum /src/go.sum COPY ./testdata /src/testdata +# Cross-compile natively on the build host. -linkmode external requires a C +# toolchain for the target architecture, so install a cross toolchain when +# the build and target architectures differ. RUN set -xe; \ + if [ "$(dpkg --print-architecture)" != "${TARGETARCH}" ]; then \ + apt-get update; \ + apt-get install -y --no-install-recommends crossbuild-essential-${TARGETARCH}; \ + case "${TARGETARCH}" in \ + amd64) export CC=x86_64-linux-gnu-gcc ;; \ + arm64) export CC=aarch64-linux-gnu-gcc ;; \ + *) echo "unsupported TARGETARCH: ${TARGETARCH}" >&2; exit 1 ;; \ + esac; \ + fi; \ + CGO_ENABLED=1 GOOS=${TARGETOS} GOARCH=${TARGETARCH} \ go build -v \ -buildmode=pie \ -ldflags "-linkmode external -extldflags -static-pie" \ diff --git a/wordpress-all-in-one/Dockerfile b/wordpress-all-in-one/Dockerfile index 2707bcef..ad4060d1 100644 --- a/wordpress-all-in-one/Dockerfile +++ b/wordpress-all-in-one/Dockerfile @@ -1,4 +1,4 @@ -FROM debian:bookworm AS base +FROM --platform=linux/x86_64 debian:bookworm AS base RUN set -xe; \ apt-get -yqq update; \ diff --git a/wordpress/test_wordpress.py b/wordpress/test_wordpress.py index e04d7c59..3dd23ef6 100644 --- a/wordpress/test_wordpress.py +++ b/wordpress/test_wordpress.py @@ -47,13 +47,15 @@ def _cleanup_volumes(): # 2. Build and deploy the MariaDB instance. mariadb_image = build_image("wordpress/mariadb", "wordpress-mariadb") + mariadb_domain = f"{test_run_id}-mariadb.internal" + run_instance( mariadb_image, memory="1G", - domain="wordpress-mariadb.internal", + domain=mariadb_domain, volume=f"{db_volume}:/var/lib/mysql", env={"MARIADB_ROOT_PASSWORD": MARIADB_ROOT_PASSWORD}, - name=f"mariadb-{test_run_id}" + name=f"mariadb-{test_run_id}", ) # 3. Build and deploy the WordPress instance. @@ -64,8 +66,8 @@ def _cleanup_volumes(): publish=["443:8080/tls+http"], memory="2G", volume=f"{wp_volume}:/var/www/html", - env={"WORDPRESS_DB_HOST": "wordpress-mariadb.internal"}, - name=f"wordpress-{test_run_id}" + name=f"wordpress-{test_run_id}", + env={"WORDPRESS_DB_HOST": mariadb_domain}, ) url = extract_instance_url(wp_instance) diff --git a/wordpress/wordpress/Dockerfile b/wordpress/wordpress/Dockerfile index 7c635162..b2d9ecf9 100644 --- a/wordpress/wordpress/Dockerfile +++ b/wordpress/wordpress/Dockerfile @@ -1,11 +1,5 @@ FROM debian:bookworm AS base -ARG WORDPRESS_DB_NAME=wordpress -ARG WORDPRESS_DB_USER=wordpress -ARG WORDPRESS_DB_PASSWORD=wordpresspass -ARG WORDPRESS_DB_HOST=wordpress-mariadb.internal -ARG WORDPRESS_VERSION=wordpress-6.5.5 - # General packages RUN set -xe; \ apt-get -yqq update; \ @@ -27,21 +21,17 @@ RUN set -xe; \ # MariaDB setup COPY init.sql /init.sql -# Wordpress setup +# Wordpress setup. The database settings in wp-config.php are filled in at +# startup by wrapper.sh from the WORDPRESS_DB_* environment variables. WORKDIR /src RUN set -xe; \ + WORDPRESS_VERSION=wordpress-6.5.5; \ wget https://wordpress.org/${WORDPRESS_VERSION}.zip; \ unzip ${WORDPRESS_VERSION}.zip; \ rm ${WORDPRESS_VERSION}.zip; \ mkdir -p /var/www/html-tmp; \ mv wordpress/* /var/www/html-tmp; \ - cp /var/www/html-tmp/wp-config-sample.php /var/www/html-tmp/wp-config.php; \ - sed -i \ - -e "s/define( 'DB_NAME', '[^']*' );/define( 'DB_NAME', '${WORDPRESS_DB_NAME}' );/" \ - -e "s/define( 'DB_USER', '[^']*' );/define( 'DB_USER', '${WORDPRESS_DB_USER}' );/" \ - -e "s/define( 'DB_PASSWORD', '[^']*' );/define( 'DB_PASSWORD', '${WORDPRESS_DB_PASSWORD}' );/" \ - -e "s/define( 'DB_HOST', '[^']*' );/define( 'DB_HOST', '${WORDPRESS_DB_HOST}' );/" \ - /var/www/html-tmp/wp-config.php \ + cp /var/www/html-tmp/wp-config-sample.php /var/www/html-tmp/wp-config.php \ ; # Nginx setup diff --git a/wordpress/wordpress/wrapper.sh b/wordpress/wordpress/wrapper.sh index 6cd1082b..3144f393 100644 --- a/wordpress/wordpress/wrapper.sh +++ b/wordpress/wordpress/wrapper.sh @@ -2,6 +2,13 @@ set -e +# Database settings, overridable via --env at instance creation. The defaults +# match the database, user and password created by /init.sql. +WORDPRESS_DB_NAME="${WORDPRESS_DB_NAME:-wordpress}" +WORDPRESS_DB_USER="${WORDPRESS_DB_USER:-wordpress}" +WORDPRESS_DB_PASSWORD="${WORDPRESS_DB_PASSWORD:-wordpresspass}" +WORDPRESS_DB_HOST="${WORDPRESS_DB_HOST:-wordpress-mariadb.internal}" + # Wait for MariaDB to be ready retries=0 max_retries=5 @@ -21,6 +28,14 @@ mysql -h "$WORDPRESS_DB_HOST" -u root -punikraft < /init.sql echo "Copying WordPress files ..." cp -r /var/www/html-tmp/* /var/www/html/ +echo "Configuring wp-config.php ..." +sed -i \ + -e "s/define( 'DB_NAME', '[^']*' );/define( 'DB_NAME', '${WORDPRESS_DB_NAME}' );/" \ + -e "s/define( 'DB_USER', '[^']*' );/define( 'DB_USER', '${WORDPRESS_DB_USER}' );/" \ + -e "s/define( 'DB_PASSWORD', '[^']*' );/define( 'DB_PASSWORD', '${WORDPRESS_DB_PASSWORD}' );/" \ + -e "s/define( 'DB_HOST', '[^']*' );/define( 'DB_HOST', '${WORDPRESS_DB_HOST}' );/" \ + /var/www/html/wp-config.php + echo "Starting PHP FPM daemon..." /usr/sbin/php-fpm8.2 --fpm-config /etc/php/8.2/fpm/php-fpm.conf /usr/lib/php/php-fpm-socket-helper install /run/php/php-fpm.sock /etc/php/8.2/fpm/pool.d/www.conf 82