From ae2d0be2eaf14a22c2933808a8477e25098472f3 Mon Sep 17 00:00:00 2001 From: luulam Date: Tue, 25 Aug 2026 11:46:28 +0700 Subject: [PATCH] feat(docker): add all-in-one multi-arch Dockerfile and Gitea deploy config - Dockerfile.all-in-one: self-contained multi-stage build (frontend, Rust native cross-compile with Debian cross-GCC, server bundle, prod deps, runtime) for linux/amd64 + linux/arm64 - Fix aws-lc-sys asm build on arm64: use gcc-aarch64-linux-gnu instead of napi-rs cross-toolchain (bundled GCC 4.8.5 lacks __ARM_ARCH) - Reduce memory via CARGO_PROFILE_RELEASE_LTO=thin + codegen-units=8 - Runtime mirrors upstream layout incl. schema.prisma/migrations so the selfhost predeploy job works; installs openssl + libjemalloc2 - scripts/deploy-gitea.sh + .gitea-deploy: build/push to Gitea registry - .dockerignore: exclude dist/node_modules/tsbuildinfo from context --- .dockerignore | 3 + .gitea-deploy | 4 + Dockerfile.all-in-one | 173 ++++++++++++++++++++++++++++++++++++ scripts/deploy-gitea.sh | 191 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 371 insertions(+) create mode 100644 .gitea-deploy create mode 100644 Dockerfile.all-in-one create mode 100755 scripts/deploy-gitea.sh diff --git a/.dockerignore b/.dockerignore index 5cfbb2f9b..46dd1f7cc 100644 --- a/.dockerignore +++ b/.dockerignore @@ -11,6 +11,9 @@ /.yarn/unplugged /.yarn/install-state.gz /.pnp.* +**/dist +**/node_modules +**/*.tsbuildinfo # Test artifacts /test-results diff --git a/.gitea-deploy b/.gitea-deploy new file mode 100644 index 000000000..77cf9b6ae --- /dev/null +++ b/.gitea-deploy @@ -0,0 +1,4 @@ +DOMAIN="gitea.luulam.dev" +REPO_PATH="luulam/affine" +PLATFORMS="linux/amd64,linux/arm64" +DOCKERFILE="./Dockerfile.all-in-one" diff --git a/Dockerfile.all-in-one b/Dockerfile.all-in-one new file mode 100644 index 000000000..f9a44d62a --- /dev/null +++ b/Dockerfile.all-in-one @@ -0,0 +1,173 @@ +# syntax=docker/dockerfile:1.7 +# ============================================================================= +# AFFiNE all-in-one Docker build (self-contained, no prebuilt artifacts needed) +# +# Stages: +# 1. frontend - builds @affine/web + @affine/admin + @affine/mobile dists +# 2. native - cross-compiles @affine/server-native (Rust) per TARGETARCH +# 3. server-dist - bundles @affine/server (rspack) -> dist/main.js +# 4. deps - installs production node_modules (multi-arch aware) +# 5. runtime - assembles final image, mirrors .github/deployment/node/Dockerfile +# +# Build: docker buildx build --platform linux/amd64,linux/arm64 -f Dockerfile.all-in-one . +# ============================================================================= + +# --- Shared base args --------------------------------------------------------- +FROM node:22-bookworm-slim AS base +RUN apt-get update && \ + apt-get install -y --no-install-recommends jq python3 make g++ && \ + rm -rf /var/lib/apt/lists/* +WORKDIR /app + +# Copy everything the yarn install / builds need (respects .dockerignore). +COPY . /app/ + +# Yarn 4 via corepack; keep scripts off like CI (.yarnrc.yml enableScripts=false) +RUN corepack enable && corepack prepare yarn@4.18.0 --activate + +# Full dependency install for build stages. +# Prisma postinstall generates client for the BUILD platform only; the runtime +# stage re-generates it for the target platform (see `deps` stage). +RUN yarn install --immutable + +# ============================================================================= +# Stage 1: frontend dists (web + admin + mobile) +# ============================================================================= +FROM base AS frontend + +# Self-hosted bundle: force publicPath '/' so assets are served from this +# container instead of affineassets CDN (html-plugin getPublicPath()). +ENV BUILD_TYPE=stable \ + PUBLIC_PATH=/ \ + NODE_ENV=production \ + GITHUB_SHA=dockerselfhost + +RUN yarn affine @affine/web build && \ + yarn affine @affine/admin build && \ + yarn affine @affine/mobile build + +# ============================================================================= +# Stage 2: Rust native module (@affine/server-native), per target arch +# ============================================================================= +FROM base AS native +ARG TARGETARCH + +# Rust toolchain (minimal profile; extra targets added per-arch below). +RUN apt-get update && \ + apt-get install -y --no-install-recommends curl ca-certificates && \ + rm -rf /var/lib/apt/lists/* + +ENV RUSTUP_HOME=/usr/local/rustup \ + CARGO_HOME=/usr/local/cargo \ + PATH=/usr/local/cargo/bin:${PATH} +RUN curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain 1.97.1 --profile minimal && \ + cargo --version && rustc --version +ENV CARGO_PROFILE_RELEASE_LTO=thin \ + CARGO_PROFILE_RELEASE_CODEGEN_UNITS=8 \ + CARGO_PROFILE_RELEASE_OPT_LEVEL=3 \ + CARGO_PROFILE_RELEASE_STRIP=symbols + + +WORKDIR /app/packages/backend/native + +# Cross-compile with Debian's cross-GCC instead of napi-rs' bundled GCC 4.8.5: +# gcc 4.8.5 does not define __ARM_ARCH for aarch64, breaking aws-lc-sys asm. +RUN if [ "$TARGETARCH" = "arm64" ]; then \ + apt-get update && \ + apt-get install -y --no-install-recommends \ + gcc-aarch64-linux-gnu g++-aarch64-linux-gnu libc6-dev-arm64-cross && \ + rm -rf /var/lib/apt/lists/* && \ + rustup target add aarch64-unknown-linux-gnu && \ + CC_aarch64_unknown_linux_gnu=aarch64-linux-gnu-gcc \ + CXX_aarch64_unknown_linux_gnu=aarch64-linux-gnu-g++ \ + AR_aarch64_unknown_linux_gnu=aarch64-linux-gnu-ar \ + CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc \ + yarn build --target aarch64-unknown-linux-gnu && \ + # index.js resolves arch-specific names; rspack bundles all of them. + cp server-native.node server-native.arm64.node && \ + cp server-native.node server-native.x64.node && \ + cp server-native.node server-native.armv7.node; \ + else \ + yarn build --target x86_64-unknown-linux-gnu && \ + # Only server-native.node is present from napi; provide arch aliases so + # rspack can resolve index.js's arch-specific requires (docker-clean + # prunes the wrong-arch binary at runtime anyway). + cp server-native.node server-native.x64.node && \ + cp server-native.node server-native.arm64.node && \ + cp server-native.node server-native.armv7.node; \ + fi && \ + ls -la server-native*.node + +# ============================================================================= +# Stage 3: server bundle (dist/main.js) +# ============================================================================= +FROM base AS server-dist +COPY --from=native /app/packages/backend/native/*.node /app/packages/backend/native/ + +RUN yarn workspace @affine/server build + +# ============================================================================= +# Stage 4: production node_modules (per target arch) +# Reuse base (full install already done & cached), then prune to server prod +# deps. supportedArchitectures covers both cpus so optional native deps for +# either arch get fetched. +# ============================================================================= +FROM base AS deps +ARG TARGETARCH + +RUN yarn config set --json supportedArchitectures.cpu "[\"x64\", \"arm64\"]" && \ + yarn config set --json supportedArchitectures.libc "[\"glibc\"]" + +# Re-install with both cpu targets so the image works on amd64+arm64 builds. +RUN yarn install --immutable && \ + yarn workspaces focus @affine/server @types/affine__env --production + +# Regenerate prisma client engines for this build platform's node_modules +RUN cd packages/backend/server && yarn prisma generate + +# Move to server dir layout expected by docker-clean.mjs & runtime stage +RUN mv /app/node_modules /app/packages/backend/server/node_modules + +# ============================================================================= +# Stage 5: runtime — mirror .github/deployment/node/Dockerfile layout +# ============================================================================= +FROM node:22-bookworm-slim AS runtime +ARG TARGETARCH +WORKDIR /app + +COPY --from=deps /app/packages/backend/server/node_modules /app/node_modules +COPY --from=server-dist /app/packages/backend/server/dist /app/dist +COPY --from=server-dist /app/packages/backend/server/scripts /app/scripts +COPY --from=frontend /app/packages/frontend/apps/web/dist /app/static +COPY --from=frontend /app/packages/frontend/admin/dist /app/static/admin +COPY --from=frontend /app/packages/frontend/apps/mobile/dist /app/static/mobile +# Mirror upstream layout: /app IS the server package dir — the selfhost +# predeploy job runs `yarn prisma migrate deploy` + `yarn cli` from here. +COPY --from=base /app/packages/backend/server/package.json /app/package.json +COPY --from=base /app/packages/backend/server/schema.prisma /app/schema.prisma +COPY --from=base /app/packages/backend/server/migrations /app/migrations + +# Upstream installs these in the final image (jemalloc for ENV below, +# openssl for prisma engines). +RUN apt-get update && \ + apt-get install -y --no-install-recommends openssl libjemalloc2 && \ + rm -rf /var/lib/apt/lists/* + + +# Native binding for this target arch. node-loader emitted +# `require('./server-native.node')` inside dist/main.js — the binary must sit +# next to it. +COPY --from=native /app/packages/backend/native/server-native.node /app/dist/server-native.node + +ENV LD_PRELOAD=libjemalloc.so.2 + + +# Same pruning as upstream: sourcemaps, wrong-arch natives/prisma engines, +# dedupe static files. +RUN AFFINE_DOCKER_CLEAN=1 \ + TARGETARCH="$( [ "$TARGETARCH" = "amd64" ] && echo amd64 || echo arm64 )" \ + node ./scripts/docker-clean.mjs + +EXPOSE 3010 + +CMD ["node", "./dist/main.js"] diff --git a/scripts/deploy-gitea.sh b/scripts/deploy-gitea.sh new file mode 100755 index 000000000..42c649210 --- /dev/null +++ b/scripts/deploy-gitea.sh @@ -0,0 +1,191 @@ +#!/bin/bash +set -e + +# ============================================================================= +# Gitea Docker Deploy Script +# Build multi-platform Docker image and push to Gitea container registry +# ============================================================================= + +# --- Load config from file if exists --- +CONFIG_FILE=".gitea-deploy" +if [[ -f "$CONFIG_FILE" ]]; then + source "$CONFIG_FILE" +fi + +# --- Configuration --- +# Priority: 1) environment variables, 2) .gitea-deploy file, 3) auto-detect +DOMAIN="${GITEA_DOMAIN:-$DOMAIN}" +REPO_PATH="${GITEA_REPO:-$REPO_PATH}" +PLATFORMS="${DOCKER_PLATFORMS:-${PLATFORMS:-linux/amd64,linux/arm64}}" +DOCKERFILE="${DOCKERFILE:-./Dockerfile}" + +# --- Output colors --- +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +log_info() { echo -e "${GREEN}[INFO]${NC} $1"; } +log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } +log_error() { echo -e "${RED}[ERROR]${NC} $1"; } + +# --- Auto-detect from git remote --- +detect_gitea_info() { + local remote_url + local remote_name="${GIT_REMOTE:-}" + + # Priority: 1) GIT_REMOTE env, 2) remote "gitea", 3) remote "origin" + if [[ -n "$remote_name" ]]; then + remote_url=$(git remote get-url "$remote_name" 2>/dev/null || echo "") + elif git remote get-url gitea &>/dev/null; then + remote_name="gitea" + remote_url=$(git remote get-url gitea) + log_info "Using remote 'gitea'" + else + remote_name="origin" + remote_url=$(git remote get-url origin 2>/dev/null || echo "") + fi + + if [[ -z "$remote_url" ]]; then + log_error "Git remote 'origin' not found" + exit 1 + fi + + # Parse URL: support multiple formats + # SSH short: git@gitea.luulam.dev:luulam/9router.git + # SSH full: ssh://git@192.168.1.10:2222/luulam/9router.git + # HTTPS: https://gitea.luulam.dev/luulam/9router.git + + local detected_domain="" + local detected_repo="" + + if [[ "$remote_url" =~ ^ssh:// ]]; then + # SSH full format: ssh://git@host:port/path.git + detected_domain=$(echo "$remote_url" | sed 's|ssh://[^@]*@\([^:/]*\).*|\1|') + detected_repo=$(echo "$remote_url" | sed 's|ssh://[^/]*/\(.*\)\.git|\1|' | sed 's|\.git$||') + elif [[ "$remote_url" =~ ^git@ ]]; then + # SSH short format: git@host:path.git + detected_domain=$(echo "$remote_url" | sed 's/git@\([^:]*\):.*/\1/') + detected_repo=$(echo "$remote_url" | sed 's/git@[^:]*:\(.*\)\.git/\1/' | sed 's/\.git$//') + elif [[ "$remote_url" =~ ^https?:// ]]; then + # HTTPS format + detected_domain=$(echo "$remote_url" | sed 's|https\?://\([^/]*\)/.*|\1|') + detected_repo=$(echo "$remote_url" | sed 's|https\?://[^/]*/\(.*\)\.git|\1|' | sed 's/\.git$//') + else + log_error "Unrecognized git remote format: $remote_url" + exit 1 + fi + + # Use detected values if not configured + REPO_PATH="${REPO_PATH:-$detected_repo}" + + # Check if domain is IP -> need to configure DOMAIN + if [[ -z "$DOMAIN" ]]; then + if [[ "$detected_domain" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + log_error "Git remote points to IP ($detected_domain), not domain." + log_error "Please create .gitea-deploy file with:" + echo "" + echo " DOMAIN=\"gitea.luulam.dev\"" + echo " REPO_PATH=\"$detected_repo\"" + echo "" + exit 1 + fi + DOMAIN="$detected_domain" + fi +} + +# --- Check prerequisites --- +check_prerequisites() { + log_info "Checking prerequisites..." + + # Dockerfile + if [[ ! -f "$DOCKERFILE" ]]; then + log_error "$DOCKERFILE not found" + exit 1 + fi + + # Docker daemon + if ! docker info > /dev/null 2>&1; then + log_error "Docker daemon not running. Please start Docker Desktop." + exit 1 + fi + + # Docker buildx + if ! docker buildx version > /dev/null 2>&1; then + log_error "Docker Buildx not installed." + log_info "Install with: docker buildx install" + exit 1 + fi + + # Check if builder supports multi-platform + local builder_platforms + builder_platforms=$(docker buildx inspect --bootstrap 2>/dev/null | grep -i "platforms" || echo "") + if [[ -z "$builder_platforms" ]]; then + log_warn "Creating new multi-platform builder..." + docker buildx create --use --name multiarch-builder --driver docker-container --bootstrap + fi + + log_info "All prerequisites ready!" +} + +# --- Build and Push --- +build_and_push() { + local git_rev + git_rev=$(git rev-parse --short HEAD) + + local image_base="$DOMAIN/$REPO_PATH" + local tag_rev="$image_base:$git_rev" + local tag_latest="$image_base:latest" + + echo "" + log_info "==========================================" + log_info "Build Docker Image" + log_info "==========================================" + log_info "Registry: $DOMAIN" + log_info "Repository: $REPO_PATH" + log_info "Platforms: $PLATFORMS" + log_info "Tags: $git_rev, latest" + log_info "Dockerfile: $DOCKERFILE" + echo "" + + # Build and push + docker buildx build \ + --platform "$PLATFORMS" \ + --tag "$tag_rev" \ + --tag "$tag_latest" \ + --file "$DOCKERFILE" \ + --push . + + if [[ $? -eq 0 ]]; then + echo "" + log_info "==========================================" + log_info "Deploy successful!" + log_info "==========================================" + log_info "Image: $tag_rev" + log_info "Image: $tag_latest" + echo "" + log_info "Pull image:" + echo " docker pull $tag_latest" + echo "" + else + log_error "Build failed!" + exit 1 + fi +} + +# --- Main --- +main() { + echo "" + log_info "Gitea Docker Deploy" + echo "" + + # Detect info if not configured + if [[ -z "$DOMAIN" ]] || [[ -z "$REPO_PATH" ]]; then + detect_gitea_info + fi + + check_prerequisites + build_and_push +} + +main "$@"