Dockerfile
Rama main · réplica de lectura del repositorio original.
# 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"]