# Stage 1: Build Flutter web FROM --platform=$BUILDPLATFORM ghcr.io/cirruslabs/flutter:stable AS flutter-builder # The base image's stable SDK can lag the stable SDK installed by GitHub Actions. # Use the same checked-in version as CI and the store builds before resolving pub. COPY app/.flutter-version /tmp/cantinarr-flutter-version # Keep the tag locally: Flutter derives its version from Git tags, not just HEAD. RUN cantinarr_flutter_version="$(cat /tmp/cantinarr-flutter-version)" \ && git -C "$FLUTTER_ROOT" fetch --depth=1 origin \ "refs/tags/$cantinarr_flutter_version:refs/tags/$cantinarr_flutter_version" \ && git -C "$FLUTTER_ROOT" checkout --detach --force "refs/tags/$cantinarr_flutter_version" \ && rm -f "$FLUTTER_ROOT/version" "$FLUTTER_ROOT/bin/cache/flutter.version.json" \ && flutter --version --machine > /tmp/cantinarr-flutter-sdk.json \ && cat /tmp/cantinarr-flutter-sdk.json \ && grep -Fq "\"frameworkVersion\": \"$cantinarr_flutter_version\"" /tmp/cantinarr-flutter-sdk.json ARG CANTINARR_E2E_WEB_SEMANTICS=false # Build number for the web bundle, shown in Settings → About. Without it the # bundle reports pubspec's placeholder (`+1`) and every self-hosted deployment # claims to be build 1. CI passes the count of commits that have touched app/ # (see docker.yml); local builds leave it unset and keep the placeholder. ARG APP_BUILD_NUMBER= WORKDIR /app COPY app/pubspec.yaml app/pubspec.lock ./ # pub get generates the locale classes, so its inputs must be present in the # dependency layer as well as in the later full source copy. COPY app/l10n.yaml ./ COPY app/lib/l10n/*.arb ./lib/l10n/ RUN flutter pub get --enforce-lockfile COPY app/ . RUN flutter build web --release --no-pub \ --dart-define=CANTINARR_E2E_WEB_SEMANTICS=${CANTINARR_E2E_WEB_SEMANTICS} \ ${APP_BUILD_NUMBER:+--build-number=${APP_BUILD_NUMBER}} # Codex app-server is the supported boundary for ChatGPT subscription auth. # Pin and verify the standalone musl binary for reproducible multi-arch images. FROM --platform=$BUILDPLATFORM alpine:3.22 AS codex-downloader ARG TARGETARCH ARG CODEX_VERSION=0.162.0 RUN apk add --no-cache ca-certificates curl tar RUN set -eux; \ case "$TARGETARCH" in \ amd64) target="x86_64-unknown-linux-musl"; sha="629ef675191921f687ed1e016ba6d3dfc8c171c62fa506e3082a9db32817dc3e" ;; \ arm64) target="aarch64-unknown-linux-musl"; sha="9cb0f864a44aaa1f3dbdec881c04c8e1d48eb9dc8ec2af4a3fadc7ad9b78a469" ;; \ *) echo "unsupported Codex architecture: $TARGETARCH" >&2; exit 1 ;; \ esac; \ archive="codex-app-server-${target}.tar.gz"; \ curl --fail --location --retry 3 \ "https://github.com/openai/codex/releases/download/rust-v${CODEX_VERSION}/${archive}" \ --output "/tmp/${archive}"; \ echo "${sha} /tmp/${archive}" | sha256sum -c -; \ tar -xzf "/tmp/${archive}" -C /tmp; \ install -m 0755 "/tmp/codex-app-server-${target}" /codex-app-server; \ curl --fail --location --retry 3 \ "https://raw.githubusercontent.com/openai/codex/rust-v${CODEX_VERSION}/LICENSE" \ --output /tmp/CODEX-LICENSE; \ curl --fail --location --retry 3 \ "https://raw.githubusercontent.com/openai/codex/rust-v${CODEX_VERSION}/NOTICE" \ --output /tmp/CODEX-NOTICE; \ echo "d17f227e4df5da1600391338865ce0f3055211760a36688f816941d58232d8dc /tmp/CODEX-LICENSE" | sha256sum -c -; \ echo "9d71575ecfd9a843fc1677b0efb08053c6ba9fd686a0de1a6f5382fd3c220915 /tmp/CODEX-NOTICE" | sha256sum -c -; \ install -d /codex-license; \ install -m 0644 /tmp/CODEX-LICENSE /codex-license/LICENSE; \ install -m 0644 /tmp/CODEX-NOTICE /codex-license/NOTICE # Stage 2: Build Go binary FROM golang:1.26.10-alpine AS go-builder WORKDIR /build COPY server/go.mod server/go.sum ./ RUN go mod download COPY server/ . # Copy Flutter web build into the Go embed directory COPY --from=flutter-builder /app/build/web/ ./internal/web/dist/ RUN go run ./cmd/compress-web ARG VERSION=dev RUN CGO_ENABLED=0 go build -ldflags "-X github.com/windoze95/cantinarr-server/internal/version.Version=${VERSION}" -o cantinarr ./cmd/server # Build and run the copied Python environment on the same Alpine/Python ABI. FROM alpine:3.24 AS runtime-base # Companion runs in a private stdio worker, with a pinned Python dependency set. FROM runtime-base AS appletv-builder RUN apk add --no-cache python3 py3-pip libstdc++ build-base python3-dev libffi-dev RUN python3 -m venv /opt/cantinarr-appletv COPY server/tools/apple_tv/requirements.lock /tmp/apple-tv-requirements.lock RUN /opt/cantinarr-appletv/bin/pip install --no-cache-dir --require-hashes -r /tmp/apple-tv-requirements.lock # Keep package metadata and bundled license files in the venv. An index lets # operators find each distribution's notices without relying on network access. COPY server/tools/apple_tv/collect_notices.py /tmp/collect_notices.py RUN /opt/cantinarr-appletv/bin/python /tmp/collect_notices.py /apple-tv-notices # Final image FROM runtime-base # su-exec is what the entrypoint drops privileges with when PUID/PGID are set. RUN apk add --no-cache ca-certificates su-exec python3 libstdc++ COPY --from=go-builder /build/cantinarr /usr/local/bin/ COPY --from=codex-downloader /codex-app-server /usr/local/bin/ COPY --from=codex-downloader /codex-license/ /usr/share/licenses/codex-app-server/ COPY --from=appletv-builder /opt/cantinarr-appletv/ /opt/cantinarr-appletv/ COPY --from=appletv-builder /apple-tv-notices/ /usr/share/licenses/cantinarr-appletv/ COPY server/tools/apple_tv/cantinarr_appletv.py /usr/lib/cantinarr/apple_tv/cantinarr_appletv.py COPY --chmod=0755 server/tools/apple_tv/helper.sh /usr/local/bin/cantinarr-appletv-helper RUN cantinarr-appletv-helper --self-test # Shared with server/Dockerfile: honours PUID/PGID (own /config, run as that # user), otherwise exec's the command untouched, root as before. COPY --chmod=0755 server/docker/entrypoint.sh /entrypoint.sh EXPOSE 8585 VOLUME /config ENTRYPOINT ["/entrypoint.sh"] CMD ["cantinarr"]