Skip to content

Commit f603725

Browse files
authored
Feat: unified deploys for self-hosted and cloud users incl. multi-platform support (#2138)
* remove registry proxy * remove --self-hosted flag * automatically set network build flag * update syncEnvVars debug log * improve switch command * always display deploy errors if they exist * fix stuck deploy command after finalize error * webapp-driven deploys, multi-platform support, lots of fixes * add worker deployment migration * rename image platform env var * only try to sync parent env vars for preview deployments * add KEEP_TMP_DIRS * supervisor: docker api version lock, auth, multi-platform * set image ref on create, validate digest * use metadata for digest, fix local multi-platform builds * print git meta branch before commit * improve push and load flag handling * make runs after local builds compatible with load and push * small improvement for platform overrides * add image platform to dequeued message * remove deprecated init request body fields * fix fail deployment id param * remove build debug logs * pass report merge with no tests * structured run debug logs * add required env var for tests * should not be an error log * add changeset
1 parent a619510 commit f603725

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+951
-985
lines changed

.changeset/ninety-games-grow.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
"trigger.dev": patch
3+
"@trigger.dev/core": patch
4+
---
5+
6+
- Resolve issue where CLI could get stuck during deploy finalization
7+
- Unify local and remote build logic, with multi-platform build support
8+
- Improve switch command; now accepts profile name as an argument
9+
- Registry configuration is now fully managed by the webapp
10+
- The deploy `--self-hosted` flag is no longer required
11+
- Enhance deployment error reporting and image digest retrieval

.env.example

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -80,14 +80,9 @@ CLOUD_SLACK_CLIENT_SECRET=
8080
PROVIDER_SECRET=provider-secret # generate the actual secret with `openssl rand -hex 32`
8181
COORDINATOR_SECRET=coordinator-secret # generate the actual secret with `openssl rand -hex 32`
8282

83-
# Uncomment the following line to enable the registry proxy
84-
# ENABLE_REGISTRY_PROXY=true
83+
# DEPOT_ORG_ID=<Depot org id>
8584
# DEPOT_TOKEN=<Depot org token>
86-
# DEPOT_PROJECT_ID=<Depot project id>
87-
# DEPLOY_REGISTRY_HOST=${APP_ORIGIN} # This is the host that the deploy CLI will use to push images to the registry
88-
# CONTAINER_REGISTRY_ORIGIN=<Container registry origin e.g. https://registry.digitalocean.com>
89-
# CONTAINER_REGISTRY_USERNAME=<Container registry username e.g. Digital ocean email address>
90-
# CONTAINER_REGISTRY_PASSWORD=<Container registry password e.g. Digital ocean PAT>
85+
DEPLOY_REGISTRY_HOST=${APP_ORIGIN} # This is the host that the deploy CLI will use to push images to the registry
9186
# DEV_OTEL_EXPORTER_OTLP_ENDPOINT="http://0.0.0.0:4318"
9287
# These are needed for the object store (for handling large payloads/outputs)
9388
# OBJECT_STORE_BASE_URL="https://{bucket}.{accountId}.r2.cloudflarestorage.com"

.github/workflows/unit-tests-internal.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,4 +127,4 @@ jobs:
127127
merge-multiple: true
128128

129129
- name: Merge reports
130-
run: pnpm dlx vitest@3.1.4 run --merge-reports
130+
run: pnpm dlx vitest@3.1.4 run --merge-reports --pass-with-no-tests

.github/workflows/unit-tests-packages.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,4 +127,4 @@ jobs:
127127
merge-multiple: true
128128

129129
- name: Merge reports
130-
run: pnpm dlx vitest@3.1.4 run --merge-reports
130+
run: pnpm dlx vitest@3.1.4 run --merge-reports --pass-with-no-tests

.github/workflows/unit-tests-webapp.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ jobs:
8686
SESSION_SECRET: "secret"
8787
MAGIC_LINK_SECRET: "secret"
8888
ENCRYPTION_KEY: "secret"
89+
DEPLOY_REGISTRY_HOST: "docker.io"
8990

9091
- name: Gather all reports
9192
if: ${{ !cancelled() }}
@@ -133,4 +134,4 @@ jobs:
133134
merge-multiple: true
134135

135136
- name: Merge reports
136-
run: pnpm dlx vitest@3.1.4 run --merge-reports
137+
run: pnpm dlx vitest@3.1.4 run --merge-reports --pass-with-no-tests

apps/supervisor/src/env.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ const Env = z.object({
2828
RUNNER_HEARTBEAT_INTERVAL_SECONDS: z.coerce.number().optional(),
2929
RUNNER_SNAPSHOT_POLL_INTERVAL_SECONDS: z.coerce.number().optional(),
3030
RUNNER_ADDITIONAL_ENV_VARS: AdditionalEnvVars, // optional (csv)
31+
RUNNER_PRETTY_LOGS: BoolEnv.default(false),
3132
RUNNER_DOCKER_AUTOREMOVE: BoolEnv.default(true),
3233
/**
3334
* Network mode to use for all runners. Supported standard values are: `bridge`, `host`, `none`, and `container:<name|id>`.
@@ -41,6 +42,14 @@ const Env = z.object({
4142
*/
4243
RUNNER_DOCKER_NETWORKS: z.string().default("host"),
4344

45+
// Docker settings
46+
DOCKER_API_VERSION: z.string().default("v1.41"),
47+
DOCKER_PLATFORM: z.string().optional(), // e.g. linux/amd64, linux/arm64
48+
DOCKER_STRIP_IMAGE_DIGEST: BoolEnv.default(true),
49+
DOCKER_REGISTRY_USERNAME: z.string().optional(),
50+
DOCKER_REGISTRY_PASSWORD: z.string().optional(),
51+
DOCKER_REGISTRY_URL: z.string().optional(), // e.g. https://index.docker.io/v1
52+
4453
// Dequeue settings (provider mode)
4554
TRIGGER_DEQUEUE_ENABLED: BoolEnv.default("true"),
4655
TRIGGER_DEQUEUE_INTERVAL_MS: z.coerce.number().int().default(250),

apps/supervisor/src/util.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
1-
export function getDockerHostDomain() {
2-
const isMacOs = process.platform === "darwin";
3-
const isWindows = process.platform === "win32";
1+
import { isMacOS, isWindows } from "std-env";
42

5-
return isMacOs || isWindows ? "host.docker.internal" : "localhost";
3+
export function getDockerHostDomain() {
4+
return isMacOS || isWindows ? "host.docker.internal" : "localhost";
65
}
76

87
export function getRunnerId(runId: string, attemptNumber?: number) {

apps/supervisor/src/workloadManager/docker.ts

Lines changed: 118 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,13 @@ export class DockerWorkloadManager implements WorkloadManager {
1414
private readonly docker: Docker;
1515

1616
private readonly runnerNetworks: string[];
17+
private readonly auth?: Docker.AuthConfig;
18+
private readonly platformOverride?: string;
1719

1820
constructor(private opts: WorkloadManagerOptions) {
19-
this.docker = new Docker();
21+
this.docker = new Docker({
22+
version: env.DOCKER_API_VERSION,
23+
});
2024

2125
if (opts.workloadApiDomain) {
2226
this.logger.warn("⚠️ Custom workload API domain", {
@@ -25,6 +29,29 @@ export class DockerWorkloadManager implements WorkloadManager {
2529
}
2630

2731
this.runnerNetworks = env.RUNNER_DOCKER_NETWORKS.split(",");
32+
33+
this.platformOverride = env.DOCKER_PLATFORM;
34+
if (this.platformOverride) {
35+
this.logger.info("🖥️ Platform override", {
36+
targetPlatform: this.platformOverride,
37+
hostPlatform: process.arch,
38+
});
39+
}
40+
41+
if (env.DOCKER_REGISTRY_USERNAME && env.DOCKER_REGISTRY_PASSWORD && env.DOCKER_REGISTRY_URL) {
42+
this.logger.info("🐋 Using Docker registry credentials", {
43+
username: env.DOCKER_REGISTRY_USERNAME,
44+
url: env.DOCKER_REGISTRY_URL,
45+
});
46+
47+
this.auth = {
48+
username: env.DOCKER_REGISTRY_USERNAME,
49+
password: env.DOCKER_REGISTRY_PASSWORD,
50+
serveraddress: env.DOCKER_REGISTRY_URL,
51+
};
52+
} else {
53+
this.logger.warn("🐋 No Docker registry credentials provided, skipping auth");
54+
}
2855
}
2956

3057
async create(opts: WorkloadManagerCreateOptions) {
@@ -45,6 +72,7 @@ export class DockerWorkloadManager implements WorkloadManager {
4572
`TRIGGER_WORKER_INSTANCE_NAME=${env.TRIGGER_WORKER_INSTANCE_NAME}`,
4673
`OTEL_EXPORTER_OTLP_ENDPOINT=${env.OTEL_EXPORTER_OTLP_ENDPOINT}`,
4774
`TRIGGER_RUNNER_ID=${runnerId}`,
75+
`PRETTY_LOGS=${env.RUNNER_PRETTY_LOGS}`,
4876
];
4977

5078
if (this.opts.warmStartUrl) {
@@ -90,41 +118,103 @@ export class DockerWorkloadManager implements WorkloadManager {
90118
hostConfig.Memory = opts.machine.memory * 1024 * 1024 * 1024;
91119
}
92120

121+
let imageRef = opts.image;
122+
123+
if (env.DOCKER_STRIP_IMAGE_DIGEST) {
124+
imageRef = opts.image.split("@")[0]!;
125+
}
126+
93127
const containerCreateOpts: Docker.ContainerCreateOptions = {
94-
Env: envVars,
95128
name: runnerId,
96129
Hostname: runnerId,
97130
HostConfig: hostConfig,
98-
Image: opts.image,
131+
Image: imageRef,
99132
AttachStdout: false,
100133
AttachStderr: false,
101134
AttachStdin: false,
102135
};
103136

104-
try {
105-
// Create container
106-
const container = await this.docker.createContainer(containerCreateOpts);
137+
if (this.platformOverride) {
138+
containerCreateOpts.platform = this.platformOverride;
139+
}
140+
141+
const logger = this.logger.child({ opts, containerCreateOpts });
142+
143+
const [inspectError, inspectResult] = await tryCatch(this.docker.getImage(imageRef).inspect());
144+
145+
let shouldPull = !!inspectError;
146+
if (this.platformOverride) {
147+
const imageArchitecture = inspectResult?.Architecture;
148+
149+
// When the image architecture doesn't match the platform, we need to pull the image
150+
if (imageArchitecture && !this.platformOverride.includes(imageArchitecture)) {
151+
shouldPull = true;
152+
}
153+
}
154+
155+
// If the image is not present, try to pull it
156+
if (shouldPull) {
157+
logger.info("Pulling image", {
158+
error: inspectError,
159+
image: opts.image,
160+
targetPlatform: this.platformOverride,
161+
imageArchitecture: inspectResult?.Architecture,
162+
});
107163

108-
// If there are multiple networks to attach to we need to attach the remaining ones after creation
109-
if (remainingNetworks.length > 0) {
110-
await this.attachContainerToNetworks({
111-
containerId: container.id,
112-
networkNames: remainingNetworks,
113-
});
164+
// Ensure the image is present
165+
const [createImageError, imageResponseReader] = await tryCatch(
166+
this.docker.createImage(this.auth, {
167+
fromImage: imageRef,
168+
...(this.platformOverride ? { platform: this.platformOverride } : {}),
169+
})
170+
);
171+
if (createImageError) {
172+
logger.error("Failed to pull image", { error: createImageError });
173+
return;
114174
}
115175

116-
// Start container
117-
const startResult = await container.start();
176+
const [imageReadError, imageResponse] = await tryCatch(readAllChunks(imageResponseReader));
177+
if (imageReadError) {
178+
logger.error("failed to read image response", { error: imageReadError });
179+
return;
180+
}
181+
182+
logger.debug("pulled image", { image: opts.image, imageResponse });
183+
} else {
184+
// Image is present, so we can use it to create the container
185+
}
186+
187+
// Create container
188+
const [createContainerError, container] = await tryCatch(
189+
this.docker.createContainer({
190+
...containerCreateOpts,
191+
// Add env vars here so they're not logged
192+
Env: envVars,
193+
})
194+
);
118195

119-
this.logger.debug("create succeeded", {
120-
opts,
121-
startResult,
196+
if (createContainerError) {
197+
logger.error("Failed to create container", { error: createContainerError });
198+
return;
199+
}
200+
201+
// If there are multiple networks to attach to we need to attach the remaining ones after creation
202+
if (remainingNetworks.length > 0) {
203+
await this.attachContainerToNetworks({
122204
containerId: container.id,
123-
containerCreateOpts,
205+
networkNames: remainingNetworks,
124206
});
125-
} catch (error) {
126-
this.logger.error("create failed:", { opts, error, containerCreateOpts });
127207
}
208+
209+
// Start container
210+
const [startError, startResult] = await tryCatch(container.start());
211+
212+
if (startError) {
213+
logger.error("Failed to start container", { error: startError, containerId: container.id });
214+
return;
215+
}
216+
217+
logger.debug("create succeeded", { startResult, containerId: container.id });
128218
}
129219

130220
private async attachContainerToNetworks({
@@ -173,3 +263,11 @@ export class DockerWorkloadManager implements WorkloadManager {
173263
});
174264
}
175265
}
266+
267+
async function readAllChunks(reader: NodeJS.ReadableStream) {
268+
const chunks = [];
269+
for await (const chunk of reader) {
270+
chunks.push(chunk.toString());
271+
}
272+
return chunks;
273+
}

apps/webapp/app/components/GitMetadata.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ export function GitMetadata({ git }: { git?: GitMetaLinks | null }) {
88
return (
99
<>
1010
{git.pullRequestUrl && git.pullRequestNumber && <GitMetadataPullRequest git={git} />}
11-
{git.shortSha && <GitMetadataCommit git={git} />}
1211
{git.branchUrl && <GitMetadataBranch git={git} />}
12+
{git.shortSha && <GitMetadataCommit git={git} />}
1313
</>
1414
);
1515
}

apps/webapp/app/entry.server.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -215,14 +215,20 @@ export { apiRateLimiter } from "./services/apiRateLimit.server";
215215
export { engineRateLimiter } from "./services/engineRateLimit.server";
216216
export { socketIo } from "./v3/handleSocketIo.server";
217217
export { wss } from "./v3/handleWebsockets.server";
218-
export { registryProxy } from "./v3/registryProxy.server";
219218
export { runWithHttpContext } from "./services/httpAsyncStorage.server";
220219
import { eventLoopMonitor } from "./eventLoopMonitor.server";
221220
import { env } from "./env.server";
222221
import { logger } from "./services/logger.server";
223222
import { Prisma } from "./db.server";
224223
import { registerRunEngineEventBusHandlers } from "./v3/runEngineHandlers.server";
224+
import { remoteBuildsEnabled } from "./v3/remoteImageBuilder.server";
225225

226226
if (env.EVENT_LOOP_MONITOR_ENABLED === "1") {
227227
eventLoopMonitor.enable();
228228
}
229+
230+
if (remoteBuildsEnabled()) {
231+
console.log("🏗️ Remote builds enabled");
232+
} else {
233+
console.log("🏗️ Local builds enabled");
234+
}

0 commit comments

Comments
 (0)