Skip to content

Commit feeb85a

Browse files
committed
perf(api): speed up controller Docker rebuilds
1 parent 525ed8b commit feeb85a

12 files changed

Lines changed: 559 additions & 61 deletions

.dockerignore

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,19 @@
11
.git
2+
.codex
3+
.github
4+
.knowledge
25
node_modules
36
**/node_modules
47
**/.cache
8+
**/.turbo
9+
coverage
10+
dist
11+
dist-test
12+
dist-web
13+
packages/*/coverage
14+
packages/*/dist
15+
packages/*/dist-test
16+
packages/*/dist-web
17+
**/.vite
18+
**/*.log
519
third_party/skiller-desktop-skills-manager/out

.github/workflows/check.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,8 @@ jobs:
153153
name: E2E (Browser command)
154154
runs-on: ubuntu-latest
155155
timeout-minutes: 40
156+
env:
157+
DOCKER_GIT_CONTROLLER_BUILD_SKILLER: "0"
156158
steps:
157159
- uses: actions/checkout@v6
158160
with:
@@ -170,6 +172,8 @@ jobs:
170172
name: E2E (OpenCode)
171173
runs-on: ubuntu-latest
172174
timeout-minutes: 40
175+
env:
176+
DOCKER_GIT_CONTROLLER_BUILD_SKILLER: "0"
173177
steps:
174178
- uses: actions/checkout@v6
175179
with:
@@ -187,6 +191,8 @@ jobs:
187191
name: E2E (Clone cache)
188192
runs-on: ubuntu-latest
189193
timeout-minutes: 40
194+
env:
195+
DOCKER_GIT_CONTROLLER_BUILD_SKILLER: "0"
190196
steps:
191197
- uses: actions/checkout@v6
192198
with:
@@ -204,6 +210,8 @@ jobs:
204210
name: E2E (Login context)
205211
runs-on: ubuntu-latest
206212
timeout-minutes: 40
213+
env:
214+
DOCKER_GIT_CONTROLLER_BUILD_SKILLER: "0"
207215
steps:
208216
- uses: actions/checkout@v6
209217
with:
@@ -221,6 +229,8 @@ jobs:
221229
name: E2E (Runtime volumes + SSH)
222230
runs-on: ubuntu-latest
223231
timeout-minutes: 60
232+
env:
233+
DOCKER_GIT_CONTROLLER_BUILD_SKILLER: "0"
224234
steps:
225235
- uses: actions/checkout@v6
226236
with:
@@ -238,6 +248,8 @@ jobs:
238248
name: E2E (Clone auto-open SSH)
239249
runs-on: ubuntu-latest
240250
timeout-minutes: 40
251+
env:
252+
DOCKER_GIT_CONTROLLER_BUILD_SKILLER: "0"
241253
steps:
242254
- uses: actions/checkout@v6
243255
with:

docker-compose.api.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,13 @@ services:
44
context: .
55
dockerfile: packages/api/Dockerfile
66
args:
7+
DOCKER_GIT_CONTROLLER_REV: ${DOCKER_GIT_CONTROLLER_REV:-unknown}
8+
DOCKER_GIT_CONTROLLER_BUILD_SKILLER: ${DOCKER_GIT_CONTROLLER_BUILD_SKILLER:-1}
79
UBUNTU_APT_MIRROR: ${UBUNTU_APT_MIRROR:-}
810
container_name: ${DOCKER_GIT_API_CONTAINER_NAME:-docker-git-api}
911
environment:
1012
DOCKER_GIT_API_PORT: ${DOCKER_GIT_API_PORT:-3334}
13+
DOCKER_GIT_CONTROLLER_REV: ${DOCKER_GIT_CONTROLLER_REV:-unknown}
1114
DOCKER_GIT_DOCKER_RUNTIME: ${DOCKER_GIT_DOCKER_RUNTIME:-isolated}
1215
DOCKER_HOST: ${DOCKER_GIT_CONTROLLER_DOCKER_HOST:-unix:///var/run/docker.sock}
1316
DOCKER_GIT_DOCKERD_TCP_HOST: ${DOCKER_GIT_DOCKERD_TCP_HOST:-tcp://0.0.0.0:2375}

docker-compose.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ services:
55
dockerfile: packages/api/Dockerfile
66
args:
77
DOCKER_GIT_CONTROLLER_REV: ${DOCKER_GIT_CONTROLLER_REV:-unknown}
8+
DOCKER_GIT_CONTROLLER_BUILD_SKILLER: ${DOCKER_GIT_CONTROLLER_BUILD_SKILLER:-1}
89
UBUNTU_APT_MIRROR: ${UBUNTU_APT_MIRROR:-}
910
container_name: ${DOCKER_GIT_API_CONTAINER_NAME:-docker-git-api}
1011
environment:

packages/api/Dockerfile

Lines changed: 84 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,8 @@
1-
FROM ubuntu:26.04
1+
FROM ubuntu:26.04 AS controller-base
22

3-
ARG DOCKER_GIT_CONTROLLER_REV=unknown
43
ARG UBUNTU_APT_MIRROR=
5-
LABEL io.prover-coder-ai.docker-git.controller-rev=$DOCKER_GIT_CONTROLLER_REV
64

75
ENV DEBIAN_FRONTEND=noninteractive
8-
ENV DOCKER_GIT_CONTROLLER_REV=$DOCKER_GIT_CONTROLLER_REV
96
ENV BUN_INSTALL=/opt/bun
107
ENV PATH=/opt/bun/bin:$PATH
118
WORKDIR /workspace
@@ -55,16 +52,23 @@ RUN curl -fsSL https://deb.nodesource.com/setup_24.x | bash - \
5552
&& npm i -g node-gyp \
5653
&& rm -rf /var/lib/apt/lists/*
5754

55+
FROM controller-base AS workspace-deps
56+
5857
COPY package.json bun.lock bunfig.toml tsconfig.base.json tsconfig.json ./
59-
COPY patches ./patches
60-
COPY scripts ./scripts
61-
COPY packages ./packages
62-
COPY .gitmodules ./.gitmodules
63-
COPY third_party ./third_party
58+
RUN mkdir -p packages/api packages/app packages/docker-git-session-sync packages/lib
59+
COPY packages/api/package.json ./packages/api/package.json
60+
COPY packages/app/package.json ./packages/app/package.json
61+
COPY packages/docker-git-session-sync/package.json ./packages/docker-git-session-sync/package.json
62+
COPY packages/lib/package.json ./packages/lib/package.json
6463

6564
RUN set -eu; \
6665
for attempt in 1 2 3 4 5; do \
67-
if bun install --frozen-lockfile --silent; then \
66+
if bun install \
67+
--frozen-lockfile \
68+
--silent \
69+
--filter @effect-template/api \
70+
--filter @effect-template/lib \
71+
--filter @prover-coder-ai/docker-git-session-sync; then \
6872
exit 0; \
6973
fi; \
7074
echo "bun install attempt ${attempt} failed; retrying..." >&2; \
@@ -73,28 +77,78 @@ RUN set -eu; \
7377
done; \
7478
echo "bun install failed after retries" >&2; \
7579
exit 1
80+
81+
FROM workspace-deps AS workspace-static
82+
83+
COPY patches ./patches
84+
COPY scripts ./scripts
85+
COPY packages/docker-git-session-sync ./packages/docker-git-session-sync
86+
COPY packages/lib ./packages/lib
87+
88+
RUN bun run --cwd packages/docker-git-session-sync build
7689
RUN bun run --cwd packages/lib build
77-
RUN bun run --cwd packages/api build
78-
RUN bun scripts/skiller-apply-docker-git-patches.mjs
79-
RUN test -f third_party/skiller-desktop-skills-manager/package.json \
80-
&& cd third_party/skiller-desktop-skills-manager \
81-
&& for attempt in 1 2 3 4 5; do \
82-
if bun install --frozen-lockfile --silent; then \
83-
break; \
84-
fi; \
85-
if [ "$attempt" = "5" ]; then \
86-
echo "skiller bun install failed after retries" >&2; \
87-
exit 1; \
88-
fi; \
89-
echo "skiller bun install attempt ${attempt} failed; retrying..." >&2; \
90-
rm -rf /root/.bun/install/cache node_modules; \
91-
sleep $((attempt * 2)); \
92-
done \
93-
&& bun run build \
94-
&& touch out/.docker-git-browser-folder-picker.patch \
95-
&& mkdir -p out/preload \
96-
&& ln -sf index.mjs out/preload/index.js
9790

91+
FROM controller-base AS skiller-build
92+
93+
ARG DOCKER_GIT_CONTROLLER_BUILD_SKILLER=1
94+
95+
COPY patches ./patches
96+
COPY scripts/skiller-apply-docker-git-patches.mjs ./scripts/skiller-apply-docker-git-patches.mjs
97+
COPY .gitmodules ./.gitmodules
98+
COPY third_party ./third_party
99+
100+
RUN if [ "$DOCKER_GIT_CONTROLLER_BUILD_SKILLER" = "1" ]; then \
101+
bun scripts/skiller-apply-docker-git-patches.mjs; \
102+
else \
103+
echo "Skipping Skiller build for controller image."; \
104+
fi
105+
RUN if [ "$DOCKER_GIT_CONTROLLER_BUILD_SKILLER" = "1" ]; then \
106+
test -f third_party/skiller-desktop-skills-manager/package.json \
107+
&& cd third_party/skiller-desktop-skills-manager \
108+
&& for attempt in 1 2 3 4 5; do \
109+
if bun install --frozen-lockfile --silent; then \
110+
break; \
111+
fi; \
112+
if [ "$attempt" = "5" ]; then \
113+
echo "skiller bun install failed after retries" >&2; \
114+
exit 1; \
115+
fi; \
116+
echo "skiller bun install attempt ${attempt} failed; retrying..." >&2; \
117+
rm -rf /root/.bun/install/cache node_modules; \
118+
sleep $((attempt * 2)); \
119+
done \
120+
&& bun run build \
121+
&& touch out/.docker-git-browser-folder-picker.patch \
122+
&& mkdir -p out/preload \
123+
&& ln -sf index.mjs out/preload/index.js; \
124+
else \
125+
cd third_party/skiller-desktop-skills-manager \
126+
&& mkdir -p node_modules/electron/dist out/main out/renderer out/preload \
127+
&& printf '%s\n' '#!/usr/bin/env sh' 'echo "Skiller is not bundled in this controller image." >&2' 'exit 1' \
128+
> node_modules/electron/dist/electron \
129+
&& chmod +x node_modules/electron/dist/electron \
130+
&& printf '%s\n' 'throw new Error("Skiller is not bundled in this controller image.")' > out/main/index.js \
131+
&& printf '%s\n' '<!doctype html><title>Skiller unavailable</title>' > out/renderer/index.html \
132+
&& printf '%s\n' 'export {}' > out/preload/index.mjs \
133+
&& touch out/.docker-git-browser-folder-picker.patch \
134+
&& ln -sf index.mjs out/preload/index.js; \
135+
fi
136+
137+
FROM workspace-static AS controller-final
138+
139+
COPY .gitmodules ./.gitmodules
140+
COPY --from=skiller-build /workspace/third_party/skiller-desktop-skills-manager ./third_party/skiller-desktop-skills-manager
141+
COPY packages/api ./packages/api
142+
143+
RUN ./packages/api/node_modules/.bin/tsc -p packages/api/tsconfig.json
144+
145+
ARG DOCKER_GIT_CONTROLLER_REV=unknown
146+
ARG DOCKER_GIT_CONTROLLER_BUILD_SKILLER=1
147+
LABEL io.prover-coder-ai.docker-git.controller-rev=$DOCKER_GIT_CONTROLLER_REV
148+
LABEL io.prover-coder-ai.docker-git.controller-build-skiller=$DOCKER_GIT_CONTROLLER_BUILD_SKILLER
149+
150+
ENV DOCKER_GIT_CONTROLLER_REV=$DOCKER_GIT_CONTROLLER_REV
151+
ENV DOCKER_GIT_CONTROLLER_BUILD_SKILLER=$DOCKER_GIT_CONTROLLER_BUILD_SKILLER
98152
ENV DOCKER_GIT_API_PORT=3334
99153
ENV DOCKER_GIT_DOCKER_RUNTIME=isolated
100154
ENV DOCKER_HOST=unix:///var/run/docker.sock
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
export type ControllerComposeUpPlan = {
2+
readonly buildController: boolean
3+
readonly forceRecreateController: boolean
4+
}
5+
6+
export type ControllerImageBuildInput = {
7+
readonly localControllerRevision: string
8+
readonly currentControllerRevision: string | null
9+
readonly currentImageRevision: string | null
10+
readonly forceRecreateController: boolean
11+
}
12+
13+
/**
14+
* Renders the docker compose `up` arguments for the controller bootstrap plan.
15+
*
16+
* @param plan - Immutable build/recreate decision.
17+
* @returns Compose arguments preserving the fixed `up -d` prefix.
18+
*
19+
* @pure true
20+
* @effect n/a
21+
* @invariant `--build` is present iff `plan.buildController`.
22+
* @precondition Plan booleans are already resolved from Docker state.
23+
* @postcondition Returned arguments contain no duplicate compose flags.
24+
* @complexity O(1) time and O(1) space.
25+
* @throws Never
26+
*/
27+
// CHANGE: derive docker compose up flags from explicit bootstrap requirements
28+
// WHY: matching controller images should be started without invalidating Docker build cache
29+
// QUOTE(ТЗ): "хочу сузить время билда докер контейнера"
30+
// REF: user-request-2026-05-22-controller-build-speed
31+
// SOURCE: n/a
32+
// FORMAT THEOREM: forall p: build(p) <=> "--build" in args(p)
33+
// PURITY: CORE
34+
// EFFECT: n/a
35+
// INVARIANT: forceRecreateController controls only --force-recreate
36+
// COMPLEXITY: O(1)
37+
export const resolveControllerComposeUpArgs = (
38+
plan: ControllerComposeUpPlan
39+
): ReadonlyArray<string> => [
40+
"up",
41+
"-d",
42+
...(plan.buildController ? ["--build"] : []),
43+
...(plan.forceRecreateController ? ["--force-recreate"] : [])
44+
]
45+
46+
/**
47+
* Decides whether the controller image must be rebuilt before `docker compose up`.
48+
*
49+
* @param input - Current controller/image revisions and recreate requirement.
50+
* @returns `true` only when neither reusable Docker object proves the local revision.
51+
*
52+
* @pure true
53+
* @effect n/a
54+
* @invariant A matching image revision is sufficient proof to skip build.
55+
* @precondition Revisions are normalized controller revision strings or null.
56+
* @postcondition Forced recreation rebuilds only when no matching image exists.
57+
* @complexity O(1) time and O(1) space.
58+
* @throws Never
59+
*/
60+
// CHANGE: decide whether controller bootstrap needs a Docker image build
61+
// WHY: source revision can be satisfied by either the existing container or an already-built image
62+
// QUOTE(ТЗ): "контейнер собирается минут 5-6"
63+
// REF: user-request-2026-05-22-controller-build-speed
64+
// SOURCE: n/a
65+
// FORMAT THEOREM: image_rev = local_rev -> build_required = false
66+
// PURITY: CORE
67+
// EFFECT: n/a
68+
// INVARIANT: forced recreation without a matching image requires a rebuild
69+
// COMPLEXITY: O(1)
70+
export const shouldBuildControllerImage = (input: ControllerImageBuildInput): boolean => {
71+
if (input.currentImageRevision === input.localControllerRevision) {
72+
return false
73+
}
74+
75+
return input.currentControllerRevision !== input.localControllerRevision || input.forceRecreateController
76+
}

0 commit comments

Comments
 (0)