Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions .dockerignore
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
63 changes: 63 additions & 0 deletions .github/workflows/docker.yml
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/**
Comment on lines +10 to +14

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Trigger Docker E2E for every build input

The workflow watches pnpm-lock.yaml but omits pnpm-workspace.yaml and patches/**, even though the Dockerfile copies both before installation. A change to workspace dependency/build configuration or to a patch applied during pnpm install can 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 👍 / 👎.

- apps/web/**
- packages/**
- scripts/docker-config.test.ts
- scripts/docker-e2e.ts
- .github/workflows/docker.yml

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CI skips Docker build script inputs

Medium Severity

The Docker E2E path filters omit scripts/lib/** and the root Vite config even though the image build loads those files when packing the server and building the web client. Changes there can ship a broken image without this workflow running.

Additional Locations (1)
Fix in Cursor Fix in Web

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
15 changes: 15 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,18 @@ node_modules/
*.log
.env*
!.env.example
.docker-e2e-canary/

# Machine-local credentials. Project configuration under .codex, .claude, and
# .cursor may be tracked, but provider login state must never enter the repo.
/.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/
110 changes: 110 additions & 0 deletions Dockerfile
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"]
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ Full docs live in [docs/](./docs). There's no docs site yet.
- [Keyboard shortcuts](./docs/user/keybindings.md)
- [Customize a project icon](./docs/user/project-settings.md)
- [Remote access from a phone or another machine](./docs/user/remote-access.md)
- [Run T3 Code with Docker](./docs/user/docker.md)
- [Keeping app and server in sync](./docs/user/updating.md)
- [Source control integrations](./docs/user/source-control.md)
- Multiple accounts: [Codex](./docs/user/providers-codex.md) · [Claude](./docs/user/providers-claude.md)
Expand Down
29 changes: 29 additions & 0 deletions compose.yaml
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:
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
- [Customize a project icon](./user/project-settings.md)
- [Mobile appearance](./user/mobile-appearance.md)
- [Remote access](./user/remote-access.md)
- [Docker](./user/docker.md)
- [Keeping app and server in sync](./user/updating.md)
- [Source control integrations](./user/source-control.md)
- [Background service (Linux)](./user/background-service.md)
Expand Down
95 changes: 95 additions & 0 deletions docs/user/docker.md
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.
6 changes: 6 additions & 0 deletions docs/user/install.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ npx t3@latest
This starts the T3 Code server on your machine and opens the local web app. Use
`npx t3@latest --help` for the full CLI reference.

## Docker

For an isolated, headless environment, use the [Docker setup](./docker.md). It keeps T3 Code
and provider credentials in a persistent volume while exposing only the workspace you mount into
the container.

## Desktop App

Download the latest release from
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"test": "vp run -r test",
"test:resource-monitor": "cargo test --locked --manifest-path native/resource-monitor/Cargo.toml",
"test:desktop-smoke": "vp run --filter @t3tools/desktop smoke-test",
"test:docker": "node scripts/docker-e2e.ts",
"fmt": "vp fmt",
"fmt:check": "vp fmt --check",
"build:contracts": "vp run --filter @t3tools/contracts build",
Expand Down
Loading
Loading