-
Notifications
You must be signed in to change notification settings - Fork 4.8k
[codex] feat(docker): add portable agent environment #6583
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
f231c35
06c7942
d5bb220
9be22b7
ef54db0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| .git | ||
| .github | ||
| .repos | ||
| **/.t3 | ||
| **/.env | ||
| **/.env.* | ||
| **/.codex/auth.json | ||
| **/.codex/*.sqlite | ||
| **/.codex/*.sqlite-* | ||
| **/.codex/*.db | ||
| **/.codex/*.db-* | ||
| **/.claude.json | ||
| **/.claude/.credentials.json | ||
| **/.cursor/cli-config.json | ||
| **/.config/opencode | ||
| **/.local/share/opencode | ||
| **/.ssh | ||
| **/.npmrc | ||
| **/.pnpmrc | ||
| **/.yarnrc | ||
| **/.yarnrc.yml | ||
| **/node_modules | ||
| **/dist | ||
| **/.vite-plus | ||
| **/.turbo | ||
| **/.tanstack | ||
| **/coverage | ||
| **/playwright-report | ||
| **/*.log | ||
| **/*.tsbuildinfo | ||
| scripts/docker-config.test.ts | ||
| scripts/docker-e2e.ts | ||
| artifacts | ||
| build | ||
| dist-electron | ||
| release | ||
| release-mock | ||
| native/**/target |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| name: Docker E2E | ||
|
|
||
| on: | ||
| pull_request: | ||
| paths: | ||
| - .dockerignore | ||
| - .gitignore | ||
| - Dockerfile | ||
| - compose.yaml | ||
| - package.json | ||
| - pnpm-lock.yaml | ||
| - pnpm-workspace.yaml | ||
| - patches/** | ||
| - apps/server/** | ||
| - apps/web/** | ||
| - packages/** | ||
| - scripts/docker-config.test.ts | ||
| - scripts/docker-e2e.ts | ||
| - .github/workflows/docker.yml | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. CI skips Docker build script inputsMedium Severity The Docker E2E path filters omit Additional Locations (1)Reviewed by Cursor Bugbot for commit ef54db0. Configure here. |
||
| push: | ||
| branches: | ||
| - main | ||
| paths: | ||
| - .dockerignore | ||
| - .gitignore | ||
| - Dockerfile | ||
| - compose.yaml | ||
| - package.json | ||
| - pnpm-lock.yaml | ||
| - pnpm-workspace.yaml | ||
| - patches/** | ||
| - apps/server/** | ||
| - apps/web/** | ||
| - packages/** | ||
| - scripts/docker-config.test.ts | ||
| - scripts/docker-e2e.ts | ||
| - .github/workflows/docker.yml | ||
|
|
||
| concurrency: | ||
| group: docker-e2e-${{ github.event.pull_request.number || github.sha }} | ||
| cancel-in-progress: ${{ github.event_name == 'pull_request' }} | ||
|
|
||
| jobs: | ||
| docker_e2e: | ||
| name: Build and exercise image | ||
| runs-on: ubuntu-24.04 | ||
| timeout-minutes: 30 | ||
| steps: | ||
| - name: Checkout | ||
| uses: actions/checkout@v6 | ||
| with: | ||
| sparse-checkout: | | ||
| /* | ||
| !/.repos/ | ||
| sparse-checkout-cone-mode: false | ||
|
|
||
| - name: Setup Node | ||
| uses: actions/setup-node@v6 | ||
| with: | ||
| node-version-file: package.json | ||
|
|
||
| - name: Run Docker end-to-end test | ||
| run: node scripts/docker-e2e.ts | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,110 @@ | ||
| # syntax=docker/dockerfile:1.7 | ||
|
|
||
| ARG NODE_VERSION=24.13.1 | ||
|
|
||
| FROM node:${NODE_VERSION}-bookworm AS builder | ||
|
|
||
| ENV CI=true | ||
|
|
||
| RUN apt-get update \ | ||
| && apt-get install --yes --no-install-recommends g++ make python3 \ | ||
| && rm -rf /var/lib/apt/lists/* | ||
|
|
||
| RUN corepack enable | ||
|
|
||
| WORKDIR /src | ||
|
|
||
| # Keep dependency installation cacheable when application source changes. | ||
| COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ | ||
| COPY patches ./patches | ||
| COPY apps/server/package.json ./apps/server/package.json | ||
| COPY apps/web/package.json ./apps/web/package.json | ||
| COPY packages/client-runtime/package.json ./packages/client-runtime/package.json | ||
| COPY packages/contracts/package.json ./packages/contracts/package.json | ||
| COPY packages/effect-acp/package.json ./packages/effect-acp/package.json | ||
| COPY packages/effect-codex-app-server/package.json ./packages/effect-codex-app-server/package.json | ||
| COPY packages/shared/package.json ./packages/shared/package.json | ||
| COPY packages/tailscale/package.json ./packages/tailscale/package.json | ||
|
|
||
| RUN --mount=type=cache,id=t3-pnpm-store,target=/root/.local/share/pnpm/store \ | ||
| pnpm install --frozen-lockfile --filter t3... --ignore-scripts | ||
|
|
||
| COPY . . | ||
|
|
||
| # Defense in depth: these paths must be removed from the context by | ||
| # .dockerignore, even when a developer has authenticated locally. | ||
| RUN test ! -e .env \ | ||
| && test ! -e .codex/auth.json \ | ||
| && test ! -e .claude.json \ | ||
| && test ! -e .claude/.credentials.json \ | ||
| && test ! -e .cursor/cli-config.json \ | ||
| && test ! -e .config/opencode \ | ||
| && test ! -e .local/share/opencode \ | ||
| && test ! -e .ssh \ | ||
| && test -z "$(find .docker-e2e-canary -type f -print -quit 2> /dev/null)" | ||
|
|
||
| RUN pnpm rebuild esbuild msgpackr-extract node-pty | ||
| RUN pnpm --filter @t3tools/web exec vp build | ||
| RUN pnpm --filter t3 run build:bundle \ | ||
| && cp -R apps/web/dist apps/server/dist/client | ||
| RUN pnpm --filter t3 deploy --prod --legacy /out/t3 | ||
|
|
||
| FROM node:${NODE_VERSION}-bookworm-slim AS runtime | ||
|
|
||
| ARG T3CODE_PROVIDER_PACKAGES="@openai/codex@latest @anthropic-ai/claude-code@latest opencode-ai@latest" | ||
| ARG T3CODE_INSTALL_PROVIDERS=1 | ||
| ARG T3CODE_INSTALL_CURSOR=1 | ||
|
|
||
| LABEL org.opencontainers.image.source="https://github.com/pingdotgg/t3code" | ||
|
|
||
| RUN apt-get update \ | ||
| && apt-get install --yes --no-install-recommends ca-certificates curl gh git openssh-client procps ripgrep \ | ||
| && rm -rf /var/lib/apt/lists/* | ||
|
|
||
| # Provider CLIs must live in the container; host-installed binaries are not | ||
| # visible here. Set T3CODE_INSTALL_PROVIDERS=0 for a server-only image, or | ||
| # replace this argument with a pinned/custom package list. | ||
| RUN if [ "${T3CODE_INSTALL_PROVIDERS}" = "1" ] && [ -n "${T3CODE_PROVIDER_PACKAGES}" ]; then \ | ||
| npm install --global ${T3CODE_PROVIDER_PACKAGES}; \ | ||
| fi \ | ||
| && npm cache clean --force | ||
|
|
||
| # Cursor distributes its Linux CLI through its own installer rather than npm. | ||
| # Keep the executable outside HOME so it remains available when /home/node is | ||
| # backed by an existing volume. Provider updates happen by rebuilding the image. | ||
| RUN if [ "${T3CODE_INSTALL_CURSOR}" = "1" ]; then \ | ||
| mkdir -p /opt/cursor-home \ | ||
| && curl --fail --silent --show-error --location https://cursor.com/install --output /tmp/install-cursor.sh \ | ||
| && HOME=/opt/cursor-home bash /tmp/install-cursor.sh \ | ||
| && ln -s /opt/cursor-home/.local/bin/cursor-agent /usr/local/bin/cursor-agent \ | ||
| && ln -s /opt/cursor-home/.local/bin/agent /usr/local/bin/agent \ | ||
| && rm /tmp/install-cursor.sh; \ | ||
| fi | ||
|
|
||
| COPY --from=builder --chown=node:node /out/t3 /opt/t3 | ||
|
|
||
| RUN chmod +x /opt/t3/dist/bin.mjs \ | ||
| && ln -s /opt/t3/dist/bin.mjs /usr/local/bin/t3 \ | ||
| && mkdir -p /home/node/.local /home/node/.t3 /workspace \ | ||
| && chown -R node:node /home/node /workspace | ||
|
|
||
| ENV HOME=/home/node \ | ||
| NODE_ENV=production \ | ||
| NPM_CONFIG_PREFIX=/home/node/.local \ | ||
| NPM_CONFIG_UPDATE_NOTIFIER=false \ | ||
| PATH=/home/node/.local/bin:/usr/local/bin:/usr/bin:/bin \ | ||
| T3CODE_HOME=/home/node/.t3 \ | ||
| T3CODE_HOST=0.0.0.0 \ | ||
| T3CODE_NO_BROWSER=true \ | ||
| T3CODE_PORT=3773 | ||
|
|
||
| WORKDIR /workspace | ||
| USER node | ||
|
|
||
| EXPOSE 3773 | ||
|
|
||
| HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 \ | ||
| CMD curl --fail --silent --show-error http://127.0.0.1:3773/ > /dev/null || exit 1 | ||
|
|
||
| ENTRYPOINT ["t3"] | ||
| CMD ["serve", "/workspace"] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| services: | ||
| t3: | ||
| build: | ||
| context: . | ||
| args: | ||
| T3CODE_INSTALL_CURSOR: ${T3CODE_INSTALL_CURSOR:-1} | ||
| T3CODE_INSTALL_PROVIDERS: ${T3CODE_INSTALL_PROVIDERS:-1} | ||
| T3CODE_PROVIDER_PACKAGES: ${T3CODE_PROVIDER_PACKAGES-@openai/codex@latest @anthropic-ai/claude-code@latest opencode-ai@latest} | ||
| image: ${T3_IMAGE:-t3-code:local} | ||
| hostname: ${T3_HOSTNAME:-t3-code} | ||
| init: true | ||
| restart: unless-stopped | ||
| ports: | ||
| - "${T3_BIND_ADDRESS:-127.0.0.1}:${T3_PORT:-3773}:3773" | ||
| environment: | ||
| T3CODE_HOME: /home/node/.t3 | ||
| T3CODE_HOST: 0.0.0.0 | ||
| T3CODE_NO_BROWSER: "true" | ||
| T3CODE_PORT: 3773 | ||
| volumes: | ||
| - type: volume | ||
| source: t3-home | ||
| target: /home/node | ||
| - type: bind | ||
| source: ${T3_WORKSPACE_PATH:-.} | ||
| target: /workspace | ||
|
|
||
| volumes: | ||
| t3-home: |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,95 @@ | ||
| # Docker | ||
|
|
||
| The Docker image runs T3 Code, its web client, and the Codex, Claude Code, Cursor, and OpenCode | ||
| CLIs in one isolated environment. It can only work with directories mounted into the container; | ||
| tools and credentials installed on the host are not automatically available inside it. | ||
|
|
||
| ## Start the container | ||
|
|
||
| Set the workspace path to the directory the agents should be able to edit, then start the Compose | ||
| service from the T3 Code repository: | ||
|
|
||
| ```bash | ||
| T3_WORKSPACE_PATH=/absolute/path/to/code docker compose up --build | ||
| ``` | ||
|
|
||
| In PowerShell: | ||
|
|
||
| ```powershell | ||
| $env:T3_WORKSPACE_PATH = "C:\path\to\code" | ||
| docker compose up --build | ||
| ``` | ||
|
|
||
| The first build installs T3 Code and the supported provider CLIs. Later starts reuse the `t3-home` | ||
| volume, which contains T3 Code data, provider logins, Git configuration, and other files in the | ||
| container user's home directory. The container has the stable hostname `t3-code` by default, so | ||
| remote clients do not display a generated container ID as the environment name. | ||
|
|
||
| ## Pair the browser | ||
|
|
||
| The startup log prints a one-time pairing URL. Because T3 Code detects the container's internal | ||
| address, replace only that URL's origin with `http://localhost:3773` and keep the | ||
| `/pair#token=...` portion unchanged. | ||
|
|
||
| For example: | ||
|
|
||
| ```text | ||
| Printed: http://172.18.0.2:3773/pair#token=... | ||
| Open: http://localhost:3773/pair#token=... | ||
| ``` | ||
|
|
||
| If the link expired or was already used, create another one: | ||
|
|
||
| ```bash | ||
| docker compose exec t3 t3 pair | ||
| ``` | ||
|
|
||
| Then add `/workspace` as a project in T3 Code. | ||
|
|
||
| ## Sign in to a provider | ||
|
|
||
| Provider authentication happens inside the container and remains in the `t3-home` volume: | ||
|
|
||
| ```bash | ||
| docker compose exec t3 codex login --device-auth | ||
| docker compose exec t3 claude auth login | ||
| docker compose exec t3 agent login | ||
| docker compose exec t3 opencode auth login | ||
| ``` | ||
|
|
||
| These are subscription login sessions, not API keys baked into the image. Do not copy provider | ||
| credential files into the repository, pass them as Docker build arguments, or commit an exported | ||
| `t3-home` volume. Build arguments and image layers are not secret storage. | ||
|
|
||
| Grok is not installed by the default image. To use it, install its Linux CLI in a derived image and | ||
| select that executable in T3 Code's provider settings. | ||
|
|
||
| ## Configuration | ||
|
|
||
| The Compose setup supports these environment variables: | ||
|
|
||
| - `T3_WORKSPACE_PATH`: host directory mounted at `/workspace`; defaults to the T3 Code repository. | ||
| - `T3_IMAGE`: image name used by Compose; defaults to `t3-code:local`. | ||
| - `T3_HOSTNAME`: stable environment name reported by the container; defaults to `t3-code`. | ||
| - `T3_PORT`: published host port; defaults to `3773`. | ||
| - `T3_BIND_ADDRESS`: host interface used for the published port; defaults to `127.0.0.1`. | ||
| - `T3CODE_INSTALL_CURSOR`: set to `0` to omit Cursor Agent; defaults to `1`. | ||
| - `T3CODE_INSTALL_PROVIDERS`: set to `0` for a server-only image; defaults to `1`. | ||
| - `T3CODE_PROVIDER_PACKAGES`: space-separated npm packages installed in the image. Provide pinned | ||
| versions for reproducible builds. | ||
|
|
||
| Runtime-only settings, including the public T3 Connect configuration, can be added to the Compose | ||
| service's `environment` section. Keep private values in a local ignored environment file or Docker | ||
| secret and pass them only at runtime. | ||
|
|
||
| To make the server reachable from another device on a trusted network, set | ||
| `T3_BIND_ADDRESS=0.0.0.0` before starting it. Pairing is still required. Prefer an HTTPS endpoint | ||
| for access from `https://app.t3.codes`; browsers block connections from that hosted app to a plain | ||
| HTTP backend. | ||
|
|
||
| The container runs as UID/GID `1000:1000`. On Linux, the mounted workspace must be readable and | ||
| writable by that user. The image does not mount the Docker socket; add it only if an agent | ||
| explicitly needs Docker access, since doing so grants broad control over the host. | ||
|
|
||
| To remove the container while retaining state, run `docker compose down`. Adding `--volumes` also | ||
| deletes the persistent T3 and provider state. |


There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The workflow watches
pnpm-lock.yamlbut omitspnpm-workspace.yamlandpatches/**, even though the Dockerfile copies both before installation. A change to workspace dependency/build configuration or to a patch applied duringpnpm installcan therefore break the image while this Docker E2E job is skipped; include all direct build inputs in both the pull-request and push path filters.Useful? React with 👍 / 👎.