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
This commit is contained in:
2026-08-25 11:46:28 +07:00
parent 8e8a781ed1
commit ae2d0be2ea
4 changed files with 371 additions and 0 deletions

View File

@@ -11,6 +11,9 @@
/.yarn/unplugged
/.yarn/install-state.gz
/.pnp.*
**/dist
**/node_modules
**/*.tsbuildinfo
# Test artifacts
/test-results

4
.gitea-deploy Normal file
View File

@@ -0,0 +1,4 @@
DOMAIN="gitea.luulam.dev"
REPO_PATH="luulam/affine"
PLATFORMS="linux/amd64,linux/arm64"
DOCKERFILE="./Dockerfile.all-in-one"

173
Dockerfile.all-in-one Normal file
View File

@@ -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"]

191
scripts/deploy-gitea.sh Executable file
View File

@@ -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 "$@"