feat(applications): add support for Docker build secrets

- Implement build secrets functionality for Dockerfile builds
- Add new `buildSecrets` field to application schema
- Update UI and backend to handle build-time secrets
- Modify Docker build process to support secret injection during build
This commit is contained in:
Mauricio Siu
2025-03-08 19:06:14 -06:00
parent 62bd8e3c95
commit c7f44f65bc
8 changed files with 5257 additions and 28 deletions

View File

@@ -11,6 +11,7 @@ import { z } from "zod";
const addEnvironmentSchema = z.object({
env: z.string(),
buildArgs: z.string(),
buildSecrets: z.record(z.string(), z.string()),
});
type EnvironmentSchema = z.infer<typeof addEnvironmentSchema>;
@@ -36,6 +37,7 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
defaultValues: {
env: data?.env || "",
buildArgs: data?.buildArgs || "",
buildSecrets: data?.buildSecrets || {},
},
resolver: zodResolver(addEnvironmentSchema),
});
@@ -44,6 +46,7 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
mutateAsync({
env: data.env,
buildArgs: data.buildArgs,
buildSecrets: data.buildSecrets,
applicationId,
})
.then(async () => {
@@ -69,25 +72,63 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
placeholder={["NODE_ENV=production", "PORT=3000"].join("\n")}
/>
{data?.buildType === "dockerfile" && (
<Secrets
name="buildArgs"
title="Build-time Variables"
description={
<span>
Available only at build-time. See documentation&nbsp;
<a
className="text-primary"
href="https://docs.docker.com/build/guide/build-args/"
target="_blank"
rel="noopener noreferrer"
>
here
</a>
.
</span>
}
placeholder="NPM_TOKEN=xyz"
/>
<>
<Secrets
name="buildArgs"
title="Build-time Variables"
description={
<span>
Available only at build-time. See documentation&nbsp;
<a
className="text-primary"
href="https://docs.docker.com/build/guide/build-args/"
target="_blank"
rel="noopener noreferrer"
>
here
</a>
.
</span>
}
placeholder="NPM_TOKEN=xyz"
/>
<Secrets
name="buildSecrets"
title="Build Secrets"
description={
<span>
Secrets available only during build-time and not in the
final image. See documentation&nbsp;
<a
className="text-primary"
href="https://docs.docker.com/build/building/secrets/"
target="_blank"
rel="noopener noreferrer"
>
here
</a>
.
</span>
}
placeholder="API_TOKEN=xyz"
transformValue={(value) => {
// Convert the string format to object
const lines = value.split("\n").filter((line) => line.trim());
return Object.fromEntries(
lines.map((line) => {
const [key, ...valueParts] = line.split("=");
return [key.trim(), valueParts.join("=").trim()];
}),
);
}}
formatValue={(value) => {
// Convert the object back to string format
return Object.entries(value as Record<string, string>)
.map(([key, val]) => `${key}=${val}`)
.join("\n");
}}
/>
</>
)}
<div className="flex flex-row justify-end">
<Button isLoading={isLoading} className="w-fit" type="submit">

View File

@@ -0,0 +1,2 @@
ALTER TABLE "application" ADD COLUMN "buildSecrets" text;--> statement-breakpoint
ALTER TABLE "user_temp" DROP COLUMN "enableLogRotation";

File diff suppressed because it is too large Load Diff

View File

@@ -505,6 +505,13 @@
"when": 1741460060541,
"tag": "0071_flaky_black_queen",
"breakpoints": true
},
{
"idx": 72,
"version": "7",
"when": 1741481694393,
"tag": "0072_milky_lyja",
"breakpoints": true
}
]
}

View File

@@ -298,6 +298,7 @@ export const applicationRouter = createTRPCRouter({
await updateApplication(input.applicationId, {
env: input.env,
buildArgs: input.buildArgs,
buildSecrets: input.buildSecrets,
});
return true;
}),

View File

