File size: 8,437 Bytes
ee9fcd0 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 | # syntax=docker/dockerfile:1.7
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# agent-canvas all-in-one Docker image
#
# Combines three services into a single image:
# 1. Agent Server β from ghcr.io/openhands/agent-server (upstream SDK image)
# 2. Automation β installed via pip from openhands-automation
# 3. Frontend β agent-canvas static build served by Node.js
#
# The entrypoint starts all three services and an ingress proxy that unifies
# them behind a single port (default 8000):
# /api/automation/* β automation backend (:18001)
# /api/*, /sockets β agent server (:18000)
# /* (default) β static frontend + SPA fallback
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# ββ Build args ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# No hardcoded defaults β values are derived from config/defaults.json.
# CI passes these via --build-arg; for local builds use:
# node scripts/docker-build.mjs (recommended, reads JSON for you)
# docker build --build-arg AGENT_SERVER_IMAGE=... --build-arg AUTOMATION_VERSION=... -f docker/Dockerfile .
ARG AGENT_SERVER_IMAGE
ARG AUTOMATION_VERSION
# ββ Stage 1: Build frontend ββββββββββββββββββββββββββββββββββββββββββββββββββ
FROM node:24-slim AS frontend-build
WORKDIR /build
# Cache-friendly: package files first
COPY package.json package-lock.json ./
RUN npm ci
# Copy everything needed for the build
COPY . .
# Build the static frontend.
# VITE_POSTHOG_API_KEY is a public client key baked into the bundle. CI passes
# production for tagged releases and staging for PR/main images.
# VITE_BASE_PATH is baked into React Router/Vite so the Docker image serves
# Canvas under the Cloud subpath instead of a dedicated hostname.
ARG VITE_POSTHOG_API_KEY=""
ARG VITE_BASE_PATH="/canvas"
ENV VITE_POSTHOG_API_KEY=${VITE_POSTHOG_API_KEY}
ENV VITE_BASE_PATH=${VITE_BASE_PATH}
RUN npm run build
# ββ Stage 1b: Generate shell-sourceable defaults from config/defaults.json ββ
# This avoids needing jq/python at container runtime to parse the JSON.
FROM node:24-slim AS config-gen
COPY config/defaults.json /tmp/
RUN node -e " \
const c = JSON.parse(require('fs').readFileSync('/tmp/defaults.json','utf-8')); \
const lines = [ \
'CONFIG_AGENT_SERVER_PORT=' + c.ports.agentServer, \
'CONFIG_AUTOMATION_PORT=' + c.ports.automation, \
'CONFIG_PROXY_PORT=' + c.ports.proxy, \
'CONFIG_VSCODE_PORT=' + c.ports.vscode, \
'CONFIG_STATE_SUBDIR=' + c.paths.stateSubdir, \
'CONFIG_CONVERSATIONS=' + c.paths.conversations, \
'CONFIG_BASH_EVENTS=' + c.paths.bashEvents, \
'CONFIG_AUTOMATION_DB=' + c.paths.automationDb, \
'CONFIG_CANVAS_BASE_PATH=' + c.paths.canvasBasePath, \
'CONFIG_VSCODE_BASE_PATH=' + c.paths.vscodeBasePath, \
'CONFIG_POSTHOG_API_KEY=' + c.telemetry.posthogApiKey, \
'CONFIG_POSTHOG_HOST=' + c.telemetry.posthogHost, \
]; \
require('fs').writeFileSync('/tmp/defaults.env', lines.join('\n') + '\n'); \
"
# ββ Stage 2: Combined image ββββββββββββββββββββββββββββββββββββββββββββββββββ
FROM ${AGENT_SERVER_IMAGE} AS final
ARG AUTOMATION_VERSION
ARG AGENT_SERVER_SDK_GIT_REF=""
ARG OPENHANDS_BUILD_GIT_SHA=unknown
ARG OPENHANDS_BUILD_GIT_REF=unknown
ARG VITE_BASE_PATH="/canvas"
ARG VITE_POSTHOG_API_KEY=""
ARG AGENT_CANVAS_VERSION=dev
LABEL org.opencontainers.image.title="agent-canvas"
LABEL org.opencontainers.image.description="All-in-one agent-canvas: Agent Server + Automation + Frontend"
LABEL org.opencontainers.image.source="https://github.com/OpenHands/OpenHands"
LABEL org.opencontainers.image.version="${AGENT_CANVAS_VERSION}"
LABEL org.opencontainers.image.revision="${OPENHANDS_BUILD_GIT_SHA}"
ENV AGENT_CANVAS_BUILD_GIT_SHA=${OPENHANDS_BUILD_GIT_SHA}
ENV AGENT_CANVAS_BUILD_GIT_REF=${OPENHANDS_BUILD_GIT_REF}
ENV AGENT_CANVAS_BASE_PATH=${VITE_BASE_PATH}
ENV VITE_POSTHOG_API_KEY=${VITE_POSTHOG_API_KEY}
USER root
# Install system deps required by automation's transitive dependencies
# (asyncpg needs libpq, which the agent-server base image may not include).
RUN if command -v apt-get >/dev/null 2>&1; then \
apt-get update && \
apt-get install -y --no-install-recommends libpq-dev && \
rm -rf /var/lib/apt/lists/*; \
fi
# Install automation server via pip.
# Shared deps (openhands-sdk, fastapi, uvicorn, pydantic, httpx, β¦) are
# already satisfied by the agent-server base image, so only automation-
# specific packages (asyncpg, sqlalchemy, boto3, gcloud, β¦) are added.
RUN uv pip install --system "openhands-automation==${AUTOMATION_VERSION}" 2>/dev/null \
|| pip install --no-cache-dir "openhands-automation==${AUTOMATION_VERSION}"
RUN if [ -n "${AGENT_SERVER_SDK_GIT_REF}" ]; then \
BASE_GIT_URL="git+https://github.com/OpenHands/software-agent-sdk@${AGENT_SERVER_SDK_GIT_REF}" && \
uv pip install --system --force-reinstall \
"${BASE_GIT_URL}#subdirectory=openhands-sdk" \
"${BASE_GIT_URL}#subdirectory=openhands-tools" \
"${BASE_GIT_URL}#subdirectory=openhands-workspace"; \
fi
# Copy the frontend build output.
# react-router.config.ts unpacks build/client/ into build/ for non-Vercel builds.
COPY --from=frontend-build /build/build /opt/agent-canvas/frontend
# Copy the static-server scripts and their production runtime deps.
COPY scripts/static-server.mjs /opt/agent-canvas/static-server.mjs
COPY scripts/proxy-utils.mjs /opt/agent-canvas/proxy-utils.mjs
COPY --from=frontend-build /build/node_modules/httpxy /opt/agent-canvas/node_modules/httpxy
COPY --from=frontend-build /build/node_modules/sirv /opt/agent-canvas/node_modules/sirv
COPY --from=frontend-build /build/node_modules/@polka /opt/agent-canvas/node_modules/@polka
COPY --from=frontend-build /build/node_modules/mrmime /opt/agent-canvas/node_modules/mrmime
COPY --from=frontend-build /build/node_modules/totalist /opt/agent-canvas/node_modules/totalist
# Copy the runtime-services-info builder (entrypoint.sh runs it as a CLI to
# emit the agent's <RUNTIME_SERVICES> block; same builder the dev stack uses).
COPY scripts/runtime-services-info.mjs /opt/agent-canvas/runtime-services-info.mjs
# Persisted conversations created before the client_tools migration still
# import canvas_ui_tool by qualname. Keep the compatibility module available;
# new conversations define canvas_ui_control through a JSON client tool.
COPY tools/ /opt/agent-canvas/tools/
# Copy generated defaults.env (from config/defaults.json via config-gen stage)
COPY --from=config-gen /tmp/defaults.env /opt/agent-canvas/defaults.env
# Copy the entrypoint
COPY docker/entrypoint.sh /opt/agent-canvas/entrypoint.sh
RUN chmod +x /opt/agent-canvas/entrypoint.sh
# Pre-create persistence directories with correct ownership so the
# openhands user can write to them even when Docker creates anonymous
# volumes (which default to root).
RUN mkdir -p /home/openhands/.openhands/agent-canvas/conversations \
/home/openhands/.openhands/agent-canvas/bash_events \
/home/openhands/.openhands/automation \
/projects && \
chown -R openhands:openhands /home/openhands/.openhands /projects
USER openhands
# Persistence volumes:
# /home/openhands/.openhands β settings, secrets, conversations, automation DB
# /projects β user code the agent can read/edit
# Bind-mount these for data to survive container restarts:
# docker run -v ~/.openhands:/home/openhands/.openhands -v ~/projects:/projects ...
VOLUME ["/home/openhands/.openhands", "/projects"]
# The entrypoint starts all services and the ingress proxy.
# Port 8000 is the unified entry point.
EXPOSE 8000
ENTRYPOINT ["tini", "--", "/opt/agent-canvas/entrypoint.sh"]
|