Merge pull request #3907 from Dokploy/canary

🚀 Release v0.28.4
This commit is contained in:
Mauricio Siu
2026-03-06 11:51:08 -06:00
committed by GitHub
14 changed files with 91 additions and 55 deletions

View File

@@ -24,7 +24,6 @@ import {
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { authClient } from "@/lib/auth-client";
import { api } from "@/utils/api";
const organizationSchema = z.object({
@@ -55,8 +54,6 @@ export function AddOrganization({ organizationId }: Props) {
const { mutateAsync, isPending } = organizationId
? api.organization.update.useMutation()
: api.organization.create.useMutation();
const { refetch: refetchActiveOrganization } =
authClient.useActiveOrganization();
const form = useForm<OrganizationFormValues>({
resolver: zodResolver(organizationSchema),
@@ -89,7 +86,7 @@ export function AddOrganization({ organizationId }: Props) {
utils.organization.all.invalidate();
if (organizationId) {
utils.organization.one.invalidate({ organizationId });
refetchActiveOrganization();
utils.organization.active.invalidate();
}
setOpen(false);
})

View File

@@ -17,7 +17,8 @@ import { api } from "@/utils/api";
export const AddGithubProvider = () => {
const [isOpen, setIsOpen] = useState(false);
const { data: activeOrganization } = authClient.useActiveOrganization();
const { data: activeOrganization } = api.organization.active.useQuery();
const { data: session } = authClient.useSession();
const { data } = api.user.get.useQuery();
const [manifest, setManifest] = useState("");
@@ -52,7 +53,7 @@ export const AddGithubProvider = () => {
);
setManifest(manifest);
}, [data?.id, activeOrganization?.id, session?.user?.id]);
}, [activeOrganization?.id, session?.user?.id]);
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
@@ -131,11 +132,7 @@ export const AddGithubProvider = () => {
Unsure if you already have an app?
</a>
<Button
disabled={
(isOrganization && organizationName.length < 1) ||
!activeOrganization?.id ||
!session?.user?.id
}
disabled={isOrganization && organizationName.length < 1}
type="submit"
className="self-end"
>

View File

@@ -55,7 +55,7 @@ export const AddInvitation = () => {
api.notification.getEmailProviders.useQuery();
const { mutateAsync: sendInvitation } = api.user.sendInvitation.useMutation();
const [error, setError] = useState<string | null>(null);
const { data: activeOrganization } = authClient.useActiveOrganization();
const { data: activeOrganization } = api.organization.active.useQuery();
const form = useForm<AddInvitation>({
defaultValues: {

View File

@@ -557,8 +557,7 @@ function SidebarLogo() {
const { mutateAsync: setDefaultOrganization, isPending: isSettingDefault } =
api.organization.setDefault.useMutation();
const { isMobile } = useSidebar();
const { data: activeOrganization } = authClient.useActiveOrganization();
const _utils = api.useUtils();
const { data: activeOrganization } = api.organization.active.useQuery();
const { data: invitations, refetch: refetchInvitations } =
api.user.getInvitations.useQuery();

View File

@@ -1,6 +1,6 @@
{
"name": "dokploy",
"version": "v0.28.3",
"version": "v0.28.4",
"private": true,
"license": "Apache-2.0",
"type": "module",

View File

@@ -69,8 +69,7 @@ export const mountRouter = createTRPCRouter({
create: protectedProcedure
.input(apiCreateMount)
.mutation(async ({ input }) => {
await createMount(input);
return true;
return await createMount(input);
}),
remove: protectedProcedure
.input(apiRemoveMount)

View File

@@ -355,4 +355,13 @@ export const organizationRouter = createTRPCRouter({
return { success: true };
}),
active: protectedProcedure.query(async ({ ctx }) => {
if (!ctx.session.activeOrganizationId) {
return null;
}
return await db.query.organization.findFirst({
where: eq(organization.id, ctx.session.activeOrganizationId),
});
}),
});

View File

@@ -2,8 +2,24 @@ import path from "node:path";
import Docker from "dockerode";
export const IS_CLOUD = process.env.IS_CLOUD === "true";
export const DOCKER_API_VERSION = process.env.DOCKER_API_VERSION;
export const DOCKER_HOST = process.env.DOCKER_HOST;
export const DOCKER_PORT = process.env.DOCKER_PORT
? Number(process.env.DOCKER_PORT)
: undefined;
export const CLEANUP_CRON_JOB = "50 23 * * *";
export const docker = new Docker();
export const docker = new Docker({
...(DOCKER_API_VERSION && {
version: DOCKER_API_VERSION,
}),
...(DOCKER_HOST && {
host: DOCKER_HOST,
}),
...(DOCKER_PORT && {
port: DOCKER_PORT,
}),
});
// When not set, use the legacy default so 2FA remains working for users who
// enabled it before BETTER_AUTH_SECRET was introduced .

View File

@@ -365,12 +365,13 @@ const createSchema = createInsertSchema(applications, {
previewPath: z.string().optional(),
previewCertificateType: z.enum(["letsencrypt", "none", "custom"]).optional(),
previewRequireCollaboratorPermissions: z.boolean().optional(),
watchPaths: z.array(z.string()).optional(),
watchPaths: z.array(z.string()).optional().optional(),
previewLabels: z.array(z.string()).optional(),
cleanCache: z.boolean().optional(),
stopGracePeriodSwarm: z.bigint().nullable(),
endpointSpecSwarm: EndpointSpecSwarmSchema.nullable(),
ulimitsSwarm: UlimitsSwarmSchema.nullable(),
enableSubmodules: z.boolean().optional(),
});
export const apiCreateApplication = createSchema.pick({
@@ -433,13 +434,13 @@ export const apiSaveGithubProvider = createSchema
owner: true,
buildPath: true,
githubId: true,
watchPaths: true,
enableSubmodules: true,
})
.required()
.extend({
triggerType: z.enum(["push", "tag"]).default("push"),
});
})
.required()
.merge(createSchema.pick({ enableSubmodules: true, watchPaths: true }));
export const apiSaveGitlabProvider = createSchema
.pick({
@@ -451,10 +452,9 @@ export const apiSaveGitlabProvider = createSchema
gitlabId: true,
gitlabProjectId: true,
gitlabPathNamespace: true,
watchPaths: true,
enableSubmodules: true,
})
.required();
.required()
.merge(createSchema.pick({ enableSubmodules: true, watchPaths: true }));
export const apiSaveBitbucketProvider = createSchema
.pick({
@@ -465,10 +465,9 @@ export const apiSaveBitbucketProvider = createSchema
bitbucketRepositorySlug: true,
bitbucketId: true,
applicationId: true,
watchPaths: true,
enableSubmodules: true,
})
.required();
.required()
.merge(createSchema.pick({ enableSubmodules: true, watchPaths: true }));
export const apiSaveGiteaProvider = createSchema
.pick({
@@ -478,10 +477,9 @@ export const apiSaveGiteaProvider = createSchema
giteaOwner: true,
giteaRepository: true,
giteaId: true,
watchPaths: true,
enableSubmodules: true,
})
.required();
.required()
.merge(createSchema.pick({ enableSubmodules: true, watchPaths: true }));
export const apiSaveDockerProvider = createSchema
.pick({
@@ -506,6 +504,7 @@ export const apiSaveGitProvider = createSchema
.merge(
createSchema.pick({
customGitSSHKeyId: true,
enableSubmodules: true,
}),
);

View File

@@ -99,17 +99,15 @@ const createSchema = createInsertSchema(mounts, {
mountPath: z.string().min(1),
mountId: z.string().optional(),
filePath: z.string().optional(),
serviceType: z
.enum([
"application",
"postgres",
"mysql",
"mariadb",
"mongo",
"redis",
"compose",
])
.default("application"),
serviceType: z.enum([
"application",
"postgres",
"mysql",
"mariadb",
"mongo",
"redis",
"compose",
]),
});
export type ServiceType = NonNullable<

View File

@@ -16,7 +16,7 @@ function shEscape(s: string | undefined): string {
return `'${s.replace(/'/g, `'\\''`)}'`;
}
function safeDockerLoginCommand(
export function safeDockerLoginCommand(
registry: string | undefined,
user: string | undefined,
pass: string | undefined,

View File

@@ -23,7 +23,7 @@ import { findDeploymentById } from "./deployment";
import type { Mount } from "./mount";
import type { Port } from "./port";
import type { Project } from "./project";
import type { Registry } from "./registry";
import { type Registry, safeDockerLoginCommand } from "./registry";
export const createRollback = async (
input: z.infer<typeof createRollbackSchema>,
@@ -111,7 +111,7 @@ const deleteRollbackImage = async (image: string, serverId?: string | null) => {
const command = `docker image rm ${image} --force`;
if (serverId) {
await execAsyncRemote(command, serverId);
await execAsyncRemote(serverId, command);
} else {
await execAsync(command);
}
@@ -171,6 +171,23 @@ export const rollback = async (rollbackId: string) => {
);
};
const dockerLoginForRegistry = async (
registry: Registry,
serverId?: string | null,
) => {
const loginCommand = safeDockerLoginCommand(
registry.registryUrl,
registry.username,
registry.password,
);
if (serverId) {
await execAsyncRemote(serverId, loginCommand);
} else {
await execAsync(loginCommand);
}
};
const rollbackApplication = async (
appName: string,
image: string,
@@ -188,6 +205,14 @@ const rollbackApplication = async (
throw new Error("Full context is required for rollback");
}
// Ensure Docker daemon is authenticated with the rollback registry
// before updating the swarm service. The authconfig in CreateServiceOptions
// alone is not sufficient — Docker Swarm also relies on the daemon's
// cached credentials (~/.docker/config.json) to distribute auth to nodes.
if (fullContext.rollbackRegistry) {
await dockerLoginForRegistry(fullContext.rollbackRegistry, serverId);
}
const docker = await getRemoteDocker(serverId);
// Use the same configuration as mechanizeDockerContainer

View File

@@ -67,7 +67,7 @@ export const runWebServerBackup = async (backup: BackupSchedule) => {
await execAsync(cleanupCommand);
await execAsync(
`rsync -a --ignore-errors ${BASE_PATH}/ ${tempDir}/filesystem/`,
`rsync -a --ignore-errors --no-specials --no-devices ${BASE_PATH}/ ${tempDir}/filesystem/`,
);
writeStream.write("Copied filesystem to temp directory\n");

View File

@@ -152,16 +152,13 @@ export const createRouterConfig = async (
}
if ((entryPoint === "websecure" && https) || !https) {
// redirects
for (const redirect of redirects) {
let middlewareName = `redirect-${appName}-${redirect.uniqueConfigKey}`;
if (domain.domainType === "preview") {
middlewareName = `redirect-${appName.replace(
/^preview-(.+)-[^-]+$/,
"$1",
)}-${redirect.uniqueConfigKey}`;
// redirects - skip for preview deployments as wildcard subdomains
// should not inherit parent redirect rules (e.g., www-redirect)
if (domain.domainType !== "preview") {
for (const redirect of redirects) {
const middlewareName = `redirect-${appName}-${redirect.uniqueConfigKey}`;
routerConfig.middlewares?.push(middlewareName);
}
routerConfig.middlewares?.push(middlewareName);
}
// security