@@ -129,6 +129,7 @@ export const applications = pgTable("application", {
false,
),
buildArgs: text("buildArgs"),
buildSecrets: json("buildSecrets").$type<Record<string, string>>(),
memoryReservation: text("memoryReservation"),
memoryLimit: text("memoryLimit"),
cpuReservation: text("cpuReservation"),
@@ -353,6 +354,7 @@ const createSchema = createInsertSchema(applications, {
autoDeploy: z.boolean(),
env: z.string().optional(),
buildArgs: z.string().optional(),
buildSecrets: z.record(z.string(), z.string()).optional(),
name: z.string().min(1),
description: z.string().optional(),
memoryReservation: z.string().optional(),
@@ -499,11 +501,12 @@ export const apiSaveGitProvider = createSchema
}),
);
export const apiSaveEnvironmentVariables = createSchema
.pick({
applicationId: true,
env: true,
buildArgs: true,
export const apiSaveEnvironmentVariables = z
.object({
applicationId: z.string(),
env: z.string().optional(),
buildArgs: z.string().optional(),
buildSecrets: z.record(z.string(), z.string()).optional(),
})
.required();

View File

@@ -149,6 +149,7 @@ table application {
previewLimit integer [default: 3]
isPreviewDeploymentsActive boolean [default: false]
buildArgs text
buildSecrets json
memoryReservation text
memoryLimit text
cpuReservation text

View File

@@ -12,8 +12,14 @@ export const buildCustomDocker = async (
application: ApplicationNested,
writeStream: WriteStream,
) => {
const { appName, env, publishDirectory, buildArgs, dockerBuildStage } =
application;
const {
appName,
env,
publishDirectory,
buildArgs,
buildSecrets,
dockerBuildStage,
} = application;
const dockerFilePath = getBuildAppDirectory(application);
try {
const image = `${appName}`;
@@ -25,6 +31,10 @@ export const buildCustomDocker = async (
application.project.env,
);
const secrets = buildSecrets
? Object.entries(buildSecrets).map(([key, value]) => `${key}=${value}`)
: [];
const dockerContextPath = getDockerContextPath(application);
const commandArgs = ["build", "-t", image, "-f", dockerFilePath, "."];
@@ -36,6 +46,12 @@ export const buildCustomDocker = async (
for (const arg of args) {
commandArgs.push("--build-arg", arg);
}
for (const secret of secrets) {
const [key] = secret.split("=");
commandArgs.push("--secret", `id=${key},env=${key}`);
}
/*
Do not generate an environment file when publishDirectory is specified,
as it could be publicly exposed.
@@ -54,6 +70,10 @@ export const buildCustomDocker = async (
},
{
cwd: dockerContextPath || defaultContextPath,
env: {
...process.env,
...Object.fromEntries(secrets.map((s) => s.split("="))),
},
},
);
} catch (error) {
@@ -65,8 +85,14 @@ export const getDockerCommand = (
application: ApplicationNested,
logPath: string,
) => {
const { appName, env, publishDirectory, buildArgs, dockerBuildStage } =
application;
const {
appName,
env,
publishDirectory,
buildArgs,
buildSecrets,
dockerBuildStage,
} = application;
const dockerFilePath = getBuildAppDirectory(application);
try {
@@ -79,6 +105,10 @@ export const getDockerCommand = (
application.project.env,
);
const secrets = buildSecrets
? Object.entries(buildSecrets).map(([key, value]) => `${key}=${value}`)
: [];
const dockerContextPath =
getDockerContextPath(application) || defaultContextPath;
@@ -92,6 +122,11 @@ export const getDockerCommand = (
commandArgs.push("--build-arg", arg);
}
for (const secret of secrets) {
const [key] = secret.split("=");
commandArgs.push("--secret", `id=${key},env=${key}`);
}
/*
Do not generate an environment file when publishDirectory is specified,
as it could be publicly exposed.
@@ -105,6 +140,14 @@ export const getDockerCommand = (
);
}
// Export secrets as environment variables
if (secrets.length > 0) {
command += "\n# Export build secrets\n";
for (const secret of secrets) {
command += `export ${secret}\n`;
}
}
command += `
echo "Building ${appName}" >> ${logPath};
cd ${dockerContextPath} >> ${logPath} 2>> ${logPath} || {