Ctrl K
ComunidadGuías…

Dockerfile

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# Open ACE - AI Computing Explorer
# Docker build for production deployment
#
# Frontend is built inside Docker (Issue #1260: one-click deploy support)
#
# Base image registry override (for networks where Docker Hub is unreachable,
# e.g. mainland China). Pass at build time:
#   docker compose build --build-arg BASE_REGISTRY=docker.m.daocloud.io
# or export BASE_REGISTRY=docker.m.daocloud.io before `docker compose up -d --build`
# (docker-compose.yml forwards it as a build arg). Defaults to docker.io.
ARG BASE_REGISTRY=docker.io

# =============================================================================
# Frontend Build Stage (Issue #1260)
# =============================================================================
FROM ${BASE_REGISTRY}/node:20-alpine AS frontend-builder

WORKDIR /app/frontend

# Configure npm mirror for Chinese network
RUN npm config set registry https://registry.npmmirror.com/

# Copy frontend source
COPY frontend/package.json frontend/package-lock.json* ./

# Install dependencies
RUN npm ci --legacy-peer-deps || npm install --legacy-peer-deps

# Copy frontend source files
COPY frontend/ .

# Build frontend (outputs to ../static/js/dist/). Declare the build identity
# here, not at the top of the stage: every RUN after an ARG sees it, so a
# per-commit value would otherwise bust the cache for `npm ci` as well.
ARG OPENACE_VERSION
ARG GIT_COMMIT_SHA
RUN npm run build

# =============================================================================
# Python Build Stage
# =============================================================================
ARG BASE_REGISTRY=docker.io
FROM ${BASE_REGISTRY}/python:3.11-slim AS builder

WORKDIR /app

