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
16 changes: 16 additions & 0 deletions lib/docker-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1121,6 +1121,7 @@ export class DockerClient {
* @param options.target Target build stage
* @param options.outputs BuildKit output configuration in the format of a stringified JSON array of objects. Each object must have two top-level properties: `Type` and `Attrs`. The `Type` property must be set to \'moby\'. The `Attrs` property is a map of attributes for the BuildKit output configuration. See https://docs.docker.com/build/exporters/oci-docker/ for more information. Example: ``` [{\"Type\":\"moby\",\"Attrs\":{\"type\":\"image\",\"force-compression\":\"true\",\"compression\":\"zstd\"}}] ```
* @param options.version Version of the builder backend to use. - `1` is the first generation classic (deprecated) builder in the Docker daemon (default) - `2` is [BuildKit](https://github.com/moby/buildkit)
* @param options.secrets BuildKit secrets to pass to the build. A record mapping secret IDs to their values. Secrets are exposed in the build at `/run/secrets/<id>` when using `RUN --mount=type=secret,id=<id>` in the Dockerfile. Requires BuildKit (version: `2`). For more information, see https://docs.docker.com/build/building/secrets/
*/
public imageBuild(
buildContext: ReadableStream,
Expand Down Expand Up @@ -1151,6 +1152,7 @@ export class DockerClient {
target?: string;
outputs?: string;
version?: '1' | '2';
secrets?: Record<string, string>;
},
): JSONMessages<JSONMessage, string> {
const headers: Record<string, string> = {};
Expand All @@ -1162,6 +1164,19 @@ export class DockerClient {
);
}

// Prepare secrets parameter for BuildKit
let secretsParam: string | undefined;
if (options?.secrets) {
// Convert secrets to BuildKit format: array of secret specs
const secretSpecs = Object.entries(options.secrets).map(
([id, value]) => ({
ID: id,
Source: value,
}),
);
secretsParam = JSON.stringify(secretSpecs);
}

const request = this.api.post(
'/build',
{
Expand Down Expand Up @@ -1190,6 +1205,7 @@ export class DockerClient {
target: options?.target,
outputs: options?.outputs,
version: options?.version || '2',
secret: secretsParam,
Copy link
Collaborator

@ndeloof ndeloof Nov 3, 2025

Choose a reason for hiding this comment

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

},
buildContext,
headers,
Expand Down
73 changes: 73 additions & 0 deletions test/build.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,76 @@ COPY test.txt /test.txt
}
},
);

test(
'imageBuild: build image with BuildKit secrets',
{ timeout: 60000 },
async () => {
const client = await DockerClient.fromDockerConfig();
const testImageName = 'test-build-secrets-image';
const testTag = 'latest';
const testSecret = 'my-test-secret-value-12345';

try {
const pack = createTarPack();
pack.entry(
{ name: 'Dockerfile' },
`FROM alpine:latest
# Use a secret during build without including it in the final image
RUN --mount=type=secret,id=test_secret \\
if [ -f /run/secrets/test_secret ]; then \\
echo "Secret found and mounted successfully"; \\
cat /run/secrets/test_secret > /tmp/secret_check; \\
else \\
echo "ERROR: Secret not found at /run/secrets/test_secret"; \\
exit 1; \\
fi
# Verify secret was available but not in final image
RUN test ! -f /run/secrets/test_secret && echo "Secret not in final layer (good!)"
`,
);
pack.finalize();

console.log(' Building image with BuildKit secrets...');
const builtImage = await client
.imageBuild(
Readable.toWeb(pack, {
strategy: { highWaterMark: 16384 },
}),
{
tag: `${testImageName}:${testTag}`,
rm: true,
forcerm: true,
version: '2', // BuildKit required for secrets
secrets: {
test_secret: testSecret,
},
},
)
.wait();

console.log(` Inspecting built image ${builtImage}`);
const imageInspect = await client.imageInspect(builtImage || '');
console.log(' Image with secrets built successfully!');

assert.notStrictEqual(
imageInspect.RepoTags?.includes(`${testImageName}:${testTag}`),
false,
);
console.log(` Image size: ${imageInspect.Size} bytes`);
} finally {
// Clean up: delete the test image
console.log(' Cleaning up test image...');
try {
await client.imageDelete(`${testImageName}:${testTag}`, {
force: true,
});
console.log(' Test image deleted successfully');
} catch (cleanupError) {
console.log(
` Warning: Failed to delete test image: ${(cleanupError as any)?.message}`,
);
}
}
},
);