feat(docker): Build-time Secrets

This commit is contained in:
Jorge González
2025-10-01 18:24:03 -06:00
parent 9f5c2dbe92
commit 571e97f247
9 changed files with 6719 additions and 19 deletions
@@ -12,6 +12,7 @@ import { api } from "@/utils/api";
const addEnvironmentSchema = z.object({ const addEnvironmentSchema = z.object({
env: z.string(), env: z.string(),
buildArgs: z.string(), buildArgs: z.string(),
buildSecrets: z.string(),
}); });
type EnvironmentSchema = z.infer<typeof addEnvironmentSchema>; type EnvironmentSchema = z.infer<typeof addEnvironmentSchema>;
@@ -37,6 +38,7 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
defaultValues: { defaultValues: {
env: "", env: "",
buildArgs: "", buildArgs: "",
buildSecrets: "",
}, },
resolver: zodResolver(addEnvironmentSchema), resolver: zodResolver(addEnvironmentSchema),
}); });
@@ -44,15 +46,18 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
// Watch form values // Watch form values
const currentEnv = form.watch("env"); const currentEnv = form.watch("env");
const currentBuildArgs = form.watch("buildArgs"); const currentBuildArgs = form.watch("buildArgs");
const currentBuildSecrets = form.watch("buildSecrets");
const hasChanges = const hasChanges =
currentEnv !== (data?.env || "") || currentEnv !== (data?.env || "") ||
currentBuildArgs !== (data?.buildArgs || ""); currentBuildArgs !== (data?.buildArgs || "") ||
currentBuildSecrets !== (data?.buildSecrets || "");
useEffect(() => { useEffect(() => {
if (data) { if (data) {
form.reset({ form.reset({
env: data.env || "", env: data.env || "",
buildArgs: data.buildArgs || "", buildArgs: data.buildArgs || "",
buildSecrets: data.buildSecrets || "",
}); });
} }
}, [data, form]); }, [data, form]);
@@ -61,6 +66,7 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
mutateAsync({ mutateAsync({
env: formData.env, env: formData.env,
buildArgs: formData.buildArgs, buildArgs: formData.buildArgs,
buildSecrets: formData.buildSecrets,
applicationId, applicationId,
}) })
.then(async () => { .then(async () => {
@@ -76,6 +82,7 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
form.reset({ form.reset({
env: data?.env || "", env: data?.env || "",
buildArgs: data?.buildArgs || "", buildArgs: data?.buildArgs || "",
buildSecrets: data?.buildSecrets || "",
}); });
}; };
@@ -104,13 +111,36 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
{data?.buildType === "dockerfile" && ( {data?.buildType === "dockerfile" && (
<Secrets <Secrets
name="buildArgs" name="buildArgs"
title="Build-time Variables" title="Build-time Arguments"
description={ description={
<span> <span>
Available only at build-time. See documentation&nbsp; Arguments are available only at build-time. See
documentation&nbsp;
<a <a
className="text-primary" className="text-primary"
href="https://docs.docker.com/build/guide/build-args/" href="https://docs.docker.com/build/building/variables/"
target="_blank"
rel="noopener noreferrer"
>
here
</a>
.
</span>
}
placeholder="NPM_TOKEN=xyz"
/>
)}
{data?.buildType === "dockerfile" && (
<Secrets
name="buildSecrets"
title="Build-time Secrets"
description={
<span>
Secrets are specially designed for sensitive information and
are only available at build-time. See documentation&nbsp;
<a
className="text-primary"
href="https://docs.docker.com/build/building/secrets/"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
> >
@@ -46,6 +46,7 @@ const schema = z
.object({ .object({
env: z.string(), env: z.string(),
buildArgs: z.string(), buildArgs: z.string(),
buildSecrets: z.string(),
wildcardDomain: z.string(), wildcardDomain: z.string(),
port: z.number(), port: z.number(),
previewLimit: z.number(), previewLimit: z.number(),
@@ -109,6 +110,7 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
form.reset({ form.reset({
env: data.previewEnv || "", env: data.previewEnv || "",
buildArgs: data.previewBuildArgs || "", buildArgs: data.previewBuildArgs || "",
buildSecrets: data.previewBuildSecrets || "",
wildcardDomain: data.previewWildcard || "*.traefik.me", wildcardDomain: data.previewWildcard || "*.traefik.me",
port: data.previewPort || 3000, port: data.previewPort || 3000,
previewLabels: data.previewLabels || [], previewLabels: data.previewLabels || [],
@@ -127,6 +129,7 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
updateApplication({ updateApplication({
previewEnv: formData.env, previewEnv: formData.env,
previewBuildArgs: formData.buildArgs, previewBuildArgs: formData.buildArgs,
previewBuildSecrets: formData.buildSecrets,
previewWildcard: formData.wildcardDomain, previewWildcard: formData.wildcardDomain,
previewPort: formData.port, previewPort: formData.port,
previewLabels: formData.previewLabels, previewLabels: formData.previewLabels,
@@ -467,13 +470,37 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
{data?.buildType === "dockerfile" && ( {data?.buildType === "dockerfile" && (
<Secrets <Secrets
name="buildArgs" name="buildArgs"
title="Build-time Variables" title="Build-time Arguments"
description={ description={
<span> <span>
Available only at build-time. See documentation&nbsp; Arguments are available only at build-time. See
documentation&nbsp;
<a <a
className="text-primary" className="text-primary"
href="https://docs.docker.com/build/guide/build-args/" href="https://docs.docker.com/build/building/variables/"
target="_blank"
rel="noopener noreferrer"
>
here
</a>
.
</span>
}
placeholder="NPM_TOKEN=xyz"
/>
)}
{data?.buildType === "dockerfile" && (
<Secrets
name="buildSecrets"
title="Build-time Secrets"
description={
<span>
Secrets are specially designed for sensitive information
and are only available at build-time. See
documentation&nbsp;
<a
className="text-primary"
href="https://docs.docker.com/build/building/secrets/"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
> >
@@ -0,0 +1,2 @@
ALTER TABLE "application" ADD COLUMN "previewBuildSecrets" text;--> statement-breakpoint
ALTER TABLE "application" ADD COLUMN "buildSecrets" text;
File diff suppressed because it is too large Load Diff
+7
View File
@@ -799,6 +799,13 @@
"when": 1758960816504, "when": 1758960816504,
"tag": "0113_complete_rafael_vega", "tag": "0113_complete_rafael_vega",
"breakpoints": true "breakpoints": true
},
{
"idx": 114,
"version": "7",
"when": 1759360386227,
"tag": "0114_left_smasher",
"breakpoints": true
} }
] ]
} }
@@ -360,6 +360,7 @@ export const applicationRouter = createTRPCRouter({
await updateApplication(input.applicationId, { await updateApplication(input.applicationId, {
env: input.env, env: input.env,
buildArgs: input.buildArgs, buildArgs: input.buildArgs,
buildSecrets: input.buildSecrets,
}); });
return true; return true;
}), }),
@@ -80,6 +80,7 @@ export const applications = pgTable("application", {
previewEnv: text("previewEnv"), previewEnv: text("previewEnv"),
watchPaths: text("watchPaths").array(), watchPaths: text("watchPaths").array(),
previewBuildArgs: text("previewBuildArgs"), previewBuildArgs: text("previewBuildArgs"),
previewBuildSecrets: text("previewBuildSecrets"),
previewLabels: text("previewLabels").array(), previewLabels: text("previewLabels").array(),
previewWildcard: text("previewWildcard"), previewWildcard: text("previewWildcard"),
previewPort: integer("previewPort").default(3000), previewPort: integer("previewPort").default(3000),
@@ -99,6 +100,7 @@ export const applications = pgTable("application", {
).default(true), ).default(true),
rollbackActive: boolean("rollbackActive").default(false), rollbackActive: boolean("rollbackActive").default(false),
buildArgs: text("buildArgs"), buildArgs: text("buildArgs"),
buildSecrets: text("buildSecrets"),
memoryReservation: text("memoryReservation"), memoryReservation: text("memoryReservation"),
memoryLimit: text("memoryLimit"), memoryLimit: text("memoryLimit"),
cpuReservation: text("cpuReservation"), cpuReservation: text("cpuReservation"),
@@ -252,6 +254,7 @@ const createSchema = createInsertSchema(applications, {
autoDeploy: z.boolean(), autoDeploy: z.boolean(),
env: z.string().optional(), env: z.string().optional(),
buildArgs: z.string().optional(), buildArgs: z.string().optional(),
buildSecrets: z.string().optional(),
name: z.string().min(1), name: z.string().min(1),
description: z.string().optional(), description: z.string().optional(),
memoryReservation: z.string().optional(), memoryReservation: z.string().optional(),
@@ -303,6 +306,7 @@ const createSchema = createInsertSchema(applications, {
previewPort: z.number().optional(), previewPort: z.number().optional(),
previewEnv: z.string().optional(), previewEnv: z.string().optional(),
previewBuildArgs: z.string().optional(), previewBuildArgs: z.string().optional(),
previewBuildSecrets: z.string().optional(),
previewWildcard: z.string().optional(), previewWildcard: z.string().optional(),
previewLimit: z.number().optional(), previewLimit: z.number().optional(),
previewHttps: z.boolean().optional(), previewHttps: z.boolean().optional(),
@@ -456,6 +460,7 @@ export const apiSaveEnvironmentVariables = createSchema
applicationId: true, applicationId: true,
env: true, env: true,
buildArgs: true, buildArgs: true,
buildSecrets: true,
}) })
.required(); .required();
@@ -473,6 +473,7 @@ export const deployPreviewApplication = async ({
application.appName = previewDeployment.appName; application.appName = previewDeployment.appName;
application.env = `${application.previewEnv}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`; application.env = `${application.previewEnv}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`;
application.buildArgs = application.previewBuildArgs; application.buildArgs = application.previewBuildArgs;
application.buildSecrets = application.previewBuildSecrets;
if (application.sourceType === "github") { if (application.sourceType === "github") {
await cloneGithubRepository({ await cloneGithubRepository({
@@ -580,6 +581,7 @@ export const deployRemotePreviewApplication = async ({
application.appName = previewDeployment.appName; application.appName = previewDeployment.appName;
application.env = `${application.previewEnv}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`; application.env = `${application.previewEnv}\nDOKPLOY_DEPLOY_URL=${previewDeployment?.domain?.host}`;
application.buildArgs = application.previewBuildArgs; application.buildArgs = application.previewBuildArgs;
application.buildSecrets = application.previewBuildSecrets;
if (application.serverId) { if (application.serverId) {
let command = "set -e;"; let command = "set -e;";
@@ -1,5 +1,8 @@
import type { WriteStream } from "node:fs"; import type { WriteStream } from "node:fs";
import { prepareEnvironmentVariables } from "@dokploy/server/utils/docker/utils"; import {
getEnviromentVariablesObject,
prepareEnvironmentVariables,
} from "@dokploy/server/utils/docker/utils";
import { import {
getBuildAppDirectory, getBuildAppDirectory,
getDockerContextPath, getDockerContextPath,
@@ -17,6 +20,7 @@ export const buildCustomDocker = async (
env, env,
publishDirectory, publishDirectory,
buildArgs, buildArgs,
buildSecrets,
dockerBuildStage, dockerBuildStage,
cleanCache, cleanCache,
} = application; } = application;
@@ -26,11 +30,6 @@ export const buildCustomDocker = async (
const defaultContextPath = const defaultContextPath =
dockerFilePath.substring(0, dockerFilePath.lastIndexOf("/") + 1) || "."; dockerFilePath.substring(0, dockerFilePath.lastIndexOf("/") + 1) || ".";
const args = prepareEnvironmentVariables(
buildArgs,
application.environment.project.env,
application.environment.env,
);
const dockerContextPath = getDockerContextPath(application); const dockerContextPath = getDockerContextPath(application);
@@ -44,9 +43,29 @@ export const buildCustomDocker = async (
commandArgs.push("--target", dockerBuildStage); commandArgs.push("--target", dockerBuildStage);
} }
const args = prepareEnvironmentVariables(
buildArgs,
application.environment.project.env,
application.environment.env,
);
for (const arg of args) { for (const arg of args) {
commandArgs.push("--build-arg", arg); commandArgs.push("--build-arg", arg);
} }
const secrets = getEnviromentVariablesObject(
buildSecrets,
application.environment.project.env,
application.environment.env,
);
for (const key in secrets) {
// Although buildx is smart enough to know we may be referring to an environment variable name,
// we still make sure it doesn't fall back to type=file.
// See: https://docs.docker.com/reference/cli/docker/buildx/build/#secret
commandArgs.push("--secret", `type=env,id=${key}`);
}
/* /*
Do not generate an environment file when publishDirectory is specified, Do not generate an environment file when publishDirectory is specified,
as it could be publicly exposed. as it could be publicly exposed.
@@ -70,6 +89,10 @@ export const buildCustomDocker = async (
}, },
{ {
cwd: dockerContextPath || defaultContextPath, cwd: dockerContextPath || defaultContextPath,
env: {
...process.env,
...secrets,
},
}, },
); );
} catch (error) { } catch (error) {
@@ -86,6 +109,7 @@ export const getDockerCommand = (
env, env,
publishDirectory, publishDirectory,
buildArgs, buildArgs,
buildSecrets,
dockerBuildStage, dockerBuildStage,
cleanCache, cleanCache,
} = application; } = application;
@@ -96,11 +120,6 @@ export const getDockerCommand = (
const defaultContextPath = const defaultContextPath =
dockerFilePath.substring(0, dockerFilePath.lastIndexOf("/") + 1) || "."; dockerFilePath.substring(0, dockerFilePath.lastIndexOf("/") + 1) || ".";
const args = prepareEnvironmentVariables(
buildArgs,
application.environment.project.env,
application.environment.env,
);
const dockerContextPath = const dockerContextPath =
getDockerContextPath(application) || defaultContextPath; getDockerContextPath(application) || defaultContextPath;
@@ -115,10 +134,33 @@ export const getDockerCommand = (
commandArgs.push("--no-cache"); commandArgs.push("--no-cache");
} }
const args = prepareEnvironmentVariables(
buildArgs,
application.environment.project.env,
application.environment.env,
);
for (const arg of args) { for (const arg of args) {
commandArgs.push("--build-arg", `'${arg}'`); commandArgs.push("--build-arg", `'${arg}'`);
} }
const secrets = getEnviromentVariablesObject(
buildSecrets,
application.environment.project.env,
application.environment.env,
);
const joinedSecrets = Object.entries(secrets)
.map(([key, value]) => `${key}='${value}'`)
.join(" ");
for (const key in secrets) {
// Although buildx is smart enough to know we may be referring to an environment variable name,
// we still make sure it doesn't fall back to `type=file`.
// See: https://docs.docker.com/reference/cli/docker/buildx/build/#secret
commandArgs.push("--secret", `type=env,id=${key}`);
}
/* /*
Do not generate an environment file when publishDirectory is specified, Do not generate an environment file when publishDirectory is specified,
as it could be publicly exposed. as it could be publicly exposed.
@@ -140,7 +182,7 @@ cd ${dockerContextPath} >> ${logPath} 2>> ${logPath} || {
exit 1; exit 1;
} }
docker ${commandArgs.join(" ")} >> ${logPath} 2>> ${logPath} || { ${joinedSecrets} docker ${commandArgs.join(" ")} >> ${logPath} 2>> ${logPath} || {
echo "❌ Docker build failed" >> ${logPath}; echo "❌ Docker build failed" >> ${logPath};
exit 1; exit 1;
} }