# Install build dependencies (using Chinese mirror)
RUN echo "deb https://mirrors.aliyun.com/debian/ trixie main" > /etc/apt/sources.list && \
    echo "deb https://mirrors.aliyun.com/debian/ trixie-updates main" >> /etc/apt/sources.list && \
    echo "deb https://mirrors.aliyun.com/debian-security trixie-security main" >> /etc/apt/sources.list && \
    rm -f /etc/apt/sources.list.d/debian.sources && \
    apt-get update && apt-get install -y --no-install-recommends \
    gcc \
    libpq-dev \
    && rm -rf /var/lib/apt/lists/*

# Copy dependency files. requirements.lock is a hash-pinned lock compiled from
# requirements.txt and resolved against the SAME aliyun mirror used below, so
# every pin is installable here (PyPI-latest pins can lag on the mirror).
# Regenerate with scripts/gen_requirements_lock.sh (kept honest by the
# check-requirements-lock-sync pre-commit hook).
COPY requirements.lock .

# Create virtual environment and install dependencies
RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
RUN pip install --no-cache-dir --upgrade pip -i https://mirrors.aliyun.com/pypi/simple/ && \
    pip install --no-cache-dir --require-hashes -r requirements.lock -i https://mirrors.aliyun.com/pypi/simple/
# =============================================================================
# Production Stage
# =============================================================================
ARG BASE_REGISTRY=docker.io
FROM ${BASE_REGISTRY}/python:3.11-slim AS production

# Labels for container metadata (version/revision are set at the end of this
# stage so their per-commit build args do not invalidate the layers below)
LABEL maintainer="Open ACE Team"
LABEL description="AI Computing Explorer"

# Install runtime dependencies + Node.js 20 + qwen-code-webui for multi-user workspace
RUN echo "deb https://mirrors.aliyun.com/debian/ trixie main" > /etc/apt/sources.list && \
    echo "deb https://mirrors.aliyun.com/debian/ trixie-updates main" >> /etc/apt/sources.list && \
    echo "deb https://mirrors.aliyun.com/debian-security trixie-security main" >> /etc/apt/sources.list && \
    rm -f /etc/apt/sources.list.d/debian.sources && \
    apt-get update && apt-get install -y --no-install-recommends \
    libpq5 \
    postgresql-client \
    curl \
    sudo \
    ca-certificates \
    gnupg \
    procps \
    util-linux \
    openssh-client \
    sshpass \
    git \
    acl \
    && curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \
    && apt-get install -y --no-install-recommends nodejs \
    # === Node.js Installation Verification (Issue #1006) ===
    && test -x /usr/bin/node \
    && node --version | grep -q '^v22\.' \
    # Create symlink backup for node (prevent accidental removal, skip if already same file)
    && sh -c 'test -e /bin/node || ln -sf /usr/bin/node /bin/node' \
    # === Process Tools Verification (Issue #1050) ===
    && test -x /usr/bin/ps \
    && ps --version >/dev/null \
    # === npm and CLI Setup ===
    && npm config set registry https://registry.npmmirror.com/ \
    && npm install -g qwen-code-webui@0.3.0 @qwen-code/qwen-code@0.23.3 \
    # === code-server Installation (for local workspace VS Code button) ===
    # NOTE: On Debian, the install script uses deb package which ignores --prefix
    # and always installs to /usr/bin/code-server
    && curl -fsSL --connect-timeout 15 --max-time 300 https://code-server.dev/install.sh | sh -s -- \
    && test -x /usr/bin/code-server \
    # === CLI Verification ===
    && test -f /usr/lib/node_modules/@qwen-code/qwen-code/cli.js \
    && test -x /usr/bin/qwen-code-webui \
    # === Git Verification ===
    && test -x /usr/bin/git \
    && git --version \
    # === GitHub CLI Installation (deb package with fallback) ===
    && (curl -fsSL --connect-timeout 30 --max-time 60 -o /tmp/gh.deb https://github.com/cli/cli/releases/download/v2.42.1/gh_2.42.1_linux_amd64.deb \
        && apt-get install -y /tmp/gh.deb \
        && rm -f /tmp/gh.deb \
        && echo "gh CLI installed from deb package" \
        || (echo "deb package download failed, trying GitHub apt repository..." \
            && curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg \
            && chmod go+r /usr/share/keyrings/githubcli-archive-keyring.gpg \
            && echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | tee /etc/apt/sources.list.d/github-cli.list > /dev/null \
            && apt-get update \
            && apt-get install -y gh \
            && rm -f /etc/apt/sources.list.d/github-cli.list \
            && rm -f /usr/share/keyrings/githubcli-archive-keyring.gpg \
            && apt-get clean \
            && echo "gh CLI installed from apt repository")) \
    # === GitHub CLI Verification ===
    && test -x /usr/bin/gh \
    && gh --version \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/* /tmp/* /root/.npm

# Create non-root user for security. Keep uid/gid stable so Kubernetes
# runAsUser/runAsGroup can match the filesystem ownership baked into the image.
# Pre-create /home/open-ace/.open-ace (uid 1000) so the entrypoint's uid-aware
# default config dir is writable as soon as the container starts, including
# when docker-compose mounts the `config-data` named volume there: Docker's
# named-volume init copies existing uid-1000 ownership into the volume on first
# run, so `mkdir -p`/config generation won't hit Permission denied under uid 1000.
#
# /var/lib/openace (#3237) is pre-created for exactly the same reason and must
# stay that way. docker-compose mounts the `agent-state` named volume there,
# and Docker only copies uid-1000 ownership into a fresh volume if the image
# already has a directory at the mount point — otherwise it creates the volume
# root:root 0755, uid 1000 cannot mkdir inside it, every carried transcript is
# logged-and-discarded, and every sandbox turn silently starts cold. The mount
# is the PARENT: the state root itself is created at runtime by uid 1000 so it
# is owned by the process that has to chmod it.
RUN groupadd -g 1000 open-ace && \
    useradd -u 1000 -g open-ace -d /home/open-ace -s /bin/bash -c "Open ACE user" open-ace && \
    useradd --system --create-home --home-dir /var/lib/openace-agent \
        --shell /usr/sbin/nologin openace-agent && \
    mkdir -p /home/open-ace/.open-ace && \
    chown -R open-ace:open-ace /home/open-ace && \
    mkdir -p /var/lib/openace && \
    chown open-ace:open-ace /var/lib/openace

# ============================================================================
# 【安全加固 Issue #1514】单一配置源原则
# ============================================================================
# Dockerfile不生成sudoers.d文件,避免与entrypoint.sh动态配置冲突
# sudoers配置由entrypoint.sh在启动时动态生成(包含精确参数白名单)
# wrapper脚本在构建时COPY,由entrypoint.sh在启动时验证并添加sudoers规则

# Pre-configure sudoers comment for multi-user workspace mode
# This ensures environment variables are preserved when running sudo -u <user>
# Support both open-ace (container user) and openace (workspace user synced from database)
# NOTE: sudoers配置由entrypoint.sh动态生成,不在此处静态配置
# Issue #1395: openace-run-as wrapper在构建时COPY,启动时验证

# Wrapper脚本将在启动时由entrypoint.sh验证存在性,并动态添加sudoers规则
# 如wrapper不存在,entrypoint.sh会跳过wrapper规则并WARNING日志

WORKDIR /app

# Copy virtual environment from builder
COPY --from=builder /opt/venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"

# Copy application code
COPY --chown=open-ace:open-ace . .

# Copy frontend build output from frontend-builder stage (Issue #1260)
COPY --from=frontend-builder --chown=open-ace:open-ace /app/static/js/dist ./static/js/dist

# Copy and set up entrypoint script
# Use python to convert Windows line endings (CRLF) to Unix (LF) and remove BOM - Issue #1988
COPY --chown=open-ace:open-ace docker-entrypoint.sh /usr/local/bin/
RUN python3 -c "with open('/usr/local/bin/docker-entrypoint.sh','rb') as f: c=f.read(); c=c[3:] if c.startswith(b'\xef\xbb\xbf') else c; c=c.replace(b'\r\n',b'\n'); open('/usr/local/bin/docker-entrypoint.sh','wb').write(c)" && chmod +x /usr/local/bin/docker-entrypoint.sh

# Install cross-user agent launcher wrapper (Issue #1395)
# Lets the autonomous agent launch CLIs with cwd=<repo> under a 700 home dir by
# chdir'ing as root then runuser -u <owner>. Owned by root so the sudoers rule
# (ALL=(root) NOPASSWD) above can apply.
COPY scripts/openace-run-as.sh /usr/local/bin/openace-run-as
COPY scripts/openace-validate-launch /usr/local/libexec/openace-validate-launch
COPY app/modules/workspace/autonomous/agent_bin/ /usr/local/libexec/openace-agent-bin/
RUN chmod 755 /usr/local/bin/openace-run-as \
        /usr/local/libexec/openace-validate-launch \
        /usr/local/libexec/openace-agent-bin/_guard_exec.py \
        /usr/local/libexec/openace-agent-bin/git \
        /usr/local/libexec/openace-agent-bin/gh \
        /usr/local/libexec/openace-agent-bin/python \
        /usr/local/libexec/openace-agent-bin/python3 \
        /usr/local/libexec/openace-agent-bin/pytest && \
    chown root:root /usr/local/bin/openace-run-as \
        /usr/local/libexec/openace-validate-launch && \
    chown -R root:root /usr/local/libexec/openace-agent-bin && \
    mkdir -p /etc/openace && \
    printf '%s\n' \
        '# Issue #2018: constraints shared by openace-run-as and its sudoers' \
        '# rule. root:root 0644 — the openace service account can read but not edit it.' \
        'OPENACE_AUTONOMOUS_AGENT_ACCOUNT="openace-agent"' \
        'ALLOWED_WORKSPACE_ROOTS="/home /workspace"' \
        '# Issue #2020: per-task isolation + resource policy (read by the' \
        '# launcher and autonomous_scheduler). memory/pids/cpu default to 0' \
        '# (off) — set them to enforce; cgroup_enabled=auto detects a writable' \
        '# cgroup v2 hierarchy and otherwise falls back to prlimit.' \
        'agent_task_root=/run/openace-agent-tasks' \
        'agent_task_cgroup_root=/sys/fs/cgroup/openace-agent' \
        'agent_task_cgroup_enabled=auto' \
        'agent_task_memory_max_bytes=0' \
        'agent_task_pids_max=0' \
        'agent_task_cpu_max=' \
        'agent_max_concurrent_workflows=3' \
        > /etc/openace/agent-launcher.conf && \
    chown root:root /etc/openace/agent-launcher.conf && \
    chmod 0644 /etc/openace/agent-launcher.conf && \
    printf '%s\n' \
        '# Credentialless autonomous agent launcher' \
        'open-ace ALL=(root) NOPASSWD: /usr/local/bin/openace-run-as --isolated *' \
        > /etc/sudoers.d/open-ace-autonomous-agent && \
    chmod 440 /etc/sudoers.d/open-ace-autonomous-agent && \
    visudo -c -f /etc/sudoers.d/open-ace-autonomous-agent

# Install security wrappers for multi-user mode (Issue #1855 + #2181)
# These wrappers enforce path validation, UID range checks, and audit logging
# for privileged operations like chown, useradd, cat, mkdir, rm.
COPY scripts/openace-chown.sh /usr/local/bin/openace-chown
COPY scripts/openace-useradd.sh /usr/local/bin/openace-useradd
COPY scripts/openace-cat.sh /usr/local/bin/openace-cat
COPY scripts/openace-mkdir.sh /usr/local/bin/openace-mkdir
COPY scripts/openace-rm.sh /usr/local/bin/openace-rm
COPY scripts/openace-write-as.sh /usr/local/bin/openace-write-as
COPY scripts/openace-restore-sudoers.sh /usr/local/bin/openace-restore-sudoers
COPY scripts/openace-webui-launch.sh /usr/local/bin/openace-webui-launch
COPY scripts/openace-git.py /usr/local/bin/openace-git
COPY scripts/openace-gh.py /usr/local/bin/openace-gh
COPY config/openace/git-wrapper.json /etc/openace/git-wrapper.json
COPY config/openace/gh-wrapper.json /etc/openace/gh-wrapper.json
RUN chmod 755 /usr/local/bin/openace-chown /usr/local/bin/openace-useradd \
             /usr/local/bin/openace-cat /usr/local/bin/openace-mkdir \
             /usr/local/bin/openace-rm /usr/local/bin/openace-write-as \
             /usr/local/bin/openace-restore-sudoers \
             /usr/local/bin/openace-webui-launch \
             /usr/local/bin/openace-git /usr/local/bin/openace-gh && \
    chown root:root /usr/local/bin/openace-chown /usr/local/bin/openace-useradd \
                    /usr/local/bin/openace-cat /usr/local/bin/openace-mkdir \
                    /usr/local/bin/openace-rm /usr/local/bin/openace-write-as \
                    /usr/local/bin/openace-restore-sudoers \
                    /usr/local/bin/openace-webui-launch \
                    /usr/local/bin/openace-git /usr/local/bin/openace-gh \
                    /etc/openace/git-wrapper.json /etc/openace/gh-wrapper.json && \
    chmod 0644 /etc/openace/git-wrapper.json /etc/openace/gh-wrapper.json && \
    mkdir -p /var/lock && chmod 1777 /var/lock && \
    mkdir -p /app/logs && chown open-ace:open-ace /app/logs

# /app itself stays root-owned (WORKDIR creates it before USER switching, and
# COPY --chown only affects copied contents), while docker-entrypoint.sh's
# `mkdir -p /app/logs` (Issue #1205) runs as uid 1000 before the command
# dispatch. CI build contexts never contain a gitignored logs/ directory, so
# the image must pre-create it open-ace-owned or every non-root boot fails.

# NOTE: The image defaults to the non-root open-ace user (uid 1000) so that
# `docker run`, docker-compose, and Kubernetes all execute the entrypoint as
# uid 1000 without relying solely on the K8s manifest's securityContext.
# This matches the stable uid/gid 1000 created above and the COPY --chown
# ownership baked into the image.
#
# SECURITY CONSIDERATIONS:
# - Single-user mode (the default) needs only uid 1000 and runs non-root here.
# - Multi-user workspace mode genuinely needs root (it runs useradd/chown and
#   sudo -u across /home). For those deployments, opt back into root explicitly:
#   set `--user 0` (docker run) / `runAsUser: 0` (manifest) AND the env
#   OPENACE_ALLOW_ROOT_MULTI_USER=1. docker-entrypoint.sh fail-fasts otherwise.
# - sudoers is configured to allow open-ace user to run specific commands only
# - In production, use network isolation and limit container privileges
# - Refer to docker-entrypoint.sh for sudoers configuration details
USER 1000

# Environment variables (Issue #1192: add LANG/LC_ALL for Unicode support)
ENV PYTHONUNBUFFERED=1 \
    PYTHONDONTWRITEBYTECODE=1 \
    FLASK_APP=server.py \
    FLASK_ENV=production \
    LANG=C.UTF-8 \
    LC_ALL=C.UTF-8 \
    OPENACE_INSTALL_METHOD=docker

# Expose port
EXPOSE 19888

# Health check (Issue #2186: uses /readyz for actual health)
HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \
    CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:19888/readyz')" || exit 1

# Run the application
# Use bash to execute the script to ensure proper shell interpretation (Issue #1988)
ENTRYPOINT ["/bin/bash", "/usr/local/bin/docker-entrypoint.sh"]

# Build identity, declared last so a new commit only changes image metadata.
ARG OPENACE_VERSION=dev
ARG GIT_COMMIT_SHA=unknown
LABEL org.opencontainers.image.version="${OPENACE_VERSION}"
LABEL org.opencontainers.image.revision="${GIT_COMMIT_SHA}"

# =============================================================================
# Development Stage (optional)
# =============================================================================
FROM production AS development

# The development target installs extra tools into /opt/venv (root-owned by
# the builder stage), so it opts back into root for the build. The default
# `open-ace:latest` image stays non-root.
USER root

# Install development tools
RUN pip install --no-cache-dir -i https://mirrors.aliyun.com/pypi/simple/ \
    pytest \
    pytest-cov \
    pytest-asyncio \
    black \
    isort \
    ruff \
    mypy

# Development environment
ENV FLASK_ENV=development \
    FLASK_DEBUG=1

# Run with auto-reload
CMD ["python", "server.py"]

# =============================================================================
# Migration Stage (for running database migrations)
# =============================================================================
FROM production AS migration

# The migration target is a one-shot init Job (alembic upgrade), not the
# long-running web image, so it opts back into root to write /entrypoint.sh
# under / (owned by root). The default `open-ace:latest` image stays non-root.
USER root

# Create migration entrypoint script
RUN echo '#!/bin/bash\n\
set -e\n\
\n\
# Wait for database to be ready\n\
if [ -n "$DATABASE_URL" ]; then\n\
    echo "Waiting for database..."\n\
    sleep 5\n\
    \n\
    # Run migrations\n\
    echo "Running database migrations..."\n\
    alembic upgrade head\n\
    echo "Migrations completed successfully"\n\
fi\n\
\n\
# Start the application\n\
exec python server.py' > /entrypoint.sh && chmod +x /entrypoint.sh

ENTRYPOINT ["/entrypoint.sh"]