# ============================================================================= # ZhikunCode — Multi-stage Production Docker Build # Architecture: Single container (Java manages Python as subprocess) # ============================================================================= # ---- Stage 1: Build Frontend ---- FROM node:22-alpine AS frontend-build WORKDIR /build/frontend ARG NPM_REGISTRY=https://registry.npmjs.org/ COPY frontend/package.json frontend/package-lock.json ./ RUN case "${NPM_REGISTRY}" in https://*) ;; \ *) echo "NPM_REGISTRY must use HTTPS" >&2; exit 2 ;; \ esac && \ npm ci --ignore-scripts \ --registry="${NPM_REGISTRY}" \ --replace-registry-host=always COPY frontend/src ./src/ COPY frontend/index.html frontend/vite.config.ts frontend/tsconfig.json ./ COPY frontend/tsconfig.node.json frontend/postcss.config.js frontend/tailwind.config.ts ./ COPY frontend/.env.production ./.env.production RUN npm run build # ---- Stage 2: Build Backend ---- FROM eclipse-temurin:21-jdk AS backend-build WORKDIR /build ARG MAVEN_REPOSITORY_URL=https://repo.maven.apache.org/maven2 # Cache Maven dependencies (layer caching optimization) COPY backend/pom.xml ./backend/ COPY backend/.mvn ./backend/.mvn/ COPY backend/mvnw ./backend/ RUN repository="${MAVEN_REPOSITORY_URL%/}" && \ case "$repository" in https://*) ;; \ *) echo "MAVEN_REPOSITORY_URL must use HTTPS" >&2; exit 2 ;; \ esac && \ repository_path="${repository#https://}" && \ case "$repository_path" in ""|*[!A-Za-z0-9._~:/-]*) \ echo "MAVEN_REPOSITORY_URL contains unsupported characters" >&2; exit 2 ;; \ esac && \ sed -i \ "s|https://repo.maven.apache.org/maven2|${repository}|g" \ backend/.mvn/wrapper/maven-wrapper.properties && \ grep -Fq "distributionUrl=${repository}/" \ backend/.mvn/wrapper/maven-wrapper.properties && \ grep -Fq "wrapperUrl=${repository}/" \ backend/.mvn/wrapper/maven-wrapper.properties && \ if [ "$repository" != 'https://repo.maven.apache.org/maven2' ]; then \ mkdir -p /root/.m2; \ printf '%s\n' \ '' \ '' \ ' ' \ ' ' \ ' docker-build-mirror' \ ' Configured build mirror' \ " ${repository}" \ ' *' \ ' ' \ ' ' \ '' \ > /root/.m2/settings.xml; \ fi RUN cd backend && chmod +x mvnw && ./mvnw dependency:go-offline -B # Build application JAR COPY backend/src ./backend/src/ RUN cd backend && ./mvnw package -DskipTests -B \ && mv target/ai-code-assistant-*.jar target/app.jar # Official GitHub MCP binary, pinned to a release tag. FROM ghcr.io/github/github-mcp-server:v1.11.0 AS github-mcp # Pinned Meoo CLI + Fliggy flyai CLI and glibc Node runtime (not the Alpine frontend build). FROM node:22.14.0-bookworm-slim AS meoo-cli ARG NPM_REGISTRY=https://registry.npmjs.org/ RUN case "${NPM_REGISTRY}" in https://*) ;; \ *) echo "NPM_REGISTRY must use HTTPS" >&2; exit 2 ;; \ esac && \ npm install --global @aliyun-meoo/cli@0.5.7 --ignore-scripts --no-audit --no-fund \ --registry="${NPM_REGISTRY}" \ --replace-registry-host=always && \ npm install --global @fly-ai/flyai-cli@1.0.16 --ignore-scripts --no-audit --no-fund \ --registry="${NPM_REGISTRY}" \ --replace-registry-host=always # ---- Stage 3: Production Runtime ---- # Ubuntu 24.04 (noble) provides Python 3.12, matching pyproject.toml's # supported range (>=3.11,<3.13). Jammy's Python 3.10 is not supported. FROM eclipse-temurin:21-jre-noble AS runtime ARG UBUNTU_MIRROR_HOST= ARG PIP_INDEX_URL=https://pypi.org/simple LABEL maintainer="ZhikunCode Team" LABEL org.opencontainers.image.title="ZhikunCode" LABEL org.opencontainers.image.description="AI-powered code assistant with multi-agent collaboration" LABEL org.opencontainers.image.vendor="ZhikunCode" LABEL org.opencontainers.image.source="https://github.com/zhikuncode/zhikuncode" # Install runtime dependencies: # - python3 + venv + libmagic: Python subprocess for analysis/file inspection # - ripgrep: GrepTool backend # - curl: healthcheck # - git: Git tools # - tree-sitter runtime handled by python venv RUN if [ -n "${UBUNTU_MIRROR_HOST}" ]; then \ case "${UBUNTU_MIRROR_HOST}" in *[!A-Za-z0-9.-]*) \ echo "UBUNTU_MIRROR_HOST must be a hostname" >&2; exit 2 ;; \ esac; \ mirror_replaced=false; \ for source_file in /etc/apt/sources.list /etc/apt/sources.list.d/ubuntu.sources; do \ if [ -f "$source_file" ] && grep -Eq 'https?://(archive|security|ports)\.ubuntu\.com' "$source_file"; then \ sed -E -i \ "s#https?://(archive\.ubuntu\.com|security\.ubuntu\.com|ports\.ubuntu\.com)#https://${UBUNTU_MIRROR_HOST}#g" \ "$source_file"; \ grep -Fq "https://${UBUNTU_MIRROR_HOST}/ubuntu" "$source_file" \ || { echo "Ubuntu mirror replacement verification failed" >&2; exit 2; }; \ mirror_replaced=true; \ fi; \ done; \ [ "$mirror_replaced" = true ] \ || { echo "No supported Ubuntu source entry found" >&2; exit 2; }; \ fi && \ apt-get update && \ apt-get install -y --no-install-recommends \ python3 python3-pip python3-venv libmagic1 \ ripgrep curl git zip && \ python3 -c 'import sys; assert (3, 11) <= sys.version_info[:2] < (3, 13), sys.version' && \ apt-get clean && \ rm -rf /var/lib/apt/lists/* # Preinstall the full capability set so sessions can render/convert without # runtime root. Split into two layers for Docker cache granularity: # layer 1: toolchain (gcc/python headers), media, docs, diagrams, OCR, CJK fonts # layer 2: LibreOffice office suite (heaviest; isolated so layer 1 stays cached) RUN apt-get update && \ apt-get install -y --no-install-recommends \ build-essential python3-dev \ ffmpeg imagemagick poppler-utils qpdf graphviz pandoc \ tesseract-ocr tesseract-ocr-chi-sim \ fonts-wqy-zenhei fonts-noto-cjk && \ apt-get clean && \ rm -rf /var/lib/apt/lists/* RUN apt-get update && \ apt-get install -y --no-install-recommends \ libreoffice-impress libreoffice-calc libreoffice-writer && \ apt-get clean && \ rm -rf /var/lib/apt/lists/* # Create non-root user RUN groupadd -r zhikun && useradd -r -g zhikun -d /app -s /bin/sh zhikun WORKDIR /app COPY --from=meoo-cli /usr/local/bin/node /usr/local/bin/node COPY --from=meoo-cli /usr/local/lib/node_modules/@aliyun-meoo /usr/local/lib/node_modules/@aliyun-meoo COPY --from=meoo-cli /usr/local/lib/node_modules/@fly-ai /usr/local/lib/node_modules/@fly-ai COPY --from=meoo-cli /usr/local/lib/node_modules/npm /usr/local/lib/node_modules/npm RUN ln -s /usr/local/lib/node_modules/@aliyun-meoo/cli/bin/meoo.js /usr/local/bin/meoo \ && ln -s /usr/local/lib/node_modules/@fly-ai/flyai-cli/dist/flyai-bundle.cjs /usr/local/bin/flyai \ && ln -sf /usr/local/lib/node_modules/npm/bin/npm-cli.js /usr/local/bin/npm \ && ln -sf /usr/local/lib/node_modules/npm/bin/npx-cli.js /usr/local/bin/npx \ && node --version && meoo --version && npm --version && flyai --help > /dev/null # Keep dependency layers independent of application source changes. COPY python-service/requirements.lock ./python-service/ COPY python-service/requirements.txt ./python-service/ COPY python-service/pyproject.toml ./python-service/ # Setup Python virtual environment ENV PLAYWRIGHT_BROWSERS_PATH=/opt/playwright-browsers RUN case "${PIP_INDEX_URL}" in https://*) ;; \ *) echo "PIP_INDEX_URL must use HTTPS" >&2; exit 2 ;; \ esac && \ python3 -m venv /app/python-service/.venv && \ /app/python-service/.venv/bin/pip install --no-cache-dir \ --index-url "${PIP_INDEX_URL}" pip==24.0 && \ /app/python-service/.venv/bin/pip install --no-cache-dir \ --index-url "${PIP_INDEX_URL}" \ -r /app/python-service/requirements.lock # The service runs headless. Cache system libraries separately from browser downloads. RUN /app/python-service/.venv/bin/python -m playwright install-deps chromium && \ rm -rf /var/lib/apt/lists/* RUN /app/python-service/.venv/bin/python -m playwright install --only-shell chromium && \ chmod -R a+rX /opt/playwright-browsers COPY --from=backend-build /build/backend/target/app.jar ./app.jar COPY --from=frontend-build /build/frontend/dist ./static/ # PythonProcessManager starts this service as a subprocess. COPY python-service/src ./python-service/src/ # Alibaba Cloud Ops has stricter FastMCP/Pydantic pins than the application, # so isolate it from the Python analysis service. RUN python3 -m venv /app/mcp-servers/alibaba-cloud-ops && \ /app/mcp-servers/alibaba-cloud-ops/bin/pip install --no-cache-dir \ --index-url "${PIP_INDEX_URL}" pip==24.0 && \ /app/mcp-servers/alibaba-cloud-ops/bin/pip install --no-cache-dir \ --index-url "${PIP_INDEX_URL}" \ alibaba-cloud-ops-mcp-server==0.9.27 # Create symlink so PythonProcessManager can resolve 'python' command RUN ln -sf /app/python-service/.venv/bin/python /usr/local/bin/python # External MCP runtimes and stable project launchers. COPY --from=github-mcp /server/github-mcp-server /usr/local/bin/github-mcp-server COPY scripts/mcp/zhikun-github-mcp scripts/mcp/zhikun-alibaba-cloud-ops-mcp /usr/local/bin/ RUN chmod +x /usr/local/bin/github-mcp-server \ /usr/local/bin/zhikun-github-mcp \ /usr/local/bin/zhikun-alibaba-cloud-ops-mcp # Copy MCP capability registry COPY configuration/ ./configuration/ # Copy entrypoint COPY docker-entrypoint.sh ./ RUN chmod +x docker-entrypoint.sh # Create data and log directories; pre-create the HOME-side .zhikun mountpoint # tree (owned by zhikun) so deploying a bind mount over /app/.zhikun/skills # never leaves /app/.zhikun root-owned — the app creates its uploads directory # under /app/.zhikun at startup and must keep write access there. RUN mkdir -p /app/data /app/workspace /app/log /app/log/debug /app/log/mcp \ /app/.zhikun/skills /app/.ai-code-assistant && \ chown -R zhikun:zhikun /app USER zhikun # ---- Environment defaults (overridable via docker-compose / .env) ---- ENV JAVA_OPTS="-Xms256m -Xmx1024m --enable-preview" \ SPRING_PROFILES_ACTIVE="production" \ PYTHON_SERVICE_PATH="/app/python-service" \ PYTHONPATH="/app/python-service/src" \ MCP_REGISTRY_PATH="/app/configuration/mcp/mcp_capability_registry.json" \ ALLOW_PRIVATE_NETWORK="true" \ LOG_DIR="/app/log" EXPOSE 8080 # Health check using curl (installed above) HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \ CMD curl -sf http://localhost:8080/actuator/health || exit 1 ENTRYPOINT ["./docker-entrypoint.sh"]