From f6eeccf9aa8897a5cc5534faa3903cb4394a280a Mon Sep 17 00:00:00 2001
From: merge-check <140916359+agentHits@users.noreply.github.com>
Date: Thu, 8 Oct 2026 07:57:15 -0400
Subject: [PATCH 01/33] feat: installer hardening flags and one source for
pinned versions
Installer hardening (opt-in, Linux only):
- install-agenthits.sh accepts --harden, --ufw, --ssh-keys-only and
--fail2ban, plus a harden mode for a host that is already installed.
The same switches are read as HARDEN_SSH, HARDEN_UFW and HARDEN_FAIL2BAN,
so the macOS (OrbStack machine) and Windows (WSL) launchers pass them on.
- SSH keys-only writes the main sshd_config, because the Security tab reads
that file and not drop-ins. It refuses to run without a valid root key and
skips hosts that have no sshd.
- UFW denies incoming traffic except SSH, 80, 443 (tcp, and udp for HTTP/3)
and keeps forwarding enabled for Docker. Fail2Ban runs the sshd jail in
aggressive mode from /etc/fail2ban/jail.local. UFW and Fail2Ban are
Debian/Ubuntu only and are skipped elsewhere.
Panel-driven server setup:
- DOKPLOY_SERVER_HARDENING (ssh, ufw, fail2ban or all) applies the same
hardening as the last step of Setup Server. It is off by default. The
installer sets it from its flags.
One source for component versions:
- packages/server/src/setup/component-versions.ts (PINNED_VERSIONS) pins
Node 24.21.0, Docker 29.8.2, Traefik 3.7.14, Postgres 18.6, Redis 8.10.2,
Nixpacks 1.41.0, Railpack 0.40.1, pack 0.40.9 and RClone 1.75.1.
- The installer defaults, remote server setup, both Dockerfiles, CI, and the
application Railpack picker read these values. component-versions-sync
fails when a file drifts from the object.
- Remote setup installs the pinned RClone release archive. The Ubuntu 26.04
Docker repin to 29.4.2 is gone, because 29.8.2 is published for noble and
resolute on amd64 and arm64.
- Setup Server still installs only what is missing. A Components button on
each server compares installed and pinned versions and updates Docker,
Traefik, RClone, Nixpacks, Railpack and pack. Docker updates restart the
daemon and Traefik is recreated; the dialog warns about both.
- The panel installer installs Nixpacks, Railpack, pack and RClone on the
host at the pinned versions.
Fix:
- Nixpacks and Railpack installed through sudo ignored the pinned version,
because sudo resets the environment. The version is passed with env now.
Docs: docs/server-hardening.md (flags, rollback, removing a server) and
docs/server-components-update.md (pins, update behaviour, limits).
Co-Authored-By: Claude Haiku 5.5
---
.github/workflows/dokploy.yml | 2 +-
.github/workflows/pull-request.yml | 6 +-
.github/workflows/sync-openapi-docs.yml | 2 +-
Dockerfile | 18 +-
Dockerfile.cloud | 2 +-
Dockerfile.schedule | 2 +-
Dockerfile.server | 2 +-
.../server/component-versions-sync.test.ts | 165 ++++++++++
.../__test__/server/server-components.test.ts | 161 ++++++++++
.../server-destination-boundary.test.ts | 22 ++
.../server/server-metrics-boundary.test.ts | 22 ++
.../dashboard/application/build/show.tsx | 11 +-
.../servers/server-components-update.tsx | 180 +++++++++++
.../settings/servers/show-servers.tsx | 7 +
apps/dokploy/server/api/routers/server.ts | 63 ++++
.../server/api/utils/procedure-access.ts | 2 +
docs/agenthits-install.md | 8 +-
docs/server-components-update.md | 55 ++++
docs/server-hardening.md | 123 +++++++
install-agenthits.ps1 | 10 +-
install-agenthits.sh | 302 ++++++++++++++++--
packages/server/src/index.ts | 1 +
.../server/src/setup/component-versions.ts | 19 ++
.../server/src/setup/server-components.ts | 225 +++++++++++++
packages/server/src/setup/server-setup.ts | 157 +++++++--
packages/server/src/setup/traefik-setup.ts | 18 +-
26 files changed, 1505 insertions(+), 80 deletions(-)
create mode 100644 apps/dokploy/__test__/server/component-versions-sync.test.ts
create mode 100644 apps/dokploy/__test__/server/server-components.test.ts
create mode 100644 apps/dokploy/components/dashboard/settings/servers/server-components-update.tsx
create mode 100644 docs/server-components-update.md
create mode 100644 docs/server-hardening.md
create mode 100644 packages/server/src/setup/component-versions.ts
create mode 100644 packages/server/src/setup/server-components.ts
diff --git a/.github/workflows/dokploy.yml b/.github/workflows/dokploy.yml
index 175543205d2..572adc6b14f 100644
--- a/.github/workflows/dokploy.yml
+++ b/.github/workflows/dokploy.yml
@@ -207,7 +207,7 @@ jobs:
- uses: actions/setup-node@v7
with:
- node-version: 24.4.0
+ node-version: 24.21.0
cache: pnpm
- name: Generate OpenAPI specification
diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml
index 0be852a70ba..948b3483422 100644
--- a/.github/workflows/pull-request.yml
+++ b/.github/workflows/pull-request.yml
@@ -30,7 +30,7 @@ jobs:
- uses: actions/setup-node@v7
if: ${{ !cancelled() }}
with:
- node-version: 24.4.0
+ node-version: 24.21.0
- run: bun install --frozen-lockfile
if: ${{ !cancelled() }}
- run: bun run server:build
@@ -54,7 +54,7 @@ jobs:
bun-version: 1.4.2
- uses: actions/setup-node@v7
with:
- node-version: 24.4.0
+ node-version: 24.21.0
- name: Install Nixpacks
if: matrix.job == 'test' && runner.environment == 'github-hosted'
@@ -66,7 +66,7 @@ jobs:
- name: Install Railpack
if: matrix.job == 'test' && runner.environment == 'github-hosted'
run: |
- export RAILPACK_VERSION=0.15.4
+ export RAILPACK_VERSION=0.40.1
curl -sSL https://railpack.com/install.sh | bash
echo "Railpack installed $RAILPACK_VERSION"
diff --git a/.github/workflows/sync-openapi-docs.yml b/.github/workflows/sync-openapi-docs.yml
index dd9d8d7093a..86774010a27 100644
--- a/.github/workflows/sync-openapi-docs.yml
+++ b/.github/workflows/sync-openapi-docs.yml
@@ -29,7 +29,7 @@ jobs:
bun-version: 1.4.2
- uses: actions/setup-node@v7
with:
- node-version: 24.4.0
+ node-version: 24.21.0
- name: Install dependencies
run: bun install --frozen-lockfile
diff --git a/Dockerfile b/Dockerfile
index 868b1db9c14..dde2e8819f3 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,5 +1,5 @@
# syntax=docker/dockerfile:1
-FROM node:24.4.0-slim AS base
+FROM node:24.21.0-slim AS base
ENV BUN_INSTALL="/root/.bun"
ENV PATH="$BUN_INSTALL/bin:$PATH"
RUN apt-get update \
@@ -79,7 +79,17 @@ COPY --from=build /usr/src/app/packages/server/dist ./packages/server/dist
# Install docker
-RUN curl -fsSL https://get.docker.com -o get-docker.sh && sh get-docker.sh --version 28.5.2 && rm get-docker.sh && curl https://rclone.org/install.sh | bash
+ARG DOCKER_VERSION=29.8.2
+RUN curl -fsSL https://get.docker.com -o get-docker.sh && sh get-docker.sh --version "$DOCKER_VERSION" && rm get-docker.sh
+
+# Install rclone
+ARG RCLONE_VERSION=1.75.1
+RUN apt-get update \
+ && apt-get install -y --no-install-recommends unzip \
+ && curl -fsSL "https://downloads.rclone.org/v${RCLONE_VERSION}/rclone-v${RCLONE_VERSION}-linux-amd64.zip" -o /tmp/rclone.zip \
+ && unzip -q /tmp/rclone.zip -d /tmp \
+ && install -m 0755 "/tmp/rclone-v${RCLONE_VERSION}-linux-amd64/rclone" /usr/bin/rclone \
+ && rm -rf /tmp/rclone.zip "/tmp/rclone-v${RCLONE_VERSION}-linux-amd64" /var/lib/apt/lists/*
# Install Nixpacks and tsx
# | VERBOSE=1 VERSION=1.21.0 bash
@@ -92,12 +102,12 @@ RUN curl -sSL https://nixpacks.com/install.sh -o install.sh \
&& bun install -g tsx
# Install Railpack
-ARG RAILPACK_VERSION=0.15.4
+ARG RAILPACK_VERSION=0.40.1
RUN curl -sSL https://railpack.com/install.sh \
| RAILPACK_VERSION="$RAILPACK_VERSION" bash -s -- --yes --bin-dir /usr/local/bin
# Install buildpacks
-COPY --from=buildpacksio/pack:0.39.1 /usr/local/bin/pack /usr/local/bin/pack
+COPY --from=buildpacksio/pack:0.40.9 /usr/local/bin/pack /usr/local/bin/pack
EXPOSE 3000
diff --git a/Dockerfile.cloud b/Dockerfile.cloud
index d1ae296782d..e1ba3fa0c6a 100644
--- a/Dockerfile.cloud
+++ b/Dockerfile.cloud
@@ -1,5 +1,5 @@
# syntax=docker/dockerfile:1
-FROM node:24.4.0-slim AS base
+FROM node:24.21.0-slim AS base
ENV BUN_INSTALL="/root/.bun"
ENV PATH="$BUN_INSTALL/bin:$PATH"
RUN apt-get update \
diff --git a/Dockerfile.schedule b/Dockerfile.schedule
index 3ee0246cc0e..150b962d081 100644
--- a/Dockerfile.schedule
+++ b/Dockerfile.schedule
@@ -1,5 +1,5 @@
# syntax=docker/dockerfile:1
-FROM node:24.4.0-slim AS base
+FROM node:24.21.0-slim AS base
ENV BUN_INSTALL="/root/.bun"
ENV PATH="$BUN_INSTALL/bin:$PATH"
RUN apt-get update \
diff --git a/Dockerfile.server b/Dockerfile.server
index a53befd598a..333c7e92c96 100644
--- a/Dockerfile.server
+++ b/Dockerfile.server
@@ -1,5 +1,5 @@
# syntax=docker/dockerfile:1
-FROM node:24.4.0-slim AS base
+FROM node:24.21.0-slim AS base
ENV BUN_INSTALL="/root/.bun"
ENV PATH="$BUN_INSTALL/bin:$PATH"
RUN apt-get update \
diff --git a/apps/dokploy/__test__/server/component-versions-sync.test.ts b/apps/dokploy/__test__/server/component-versions-sync.test.ts
new file mode 100644
index 00000000000..db6351a3738
--- /dev/null
+++ b/apps/dokploy/__test__/server/component-versions-sync.test.ts
@@ -0,0 +1,165 @@
+import { readFileSync } from "node:fs";
+import path from "node:path";
+import { PINNED_VERSIONS } from "@dokploy/server/setup/component-versions";
+import { TRAEFIK_VERSION } from "@dokploy/server/setup/traefik-setup";
+import { describe, expect, it } from "vitest";
+
+const repoRoot = path.resolve(process.cwd(), "../..");
+const read = (file: string) => readFileSync(path.join(repoRoot, file), "utf8");
+
+const pinIn = (text: string, pattern: RegExp) => {
+ const match = text.match(pattern);
+ if (!match?.[1]) {
+ throw new Error(`pin not found: ${pattern}`);
+ }
+ return match[1];
+};
+
+describe("install-agenthits.sh defaults", () => {
+ const installer = read("install-agenthits.sh");
+
+ it("pins Docker", () => {
+ expect(
+ pinIn(installer, /DOCKER_VERSION="\$\{DOCKER_VERSION:-([^}]+)\}"/),
+ ).toBe(PINNED_VERSIONS.docker);
+ });
+ it("pins Traefik", () => {
+ expect(
+ pinIn(installer, /TRAEFIK_IMAGE="\$\{TRAEFIK_IMAGE:-traefik:v([^}]+)\}"/),
+ ).toBe(PINNED_VERSIONS.traefik);
+ });
+ it("pins Postgres", () => {
+ expect(
+ pinIn(
+ installer,
+ /POSTGRES_IMAGE="\$\{POSTGRES_IMAGE:-postgres:([^}]+)\}"/,
+ ),
+ ).toBe(PINNED_VERSIONS.postgres);
+ });
+ it("pins Redis", () => {
+ expect(
+ pinIn(installer, /REDIS_IMAGE="\$\{REDIS_IMAGE:-redis:([^}]+)\}"/),
+ ).toBe(PINNED_VERSIONS.redis);
+ });
+ it("pins Nixpacks", () => {
+ expect(
+ pinIn(installer, /NIXPACKS_VERSION="\$\{NIXPACKS_VERSION:-([^}]+)\}"/),
+ ).toBe(PINNED_VERSIONS.nixpacks);
+ });
+ it("pins Railpack", () => {
+ expect(
+ pinIn(installer, /RAILPACK_VERSION="\$\{RAILPACK_VERSION:-([^}]+)\}"/),
+ ).toBe(PINNED_VERSIONS.railpack);
+ });
+ it("pins Buildpacks", () => {
+ expect(
+ pinIn(
+ installer,
+ /BUILDPACKS_VERSION="\$\{BUILDPACKS_VERSION:-([^}]+)\}"/,
+ ),
+ ).toBe(PINNED_VERSIONS.buildpacks);
+ });
+ it("pins RClone", () => {
+ expect(
+ pinIn(installer, /RCLONE_VERSION="\$\{RCLONE_VERSION:-([^}]+)\}"/),
+ ).toBe(PINNED_VERSIONS.rclone);
+ });
+});
+
+describe("Dockerfile", () => {
+ const dockerfile = read("Dockerfile");
+
+ it("pins Docker", () => {
+ expect(pinIn(dockerfile, /ARG DOCKER_VERSION=(\S+)/)).toBe(
+ PINNED_VERSIONS.docker,
+ );
+ });
+ it("pins RClone", () => {
+ expect(pinIn(dockerfile, /ARG RCLONE_VERSION=(\S+)/)).toBe(
+ PINNED_VERSIONS.rclone,
+ );
+ });
+ it("pins Nixpacks", () => {
+ expect(pinIn(dockerfile, /ARG NIXPACKS_VERSION=(\S+)/)).toBe(
+ PINNED_VERSIONS.nixpacks,
+ );
+ });
+ it("pins Railpack", () => {
+ expect(pinIn(dockerfile, /ARG RAILPACK_VERSION=(\S+)/)).toBe(
+ PINNED_VERSIONS.railpack,
+ );
+ });
+ it("pins pack", () => {
+ expect(pinIn(dockerfile, /buildpacksio\/pack:(\S+) /)).toBe(
+ PINNED_VERSIONS.buildpacks,
+ );
+ });
+});
+
+describe("CI workflow", () => {
+ const ci = read(".github/workflows/pull-request.yml");
+
+ it("pins Nixpacks", () => {
+ expect(pinIn(ci, /export NIXPACKS_VERSION=(\S+)/)).toBe(
+ PINNED_VERSIONS.nixpacks,
+ );
+ });
+ it("pins Railpack", () => {
+ expect(pinIn(ci, /export RAILPACK_VERSION=(\S+)/)).toBe(
+ PINNED_VERSIONS.railpack,
+ );
+ });
+});
+
+describe("application Railpack picker", () => {
+ const picker = read(
+ "apps/dokploy/components/dashboard/application/build/show.tsx",
+ );
+
+ it("defaults new applications to the pinned Railpack", () => {
+ expect(
+ pinIn(
+ picker,
+ /railpackVersion: z\.string\(\)\.nullable\(\)\.default\("([^"]+)"\)/,
+ ),
+ ).toBe(PINNED_VERSIONS.railpack);
+ });
+ it("offers the pinned Railpack in the version list", () => {
+ expect(picker).toContain(`\t"${PINNED_VERSIONS.railpack}",`);
+ });
+});
+
+describe("panel Traefik default", () => {
+ it("matches the pin", () => {
+ expect(TRAEFIK_VERSION).toBe(PINNED_VERSIONS.traefik);
+ });
+});
+
+describe("Node.js image and CI version", () => {
+ const dockerfiles = [
+ "Dockerfile",
+ "Dockerfile.cloud",
+ "Dockerfile.schedule",
+ "Dockerfile.server",
+ ];
+
+ it.each(dockerfiles)("%s builds on the pinned Node image", (file) => {
+ expect(pinIn(read(file), /FROM node:(\S+)-slim AS base/)).toBe(
+ PINNED_VERSIONS.node,
+ );
+ });
+
+ it.each([
+ ".github/workflows/dokploy.yml",
+ ".github/workflows/pull-request.yml",
+ ".github/workflows/sync-openapi-docs.yml",
+ ])("%s runs on the pinned Node version", (file) => {
+ const versions = [...read(file).matchAll(/node-version: (\S+)/g)].map(
+ (match) => match[1],
+ );
+ expect(versions.length).toBeGreaterThan(0);
+ expect(versions.every((version) => version === PINNED_VERSIONS.node)).toBe(
+ true,
+ );
+ });
+});
diff --git a/apps/dokploy/__test__/server/server-components.test.ts b/apps/dokploy/__test__/server/server-components.test.ts
new file mode 100644
index 00000000000..b2d50e9e9ec
--- /dev/null
+++ b/apps/dokploy/__test__/server/server-components.test.ts
@@ -0,0 +1,161 @@
+import { spawnSync } from "node:child_process";
+import {
+ BUILDPACKS_VERSION,
+ NIXPACKS_VERSION,
+ PINNED_VERSIONS,
+ RAILPACK_VERSION,
+} from "@dokploy/server/setup/component-versions";
+import {
+ buildComponentStatuses,
+ buildComponentUpdateScript,
+ COMPONENTS_UPDATE_DONE,
+ parseComponentVersions,
+} from "@dokploy/server/setup/server-components";
+import {
+ buildTraefikRunCommand,
+ TRAEFIK_VERSION,
+} from "@dokploy/server/setup/traefik-setup";
+import { describe, expect, it } from "vitest";
+
+describe("parseComponentVersions", () => {
+ it("extracts the semantic version from each tool's output", () => {
+ const output = [
+ "docker=29.4.2",
+ "traefik=traefik:v3.6.25",
+ "nixpacks=nixpacks 1.41.0",
+ "railpack=railpack version 0.15.4",
+ "buildpacks=0.39.1+git-dc9220d.build-6702",
+ "rclone=rclone v1.75.1",
+ ].join("\n");
+
+ expect(parseComponentVersions(output)).toEqual({
+ docker: "29.4.2",
+ traefik: "3.6.25",
+ nixpacks: "1.41.0",
+ railpack: "0.15.4",
+ buildpacks: "0.39.1",
+ rclone: "1.75.1",
+ });
+ });
+
+ it("reports a missing tool as null", () => {
+ expect(parseComponentVersions("traefik=\nnixpacks=")).toEqual({
+ traefik: null,
+ nixpacks: null,
+ });
+ });
+
+ it("ignores lines that are not component values", () => {
+ expect(parseComponentVersions("something=1.2.3\nnoequals")).toEqual({});
+ });
+});
+
+describe("buildComponentStatuses", () => {
+ it("marks a pinned component outdated when the installed version differs", () => {
+ const statuses = buildComponentStatuses({ traefik: "3.6.25" });
+ const traefik = statuses.find((status) => status.name === "traefik");
+
+ expect(traefik).toEqual({
+ name: "traefik",
+ installed: "3.6.25",
+ target: TRAEFIK_VERSION,
+ outdated: TRAEFIK_VERSION !== "3.6.25",
+ });
+ });
+
+ it("is up to date when the installed version matches the pin", () => {
+ const statuses = buildComponentStatuses({
+ docker: PINNED_VERSIONS.docker,
+ rclone: PINNED_VERSIONS.rclone,
+ traefik: TRAEFIK_VERSION,
+ nixpacks: NIXPACKS_VERSION,
+ railpack: RAILPACK_VERSION,
+ buildpacks: BUILDPACKS_VERSION,
+ });
+
+ expect(statuses.filter((status) => status.outdated)).toEqual([]);
+ });
+
+ it("treats a missing pinned component as outdated", () => {
+ const statuses = buildComponentStatuses({ nixpacks: null });
+ const nixpacks = statuses.find((status) => status.name === "nixpacks");
+
+ expect(nixpacks?.installed).toBeNull();
+ expect(nixpacks?.outdated).toBe(true);
+ });
+
+ it("marks Docker and RClone outdated when they differ from the pin", () => {
+ const statuses = buildComponentStatuses({ docker: "1.0.0", rclone: null });
+ const docker = statuses.find((status) => status.name === "docker");
+ const rclone = statuses.find((status) => status.name === "rclone");
+
+ expect(docker).toMatchObject({
+ target: PINNED_VERSIONS.docker,
+ outdated: true,
+ });
+ expect(rclone).toMatchObject({
+ target: PINNED_VERSIONS.rclone,
+ outdated: true,
+ });
+ });
+});
+
+describe("buildComponentUpdateScript", () => {
+ it("recreates the Traefik container from the pinned image", () => {
+ const script = buildComponentUpdateScript(["traefik"]);
+
+ expect(script).toContain(`docker pull traefik:v${TRAEFIK_VERSION}`);
+ expect(script).toContain("docker rm -f dokploy-traefik");
+ expect(script).toContain(buildTraefikRunCommand(TRAEFIK_VERSION).trim());
+ });
+
+ it("passes the pinned versions into the installers through env", () => {
+ const script = buildComponentUpdateScript(["nixpacks", "railpack"]);
+
+ expect(script).toContain(
+ `env NIXPACKS_VERSION=${NIXPACKS_VERSION} bash -c`,
+ );
+ expect(script).toContain(
+ `env RAILPACK_VERSION=${RAILPACK_VERSION} bash -c`,
+ );
+ });
+
+ it("installs the pinned Docker and RClone", () => {
+ const script = buildComponentUpdateScript(["docker", "rclone"]);
+
+ expect(script).toContain(`--version ${PINNED_VERSIONS.docker}`);
+ expect(script).toContain(
+ `rclone-v${PINNED_VERSIONS.rclone}-linux-$RCLONE_RELEASE_ARCH.zip`,
+ );
+ });
+
+ it("ends with the marker the UI waits for", () => {
+ expect(buildComponentUpdateScript(["buildpacks"])).toContain(
+ COMPONENTS_UPDATE_DONE,
+ );
+ });
+
+ it("generates valid bash for every updatable component", () => {
+ const script = buildComponentUpdateScript([
+ "traefik",
+ "nixpacks",
+ "railpack",
+ "buildpacks",
+ ]);
+ const result = spawnSync("bash", ["-n"], { input: script });
+
+ expect(result.stderr.toString()).toBe("");
+ expect(result.status).toBe(0);
+ });
+});
+
+describe("buildTraefikRunCommand", () => {
+ it("publishes the HTTP, HTTPS and HTTP/3 ports with the requested image", () => {
+ const command = buildTraefikRunCommand("3.7.5");
+
+ expect(command).toMatch(/-p 443:443\s/);
+ expect(command).toMatch(/-p 80:80\s/);
+ expect(command).toContain("-p 443:443/udp");
+ expect(command).toContain("traefik:v3.7.5");
+ });
+});
diff --git a/apps/dokploy/__test__/server/server-destination-boundary.test.ts b/apps/dokploy/__test__/server/server-destination-boundary.test.ts
index d45dc232b47..cefe6c40de6 100644
--- a/apps/dokploy/__test__/server/server-destination-boundary.test.ts
+++ b/apps/dokploy/__test__/server/server-destination-boundary.test.ts
@@ -29,6 +29,17 @@ const mocks = vi.hoisted(() => ({
}));
vi.mock("@dokploy/server", () => ({
+ COMPONENTS_UPDATE_FAILED: "Components update failed ❌",
+ UPDATABLE_COMPONENTS: [
+ "docker",
+ "traefik",
+ "rclone",
+ "nixpacks",
+ "railpack",
+ "buildpacks",
+ ],
+ serverComponentsStatus: async () => [],
+ updateServerComponents: async () => undefined,
IS_CLOUD: true,
createServer: mocks.createServer,
defaultCommand: mocks.defaultCommand,
@@ -54,6 +65,17 @@ vi.mock("@dokploy/server", () => ({
}));
vi.mock("@dokploy/server/index", () => ({
+ COMPONENTS_UPDATE_FAILED: "Components update failed ❌",
+ UPDATABLE_COMPONENTS: [
+ "docker",
+ "traefik",
+ "rclone",
+ "nixpacks",
+ "railpack",
+ "buildpacks",
+ ],
+ serverComponentsStatus: async () => [],
+ updateServerComponents: async () => undefined,
IS_CLOUD: true,
createServer: mocks.createServer,
defaultCommand: mocks.defaultCommand,
diff --git a/apps/dokploy/__test__/server/server-metrics-boundary.test.ts b/apps/dokploy/__test__/server/server-metrics-boundary.test.ts
index d658e7f138a..cdd63c9ff7b 100644
--- a/apps/dokploy/__test__/server/server-metrics-boundary.test.ts
+++ b/apps/dokploy/__test__/server/server-metrics-boundary.test.ts
@@ -74,6 +74,17 @@ const mocks = vi.hoisted(() => ({
}));
vi.mock("@dokploy/server", () => ({
+ COMPONENTS_UPDATE_FAILED: "Components update failed ❌",
+ UPDATABLE_COMPONENTS: [
+ "docker",
+ "traefik",
+ "rclone",
+ "nixpacks",
+ "railpack",
+ "buildpacks",
+ ],
+ serverComponentsStatus: async () => [],
+ updateServerComponents: async () => undefined,
IS_CLOUD: false,
createServer: mocks.createServer,
defaultCommand: mocks.defaultCommand,
@@ -110,6 +121,17 @@ vi.mock("@dokploy/server", () => ({
}));
vi.mock("@dokploy/server/index", () => ({
+ COMPONENTS_UPDATE_FAILED: "Components update failed ❌",
+ UPDATABLE_COMPONENTS: [
+ "docker",
+ "traefik",
+ "rclone",
+ "nixpacks",
+ "railpack",
+ "buildpacks",
+ ],
+ serverComponentsStatus: async () => [],
+ updateServerComponents: async () => undefined,
IS_CLOUD: false,
createServer: mocks.createServer,
defaultCommand: mocks.defaultCommand,
diff --git a/apps/dokploy/components/dashboard/application/build/show.tsx b/apps/dokploy/components/dashboard/application/build/show.tsx
index e55db55249e..6fb90312436 100644
--- a/apps/dokploy/components/dashboard/application/build/show.tsx
+++ b/apps/dokploy/components/dashboard/application/build/show.tsx
@@ -31,6 +31,7 @@ import { api } from "@/utils/api";
// Railpack versions from https://github.com/railwayapp/railpack/releases
export const RAILPACK_VERSIONS = [
+ "0.40.1",
"0.39.0",
"0.38.0",
"0.37.1",
@@ -141,7 +142,7 @@ const mySchema = z.discriminatedUnion("buildType", [
}),
z.object({
buildType: z.literal(BuildType.railpack),
- railpackVersion: z.string().nullable().default("0.15.4"),
+ railpackVersion: z.string().nullable().default("0.40.1"),
}),
z.object({
buildType: z.literal(BuildType.static),
@@ -282,7 +283,7 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => {
: null,
railpackVersion:
data.buildType === BuildType.railpack
- ? data.railpackVersion || "0.15.4"
+ ? data.railpackVersion || "0.40.1"
: null,
})
.then(async () => {
@@ -544,7 +545,7 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => {
size="sm"
onClick={() => {
setIsManualRailpackVersion(false);
- field.onChange("0.15.4");
+ field.onChange("0.40.1");
}}
>
Use predefined versions
@@ -560,7 +561,7 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => {
field.onChange(value);
}
}}
- value={field.value ?? "0.15.4"}
+ value={field.value ?? "0.40.1"}
>
@@ -574,7 +575,7 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => {
{RAILPACK_VERSIONS.map((version) => (
v{version}
- {version === "0.15.4" && (
+ {version === "0.40.1" && (
+ (UPDATABLE_COMPONENTS as readonly string[]).includes(name);
+
+interface Props {
+ serverId: string;
+}
+
+export const ServerComponentsUpdate = ({ serverId }: Props) => {
+ const [isOpen, setIsOpen] = useState(false);
+ const [isUpdating, setIsUpdating] = useState(false);
+ const [selected, setSelected] = useState([]);
+ const [logs, setLogs] = useState("");
+
+ const { data, isPending, isError, error, refetch } =
+ api.server.components.useQuery(
+ { serverId },
+ {
+ enabled: !!serverId,
+ refetchInterval: 10 * 60 * 1000,
+ refetchOnWindowFocus: false,
+ },
+ );
+
+ const outdated = data?.filter((component) => component.outdated) ?? [];
+ const updatable = outdated
+ .map((component) => component.name)
+ .filter(isUpdatable);
+ const hasUpdates = outdated.length > 0;
+
+ api.server.updateComponentsWithLogs.useSubscription(
+ { serverId, components: selected },
+ {
+ enabled: isUpdating,
+ onData(log) {
+ setLogs((prev) => `${prev}${log}`);
+ if (log.includes(UPDATE_DONE) || log.includes(UPDATE_FAILED)) {
+ setIsUpdating(false);
+ if (log.includes(UPDATE_DONE)) {
+ toast.success("Server components updated");
+ } else {
+ toast.error("Component update failed, see the log");
+ }
+ void refetch();
+ }
+ },
+ onError(err) {
+ setLogs((prev) => `${prev}${err.message}\n`);
+ setIsUpdating(false);
+ toast.error("Component update failed");
+ },
+ },
+ );
+
+ const startUpdate = () => {
+ setLogs("");
+ setSelected(updatable);
+ setIsUpdating(true);
+ };
+
+ return (
+
+ );
+};
diff --git a/apps/dokploy/components/dashboard/settings/servers/show-servers.tsx b/apps/dokploy/components/dashboard/settings/servers/show-servers.tsx
index 5d356acef44..8f43b24624c 100644
--- a/apps/dokploy/components/dashboard/settings/servers/show-servers.tsx
+++ b/apps/dokploy/components/dashboard/settings/servers/show-servers.tsx
@@ -32,6 +32,7 @@ import { TerminalModal } from "../web-server/terminal-modal";
import { ShowServerActions } from "./actions/show-server-actions";
import { DeleteServerModal } from "./delete-server-modal";
import { HandleServers } from "./handle-servers";
+import { ServerComponentsUpdate } from "./server-components-update";
import { SetupServer } from "./setup-server";
import { ShowMonitoringModal } from "./show-monitoring-modal";
import { WelcomeSubscription } from "./welcome-stripe/welcome-subscription";
@@ -247,6 +248,12 @@ export const ShowServers = () => {
+
+
+
+
{server.sshKeyId &&
permissions?.server.terminal && (
diff --git a/apps/dokploy/server/api/routers/server.ts b/apps/dokploy/server/api/routers/server.ts
index 4b5e8a95e02..07c420aee7c 100644
--- a/apps/dokploy/server/api/routers/server.ts
+++ b/apps/dokploy/server/api/routers/server.ts
@@ -1,4 +1,5 @@
import {
+ COMPONENTS_UPDATE_FAILED,
createServer,
defaultCommand,
deleteServer,
@@ -19,10 +20,13 @@ import {
removeDeploymentsByServerId,
resolveServerMetricsConfigUpdate,
serverAudit,
+ serverComponentsStatus,
serverSetup,
serverValidate,
setupMonitoring,
+ UPDATABLE_COMPONENTS,
updateServerById,
+ updateServerComponents,
} from "@dokploy/server";
import { db } from "@dokploy/server/db";
import {
@@ -552,6 +556,65 @@ export const serverRouter = createTRPCRouter({
});
}
}),
+ components: withPermission("server", "read")
+ .input(apiFindOneServer)
+ .query(async ({ input, ctx }) => {
+ await assertServerAccess(ctx, input.serverId);
+ const server = await findServerById(input.serverId);
+ if (server.organizationId !== ctx.session.activeOrganizationId) {
+ throw new TRPCError({
+ code: "UNAUTHORIZED",
+ message: "You are not authorized to validate this server",
+ });
+ }
+ try {
+ return await serverComponentsStatus(input.serverId);
+ } catch (error) {
+ throw new TRPCError({
+ code: "BAD_REQUEST",
+ message: error instanceof Error ? error.message : `Error: ${error}`,
+ cause: error as Error,
+ });
+ }
+ }),
+ updateComponentsWithLogs: withPermission("server", "execute")
+ .meta({
+ openapi: {
+ path: "/server/update-components-with-logs",
+ method: "POST",
+ override: true,
+ enabled: false,
+ },
+ })
+ .input(
+ z.object({
+ serverId: z.string(),
+ components: z.array(z.enum(UPDATABLE_COMPONENTS)).min(1),
+ }),
+ )
+ .subscription(async ({ input, ctx }) => {
+ await assertServerAccess(ctx, input.serverId);
+ const server = await findServerById(input.serverId);
+ if (server.organizationId !== ctx.session.activeOrganizationId) {
+ throw new TRPCError({
+ code: "UNAUTHORIZED",
+ message: "You are not authorized to update this server",
+ });
+ }
+ return observable((emit) => {
+ updateServerComponents(input.serverId, input.components, (log) => {
+ emit.next(log);
+ })
+ .catch((error) => {
+ emit.next(
+ `${error instanceof Error ? error.message : error}\n${COMPONENTS_UPDATE_FAILED}\n`,
+ );
+ })
+ .finally(() => {
+ emit.complete();
+ });
+ });
+ }),
setupMonitoring: withPermission("server", "execute")
.input(apiUpdateServerMonitoring)
.mutation(async ({ input, ctx }) => {
diff --git a/apps/dokploy/server/api/utils/procedure-access.ts b/apps/dokploy/server/api/utils/procedure-access.ts
index adff9d86b6a..7b755f4e555 100644
--- a/apps/dokploy/server/api/utils/procedure-access.ts
+++ b/apps/dokploy/server/api/utils/procedure-access.ts
@@ -558,6 +558,7 @@ export const PROCEDURE_ACCESS: Record = {
"server.all": "read",
"server.allForPermissions": "read",
"server.buildServers": "read",
+ "server.components": "read",
"server.count": "read",
"server.create": "dangerous",
"server.getContainerProcesses": "read",
@@ -575,6 +576,7 @@ export const PROCEDURE_ACCESS: Record = {
"server.setupWithLogs": "write",
"server.update": "dangerous",
"server.updateBuildsConcurrency": "write",
+ "server.updateComponentsWithLogs": "write",
"server.validate": "read",
"server.withSSHKey": "read",
"settings.assignDomainServer": "dangerous",
diff --git a/docs/agenthits-install.md b/docs/agenthits-install.md
index 7cd7077eec4..d1765733d0a 100644
--- a/docs/agenthits-install.md
+++ b/docs/agenthits-install.md
@@ -29,7 +29,7 @@ Installer сам определяет систему:
| Система | Что делает installer |
| --- | --- |
-| Linux: Ubuntu, Debian, Raspbian, Fedora, CentOS, RHEL | Ставит Docker через get.docker.com (версия `DOCKER_VERSION`, по умолчанию 28.5.0; если для релиза ее нет — последнюю) |
+| Linux: Ubuntu, Debian, Raspbian, Fedora, CentOS, RHEL | Ставит Docker через get.docker.com (версия `DOCKER_VERSION`, по умолчанию 29.8.2; если для релиза ее нет — последнюю) |
| Linux: Rocky, Alma, Oracle, Arch, Manjaro, openSUSE, Alpine, производные Debian/RHEL | Ставит Docker пакетным менеджером дистрибутива. На Alpine сначала `apk add bash curl` |
| Linux на ARM (`aarch64`) | Дополнительно включает эмуляцию `amd64` (qemu): образ собирается только под `amd64` |
| macOS (Apple Silicon и Intel) | Ставит OrbStack (через Homebrew, если его нет) и панель в отдельную Linux-машину OrbStack |
@@ -48,9 +48,9 @@ Installer сам определяет систему:
```text
Dokploy: ghcr.io/agenthits/dokploy:agenthits-dev
-Traefik: traefik:v3.7.5
-Postgres: postgres:18.4
-Redis: redis:8.8.0
+Traefik: traefik:v3.7.14
+Postgres: postgres:18.6
+Redis: redis:8.10.2
```
Для `postgres:18+` installer автоматически монтирует volume в
diff --git a/docs/server-components-update.md b/docs/server-components-update.md
new file mode 100644
index 00000000000..9d78ef54c7c
--- /dev/null
+++ b/docs/server-components-update.md
@@ -0,0 +1,55 @@
+# Версии компонентов: один источник, установка и обновление
+
+Все версии, которые ставит форк, лежат в одном месте: `packages/server/src/setup/component-versions.ts`, объект `PINNED_VERSIONS`. Оттуда их берут:
+
+- удалённая настройка сервера из панели (Setup Server) и кнопка обновления компонентов;
+- установщик панели `install-agenthits.sh` (значения по умолчанию, плюс Nixpacks, Railpack, pack и RClone на хосте панели);
+- образ панели `Dockerfile` (Docker, RClone, Railpack, pack);
+- CI `.github/workflows/pull-request.yml` (Nixpacks, Railpack);
+- список версий Railpack в форме приложения (значение по умолчанию и список).
+
+Тест `apps/dokploy/__test__/server/component-versions-sync.test.ts` сверяет каждый из этих файлов с `PINNED_VERSIONS` и падает при расхождении.
+
+## Закреплённые версии
+
+| Компонент | Версия | Где проверено наличие |
+|---|---|---|
+| Node.js (базовый образ панели и CI) | 24.21.0 | `node:24.21.0-slim`, версия 24.x актуальна |
+| Docker Engine | 29.8.2 | docker-ce для noble и resolute, amd64 и arm64 |
+| Traefik | 3.7.14 | последний стабильный релиз (GitHub), образ `traefik:v3.7.14` |
+| Postgres (база панели) | 18.6 | `postgres:18.6`, более новых тегов 18.x нет |
+| Redis (база панели) | 8.10.2 | `redis:8.10.2`, более новых тегов 8.10.x нет |
+| Nixpacks | 1.41.0 | последний релиз |
+| Railpack | 0.40.1 | последний релиз, образ фронтенда `railpack-frontend:v0.40.1` |
+| Buildpacks (pack) | 0.40.9 | последний релиз |
+| RClone | 1.75.1 | последний релиз, архив `downloads.rclone.org` |
+
+Postgres и Redis относятся к базе панели. На серверах VPS их нет.
+
+## Что делает кнопка обновления
+
+Кнопка **Components** на карточке сервера сравнивает установленные версии с закреплёнными и обновляет отличающиеся:
+
+- Docker: переустанавливается закреплённая версия через get.docker.com. Демон Docker перезапускается, контейнеры с политикой перезапуска перезапускаются, сервисы swarm недоступны несколько секунд.
+- Traefik: пересоздаётся контейнер `dokploy-traefik`. Сервисы на сервере недоступны несколько секунд.
+- RClone, Nixpacks, Railpack, Buildpacks: переустанавливаются закреплённые версии, работающие сервисы не трогаются.
+
+Установка сервера (Setup Server) уже установленное не обновляет. Поэтому после смены версии нужна именно эта кнопка.
+
+## Как поменять версию
+
+1. Поменять значение в `PINNED_VERSIONS` (`component-versions.ts`). Для Traefik также можно переопределить переменной окружения `TRAEFIK_VERSION`, но для одинаковой установки этого делать не стоит.
+2. Поменять то же значение в `install-agenthits.sh` (значения по умолчанию), `Dockerfile` (ARG) и `.github/workflows/pull-request.yml`. Если тест не проходит, какой-то файл остался со старым значением.
+3. Выпустить панель обычным путём: PR, merge, образ.
+4. На каждом сервере нажать Components → Update.
+
+## Откат
+
+Вернуть значение в `PINNED_VERSIONS` и остальных файлах, выпустить панель и нажать Update на сервере. Кнопка установит прежнюю версию.
+
+## Ограничения
+
+- Railpack 0.15.4 → 0.40.1 и pack 0.39.1 → 0.40.9 — большие скачки. Перед широким выкатом проверьте сборку реального приложения.
+- Версия Railpack у каждого приложения хранится в базе (колонка `railpackVersion`, по умолчанию `0.15.4`). Новое значение по умолчанию в форме (0.40.1) применяется к приложениям, которые сохраняют настройки сборки. Изменить значение по умолчанию в базе можно только миграцией, её пока нет.
+- Версии Dokploy (`DOKPLOY_OFFICIAL_VERSION`, сейчас v0.29.8) и npm-зависимости в этот список не входят.
+- Обновление Traefik, Docker и установка в контейнере проверены только тестами и в контейнере Ubuntu. Полный прогон с приложениями на живом сервере не выполнялся.
diff --git a/docs/server-hardening.md b/docs/server-hardening.md
new file mode 100644
index 00000000000..fbcaad17f2a
--- /dev/null
+++ b/docs/server-hardening.md
@@ -0,0 +1,123 @@
+# Усиление защиты серверов: SSH, UFW, Fail2Ban
+
+Что включается, как запустить, как проверить, как откатить и как удалить сервер.
+
+## Что делает каждый флаг
+
+| Флаг установщика | Переменная окружения | Что делает |
+|---|---|---|
+| `--ssh-keys-only` | `HARDEN_SSH=1` | Вход только по ключу: `PasswordAuthentication no`, `UsePAM no`, `PermitRootLogin prohibit-password`. Требует валидный ключ в `/root/.ssh/authorized_keys`. Пропускается, если на хосте нет sshd. |
+| `--ufw` | `HARDEN_UFW=1` | UFW: входящие запрещены, разрешены SSH, 80 и 443 (tcp, и 443 udp для HTTP/3). Пересылка для Docker разрешена. Только Debian и Ubuntu. |
+| `--fail2ban` | `HARDEN_FAIL2BAN=1` | Fail2Ban: защита sshd в режиме `aggressive`, настройка в `/etc/fail2ban/jail.local`. Только Debian и Ubuntu. |
+| `--harden` | все три | Все три флага сразу. |
+| — | `DOKPLOY_SERVER_HARDENING=ssh,ufw,fail2ban` | То же самое панель применяет к новым серверам в «Setup Server». Если задан флаг установки, переменная выставляется автоматически. |
+
+Версия Traefik для серверов берётся из установщика: `TRAEFIK_IMAGE`, сейчас `traefik:v3.7.5`. Тест `apps/dokploy/__test__/server/traefik-version.test.ts` падает, если версия по умолчанию в панели расходится с установщиком.
+
+## Как запускать
+
+### macOS (панель установлена в OrbStack)
+
+```bash
+bash install-agenthits.sh install --harden
+```
+
+Флаги передаются в машину `dokploy`, там же и выполняется усиление. Вход по SSH в OrbStack-машине по умолчанию не настроен, поэтому блок SSH будет пропущен с сообщением.
+
+### Linux
+
+```bash
+sudo bash install-agenthits.sh install --harden
+```
+
+### Windows (WSL)
+
+```powershell
+$env:HARDEN_SSH = '1'; $env:HARDEN_UFW = '1'; $env:HARDEN_FAIL2BAN = '1'
+irm https://raw.githubusercontent.com/agentHits/dokploy/AgentHits-Dev/install-agenthits.ps1 | iex
+```
+
+Windows-установщик скачивает `install-agenthits.sh` с GitHub. Изменения появятся на Windows только после пуша в ветку `AgentHits-Dev`.
+
+### Уже установленная панель (macOS)
+
+Усилить хост панели:
+
+```bash
+bash install-agenthits.sh harden --harden
+```
+
+Включить усиление для новых серверов, которые настраиваются через панель:
+
+```bash
+orb -m dokploy -u root docker service update --env-add DOKPLOY_SERVER_HARDENING=ssh,ufw,fail2ban --env-add TRAEFIK_VERSION=3.7.5 dokploy
+```
+
+Чтобы панель использовала новый код, в образ сервиса должна попасть сборка с этими изменениями (после пуша и сборки образа).
+
+## Как проверить
+
+Во вкладке Security панели: Settings → Servers → сервер → Setup Server → Security → Refresh. Все пункты должны быть зелёными.
+
+Вручную на сервере (только чтение):
+
+```bash
+sshd -T | grep -E '^(passwordauthentication|usepam|permitrootlogin) '
+ufw status verbose
+fail2ban-client status sshd
+```
+
+Ожидаемо: `passwordauthentication no`, `usepam no`, `permitrootlogin prohibit-password`, `Status: active` с `deny (incoming)`, и jail `sshd` активен.
+
+## Откат
+
+### VPS, настроенный вручную и через панель
+
+Выполнять от root на сервере. Открой отдельное окно по ключу и не закрывай текущую сессию, пока не проверишь вход.
+
+```bash
+ufw --force disable
+systemctl disable --now fail2ban
+rm -f /etc/fail2ban/jail.local /etc/ssh/sshd_config.d/00-hardening.conf
+cp -p /etc/ssh/sshd_config.dokploy-backup /etc/ssh/sshd_config
+sshd -t && systemctl reload ssh
+```
+
+Удалить пакеты (по желанию):
+
+```bash
+apt-get purge -y ufw fail2ban
+```
+
+Если сервер настраивал установщик через `--ssh-keys-only`, бэкап называется `/etc/ssh/sshd_config.agenthits-backup`. Используй его вместо `dokploy-backup`.
+
+После отката вход по паролю снова включён, если на сервере стоит оригинальный конфиг. Убедись, что пароль root известен, прежде чем откатывать.
+
+### Панель на macOS
+
+Убрать настройку усиления для новых серверов:
+
+```bash
+orb -m dokploy -u root docker service update --env-rm DOKPLOY_SERVER_HARDENING dokploy
+```
+
+## Как удалить сервер
+
+1. Из панели: Settings → Servers (`/dashboard/settings/servers`), выбрать сервер и удалить его там. Это не трогает сам VPS.
+2. С VPS:
+ - откат, описанный выше, если сервер нужен дальше;
+ - или переустановка ОС в панели хостинга. **Необратимо**: стирает весь диск, включая Docker, ключи и данные. Сначала сохрани бэкап.
+3. Панель на Mac mini целиком:
+
+ ```bash
+ orb delete dokploy
+ ```
+
+ **Необратимо**: удаляет машину OrbStack вместе с панелью, её базой и секретами. Сначала сохрани бэкап.
+
+## Ограничения
+
+- UFW не закрывает порты, которые публикует Docker. Проверено на тестовом VPS: тестовый контейнер на порту 8088 открывался из интернета при `deny`. Закрыть такие порты можно правилами в `DOCKER-USER` или публикацией на `127.0.0.1`. По умолчанию это не включено.
+- `UsePAM no` означает, что для SSH-сессий не применяются `pam_limits` и `pam_systemd`. Вход по ключу проверен и работает.
+- UFW и Fail2Ban настраиваются только на Debian и Ubuntu. На других системах блок пропускается, и пункты Security остаются красными.
+- Блок SSH пропускается, если на хосте нет sshd (OrbStack-машина и WSL по умолчанию).
diff --git a/install-agenthits.ps1 b/install-agenthits.ps1
index ba03bcaf802..9e949985f44 100644
--- a/install-agenthits.ps1
+++ b/install-agenthits.ps1
@@ -7,9 +7,14 @@ runs install-agenthits.sh inside it. Run in PowerShell as administrator:
Update:
& ([scriptblock]::Create((irm https://raw.githubusercontent.com/agentHits/dokploy/AgentHits-Dev/install-agenthits.ps1))) update
+
+Hardening (optional) is set with environment variables before the install:
+
+ $env:HARDEN_SSH = '1'; $env:HARDEN_UFW = '1'; $env:HARDEN_FAIL2BAN = '1'
+ irm https://raw.githubusercontent.com/agentHits/dokploy/AgentHits-Dev/install-agenthits.ps1 | iex
#>
param(
- [ValidateSet('install', 'update')]
+ [ValidateSet('install', 'update', 'harden')]
[string]$Mode = 'install'
)
@@ -22,7 +27,8 @@ $BaseUrl = if ($env:AGENTHITS_SCRIPT_BASE_URL) { $env:AGENTHITS_SCRIPT_BASE_URL
$PassthroughVars = @(
'DOKPLOY_IMAGE', 'DOKPLOY_RELEASE_TAG', 'DOKPLOY_OFFICIAL_VERSION', 'DOKPLOY_FORK_VERSION',
'TRAEFIK_IMAGE', 'POSTGRES_IMAGE', 'REDIS_IMAGE', 'POSTGRES_DATA_TARGET',
- 'AGENTHITS_SCRIPT_BASE_URL', 'DOCKER_VERSION', 'DOCKER_SWARM_INIT_ARGS', 'ADVERTISE_ADDR', 'PUBLIC_IP'
+ 'AGENTHITS_SCRIPT_BASE_URL', 'DOCKER_VERSION', 'DOCKER_SWARM_INIT_ARGS', 'ADVERTISE_ADDR', 'PUBLIC_IP',
+ 'DOKPLOY_SERVER_HARDENING', 'HARDEN_SSH', 'HARDEN_UFW', 'HARDEN_FAIL2BAN'
)
# VM creator id of WSL in the Hyper-V firewall (documented by Microsoft).
$WslVmCreatorId = '{40E0AC32-46A5-438A-A0B2-2B479E8F2E90}'
diff --git a/install-agenthits.sh b/install-agenthits.sh
index ca3b5d578b7..6a88ecc8567 100755
--- a/install-agenthits.sh
+++ b/install-agenthits.sh
@@ -4,12 +4,16 @@ set -euo pipefail
DOKPLOY_IMAGE="${DOKPLOY_IMAGE:-ghcr.io/agenthits/dokploy:agenthits-dev}"
DOKPLOY_RELEASE_TAG="${DOKPLOY_RELEASE_TAG:-agenthits-dev}"
DOKPLOY_OFFICIAL_VERSION="${DOKPLOY_OFFICIAL_VERSION:-v0.29.8}"
-TRAEFIK_IMAGE="${TRAEFIK_IMAGE:-traefik:v3.7.5}"
-POSTGRES_IMAGE="${POSTGRES_IMAGE:-postgres:18.4}"
-REDIS_IMAGE="${REDIS_IMAGE:-redis:8.8.0}"
+TRAEFIK_IMAGE="${TRAEFIK_IMAGE:-traefik:v3.7.14}"
+POSTGRES_IMAGE="${POSTGRES_IMAGE:-postgres:18.6}"
+REDIS_IMAGE="${REDIS_IMAGE:-redis:8.10.2}"
POSTGRES_DATA_TARGET="${POSTGRES_DATA_TARGET:-}"
AGENTHITS_SCRIPT_BASE_URL="${AGENTHITS_SCRIPT_BASE_URL:-https://raw.githubusercontent.com/agentHits/dokploy/AgentHits-Dev}"
-DOCKER_VERSION="${DOCKER_VERSION:-28.5.0}"
+DOCKER_VERSION="${DOCKER_VERSION:-29.8.2}"
+NIXPACKS_VERSION="${NIXPACKS_VERSION:-1.41.0}"
+RAILPACK_VERSION="${RAILPACK_VERSION:-0.40.1}"
+BUILDPACKS_VERSION="${BUILDPACKS_VERSION:-0.40.9}"
+RCLONE_VERSION="${RCLONE_VERSION:-1.75.1}"
DOKPLOY_MACHINE="${DOKPLOY_MACHINE:-dokploy}"
DOKPLOY_MACHINE_DISTRO="${DOKPLOY_MACHINE_DISTRO:-ubuntu:noble}"
PASSTHROUGH_VARS=(
@@ -26,8 +30,15 @@ PASSTHROUGH_VARS=(
DOCKER_SWARM_INIT_ARGS
ADVERTISE_ADDR
PUBLIC_IP
+ DOKPLOY_SERVER_HARDENING
+ HARDEN_SSH
+ HARDEN_UFW
+ HARDEN_FAIL2BAN
)
ORB=""
+HARDEN_UFW="${HARDEN_UFW:-0}"
+HARDEN_SSH="${HARDEN_SSH:-0}"
+HARDEN_FAIL2BAN="${HARDEN_FAIL2BAN:-0}"
command_exists() {
command -v "$@" >/dev/null 2>&1
@@ -334,6 +345,51 @@ install_docker_if_missing() {
ensure_docker_running
}
+tool_version_matches() {
+ local expected="$1"
+ shift
+ command_exists "$1" && "$@" 2>/dev/null | grep -q -- "$expected"
+}
+
+install_toolchain() {
+ local arch="" pack_suffix=""
+ case "$(uname -m)" in
+ x86_64 | amd64)
+ arch=amd64
+ ;;
+ aarch64 | arm64)
+ arch=arm64
+ pack_suffix="-arm64"
+ ;;
+ *)
+ echo "Unsupported architecture for the toolchain: $(uname -m)" >&2
+ exit 1
+ ;;
+ esac
+
+ if ! tool_version_matches "$NIXPACKS_VERSION" nixpacks --version; then
+ NIXPACKS_VERSION="$NIXPACKS_VERSION" bash -c "$(curl -fsSL https://nixpacks.com/install.sh)"
+ fi
+ if ! tool_version_matches "$RAILPACK_VERSION" railpack --version; then
+ RAILPACK_VERSION="$RAILPACK_VERSION" bash -c "$(curl -fsSL https://railpack.com/install.sh)"
+ fi
+ if ! tool_version_matches "$BUILDPACKS_VERSION" pack --version; then
+ curl -sSL "https://github.com/buildpacks/pack/releases/download/v${BUILDPACKS_VERSION}/pack-v${BUILDPACKS_VERSION}-linux${pack_suffix}.tgz" | tar -C /usr/local/bin/ --no-same-owner -xz pack
+ fi
+ if ! tool_version_matches "$RCLONE_VERSION" rclone --version; then
+ if ! command_exists unzip; then
+ apt-get update -qq
+ DEBIAN_FRONTEND=noninteractive apt-get install -y -qq unzip >/dev/null
+ fi
+ local rclone_dir=""
+ rclone_dir="$(mktemp -d)"
+ curl -fsSL "https://downloads.rclone.org/v${RCLONE_VERSION}/rclone-v${RCLONE_VERSION}-linux-${arch}.zip" -o "$rclone_dir/rclone.zip"
+ unzip -q "$rclone_dir/rclone.zip" -d "$rclone_dir"
+ install -m 0755 "$rclone_dir/rclone-v${RCLONE_VERSION}-linux-${arch}/rclone" /usr/bin/rclone
+ rm -rf "$rclone_dir"
+ fi
+}
+
has_amd64_binfmt() {
# Without systemd (e.g. Alpine) binfmt_misc is often not mounted, which
# hides handlers the kernel already has.
@@ -619,6 +675,15 @@ install_on_macos() {
echo "Shell in the machine: $ORB -m $DOKPLOY_MACHINE -u root"
}
+harden_on_macos() {
+ if ! find_orb || ! orb_machine_exists; then
+ echo "OrbStack machine '$DOKPLOY_MACHINE' does not exist. Run this script with 'install' first." >&2
+ exit 1
+ fi
+ "$ORB" start "$DOKPLOY_MACHINE" >/dev/null 2>&1 || true
+ run_in_orb_machine harden
+}
+
update_on_macos() {
if ! find_orb || ! orb_machine_exists; then
echo "OrbStack machine '$DOKPLOY_MACHINE' does not exist. Run this script with 'install' first." >&2
@@ -628,12 +693,154 @@ update_on_macos() {
run_in_orb_machine update
}
+# Hardening flags (opt-in). They close the findings from Dokploy's
+# Setup Server > Security tab: UFW, SSH password login and Fail2Ban.
+ssh_listen_port() {
+ local port=""
+ port="$(sshd -T 2>/dev/null | awk '$1 == "port" { print $2; exit }')"
+ echo "${port:-22}"
+}
+
+require_root_public_key() {
+ local keys_file=/root/.ssh/authorized_keys
+ if [ ! -s "$keys_file" ] || ! ssh-keygen -l -f "$keys_file" >/dev/null 2>&1; then
+ echo "Refusing to turn off password login: $keys_file has no valid public key." >&2
+ echo "Add your public key to that file first, then run again." >&2
+ exit 1
+ fi
+}
+
+harden_ssh_keys_only() {
+ require_root_public_key
+
+ # The Security tab reads the main sshd_config, not drop-ins, and sshd keeps
+ # the first value it sees, so the options go on the first line of that file.
+ local config=/etc/ssh/sshd_config
+ local backup=/etc/ssh/sshd_config.agenthits-backup
+ if [ ! -f "$backup" ]; then
+ cp -p "$config" "$backup"
+ fi
+ local option=""
+ local key=""
+ for option in "PubkeyAuthentication yes" "PasswordAuthentication no" "KbdInteractiveAuthentication no" "PermitRootLogin prohibit-password" "UsePAM no"; do
+ key="${option%% *}"
+ sed -i -E "/^[#[:space:]]*${key}[[:space:]]/Id" "$config"
+ sed -i "1i ${option}" "$config"
+ done
+ if ! sshd -t; then
+ cp -p "$backup" "$config"
+ echo "sshd rejected the hardened config, the original was restored." >&2
+ exit 1
+ fi
+ systemctl reload ssh 2>/dev/null || systemctl reload sshd
+ echo "SSH: password login is off, keys only."
+}
+
+harden_ufw() {
+ if ! command_exists apt-get; then
+ echo "UFW: the --ufw flag supports Debian and Ubuntu only; skipped." >&2
+ return 0
+ fi
+
+ DEBIAN_FRONTEND=noninteractive apt-get install -y ufw >/dev/null
+ local ssh_port=""
+ ssh_port="$(ssh_listen_port)"
+
+ ufw default deny incoming >/dev/null
+ ufw default allow outgoing >/dev/null
+ # ufw drops forwarded traffic by default, which cuts container networking.
+ sed -i 's/^DEFAULT_FORWARD_POLICY=.*/DEFAULT_FORWARD_POLICY="ACCEPT"/' /etc/default/ufw
+ # SSH must be allowed before enabling, or the next login fails.
+ ufw allow "${ssh_port}/tcp" comment 'SSH' >/dev/null
+ ufw allow 80/tcp comment 'HTTP' >/dev/null
+ ufw allow 443/tcp comment 'HTTPS' >/dev/null
+ ufw allow 443/udp comment 'HTTP/3' >/dev/null
+ ufw allow 3000/tcp comment 'Dokploy panel' >/dev/null
+ ufw --force enable >/dev/null
+ echo "UFW: enabled. Incoming is denied except SSH (${ssh_port}), 80, 443 and 3000."
+}
+
+harden_fail2ban() {
+ if ! command_exists apt-get; then
+ echo "Fail2Ban: the --fail2ban flag supports Debian and Ubuntu only; skipped." >&2
+ return 0
+ fi
+
+ DEBIAN_FRONTEND=noninteractive apt-get install -y fail2ban >/dev/null
+ if [ ! -f /etc/fail2ban/jail.local ]; then
+ cat >/etc/fail2ban/jail.local <<'EOF'
+[sshd]
+enabled = true
+backend = auto
+mode = aggressive
+maxretry = 5
+findtime = 10m
+bantime = 1h
+EOF
+ fi
+ systemctl enable --now fail2ban >/dev/null 2>&1
+ systemctl restart fail2ban
+ echo "Fail2Ban: sshd jail is active in aggressive mode."
+}
+
+hardening_requested() {
+ [ "$HARDEN_SSH" = "1" ] || [ "$HARDEN_UFW" = "1" ] || [ "$HARDEN_FAIL2BAN" = "1" ]
+}
+
+traefik_version_from_image() {
+ case "$1" in
+ traefik:v*) echo "${1#traefik:v}" ;;
+ esac
+}
+
+# The panel hardens every server it sets up. An explicit DOKPLOY_SERVER_HARDENING
+# wins; otherwise it follows the host flags so one install flag covers both.
+panel_server_hardening() {
+ if [ -n "${DOKPLOY_SERVER_HARDENING:-}" ]; then
+ echo "$DOKPLOY_SERVER_HARDENING"
+ return 0
+ fi
+ local items=""
+ if [ "$HARDEN_SSH" = "1" ]; then
+ items="ssh"
+ fi
+ if [ "$HARDEN_UFW" = "1" ]; then
+ items="${items:+$items,}ufw"
+ fi
+ if [ "$HARDEN_FAIL2BAN" = "1" ]; then
+ items="${items:+$items,}fail2ban"
+ fi
+ echo "$items"
+}
+
+apply_hardening() {
+ if [ "$HARDEN_SSH" = "1" ]; then
+ if command_exists sshd; then
+ harden_ssh_keys_only
+ else
+ echo "SSH: no sshd on this host, skipped."
+ fi
+ fi
+ if [ "$HARDEN_UFW" = "1" ]; then
+ harden_ufw
+ fi
+ if [ "$HARDEN_FAIL2BAN" = "1" ]; then
+ harden_fail2ban
+ fi
+}
+
install_agenthits_dokploy() {
require_root_linux_host
+ # Check the key before anything is changed, so a missing key cannot
+ # leave a half-installed server behind.
+ if [ "$HARDEN_SSH" = "1" ] && command_exists sshd; then
+ require_root_public_key
+ fi
require_free_port 80
require_free_port 443
require_free_port 3000
install_docker_if_missing
+ install_toolchain
ensure_amd64_support
local endpoint_mode=""
@@ -680,6 +887,10 @@ install_agenthits_dokploy() {
if [ -n "${DOKPLOY_FORK_VERSION:-}" ]; then
fork_version_env_args=(-e "DOKPLOY_FORK_VERSION=$DOKPLOY_FORK_VERSION")
fi
+ local panel_hardening=""
+ panel_hardening="$(panel_server_hardening)"
+ local traefik_version=""
+ traefik_version="$(traefik_version_from_image "$TRAEFIK_IMAGE")"
docker service create \
--name dokploy-postgres \
@@ -718,6 +929,8 @@ install_agenthits_dokploy() {
--constraint 'node.role == manager' \
$endpoint_mode \
-e RELEASE_TAG="$DOKPLOY_RELEASE_TAG" \
+ -e DOKPLOY_SERVER_HARDENING="$panel_hardening" \
+ -e TRAEFIK_VERSION="$traefik_version" \
-e DOKPLOY_OFFICIAL_VERSION="$DOKPLOY_OFFICIAL_VERSION" \
"${fork_version_env_args[@]}" \
-e ADVERTISE_ADDR="$advertise_addr" \
@@ -741,6 +954,8 @@ install_agenthits_dokploy() {
docker network connect dokploy-network dokploy-traefik
+ apply_hardening
+
local public_ip="${PUBLIC_IP:-${ADVERTISE_ADDR:-$(get_public_ip)}}"
local formatted_addr
formatted_addr="$(format_ip_for_url "$public_ip")"
@@ -772,15 +987,56 @@ update_agenthits_dokploy() {
return "$status"
}
+parse_hardening_flags() {
+ local arg=""
+ for arg in "$@"; do
+ case "$arg" in
+ install | update | harden) ;;
+ --harden)
+ HARDEN_UFW=1
+ HARDEN_SSH=1
+ HARDEN_FAIL2BAN=1
+ ;;
+ --ufw)
+ HARDEN_UFW=1
+ ;;
+ --ssh-keys-only)
+ HARDEN_SSH=1
+ ;;
+ --fail2ban)
+ HARDEN_FAIL2BAN=1
+ ;;
+ *)
+ echo "Unknown argument: $arg" >&2
+ exit 1
+ ;;
+ esac
+ done
+}
+
main() {
- local mode="${1:-install}"
- case "$mode" in
- install | update) ;;
- *)
- echo "Usage: $0 [install|update]" >&2
- exit 1
- ;;
- esac
+ local mode="install"
+ if [ "$#" -gt 0 ]; then
+ case "$1" in
+ install | update | harden)
+ mode="$1"
+ ;;
+ --*) ;;
+ *)
+ echo "Usage: $0 [install|update|harden] [--harden] [--ufw] [--ssh-keys-only] [--fail2ban]" >&2
+ exit 1
+ ;;
+ esac
+ fi
+ parse_hardening_flags "$@"
+ if [ "$mode" = "update" ] && hardening_requested; then
+ echo "Hardening flags work with install and harden, not with update." >&2
+ exit 1
+ fi
+ if [ "$mode" = "harden" ] && ! hardening_requested; then
+ echo "harden needs at least one flag: --harden, --ufw, --ssh-keys-only or --fail2ban." >&2
+ exit 1
+ fi
local platform=""
platform="$(detect_platform)"
@@ -790,22 +1046,22 @@ main() {
echo "On macOS run this script as your own user, without sudo." >&2
exit 1
fi
- if [ "$mode" = "install" ]; then
- install_on_macos
- else
- update_on_macos
- fi
+ case "$mode" in
+ install) install_on_macos ;;
+ harden) harden_on_macos ;;
+ *) update_on_macos ;;
+ esac
;;
linux | wsl)
- reexec_as_root "$mode"
+ reexec_as_root "$@"
if [ "$platform" = "wsl" ]; then
prepare_wsl
fi
- if [ "$mode" = "install" ]; then
- install_agenthits_dokploy
- else
- update_agenthits_dokploy
- fi
+ case "$mode" in
+ install) install_agenthits_dokploy ;;
+ harden) apply_hardening ;;
+ *) update_agenthits_dokploy ;;
+ esac
;;
windows)
echo "On Windows run install-agenthits.ps1 in PowerShell (as administrator):" >&2
diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts
index 55470a0f2bb..a9aca1d3f5a 100644
--- a/packages/server/src/index.ts
+++ b/packages/server/src/index.ts
@@ -67,6 +67,7 @@ export * from "./setup/forward-auth-setup";
export * from "./setup/monitoring-setup";
export * from "./setup/postgres-setup";
export * from "./setup/server-audit";
+export * from "./setup/server-components";
export * from "./setup/server-setup";
export * from "./setup/server-validate";
export * from "./setup/setup";
diff --git a/packages/server/src/setup/component-versions.ts b/packages/server/src/setup/component-versions.ts
new file mode 100644
index 00000000000..92f1a5cdac8
--- /dev/null
+++ b/packages/server/src/setup/component-versions.ts
@@ -0,0 +1,19 @@
+// Every component version the fork installs. install-agenthits.sh, the
+// Dockerfile, the CI workflow and the Railpack picker in the UI repeat these
+// values; apps/dokploy/__test__/server/component-versions-sync.test.ts fails
+// when they drift from this object.
+export const PINNED_VERSIONS = {
+ node: "24.21.0",
+ docker: "29.8.2",
+ traefik: "3.7.14",
+ postgres: "18.6",
+ redis: "8.10.2",
+ nixpacks: "1.41.0",
+ railpack: "0.40.1",
+ buildpacks: "0.40.9",
+ rclone: "1.75.1",
+} as const;
+
+export const NIXPACKS_VERSION = PINNED_VERSIONS.nixpacks;
+export const RAILPACK_VERSION = PINNED_VERSIONS.railpack;
+export const BUILDPACKS_VERSION = PINNED_VERSIONS.buildpacks;
diff --git a/packages/server/src/setup/server-components.ts b/packages/server/src/setup/server-components.ts
new file mode 100644
index 00000000000..76fff4b869c
--- /dev/null
+++ b/packages/server/src/setup/server-components.ts
@@ -0,0 +1,225 @@
+import { Client } from "ssh2";
+import { findServerById } from "../services/server";
+import {
+ assertServerDestinationAllowed,
+ resolveServerDestinationHost,
+} from "../utils/servers/destination";
+import { PINNED_VERSIONS } from "./component-versions";
+import { rcloneInstallCommand } from "./server-setup";
+import { buildTraefikRunCommand, TRAEFIK_VERSION } from "./traefik-setup";
+
+export const COMPONENT_NAMES = [
+ "docker",
+ "traefik",
+ "nixpacks",
+ "railpack",
+ "buildpacks",
+ "rclone",
+] as const;
+
+export type ComponentName = (typeof COMPONENT_NAMES)[number];
+
+export const UPDATABLE_COMPONENTS = [
+ "docker",
+ "traefik",
+ "rclone",
+ "nixpacks",
+ "railpack",
+ "buildpacks",
+] as const;
+
+export type UpdatableComponent = (typeof UPDATABLE_COMPONENTS)[number];
+
+export const COMPONENTS_UPDATE_DONE = "Components update finished ✅";
+export const COMPONENTS_UPDATE_FAILED = "Components update failed ❌";
+
+const componentTargets: Record = {
+ docker: PINNED_VERSIONS.docker,
+ traefik: TRAEFIK_VERSION,
+ nixpacks: PINNED_VERSIONS.nixpacks,
+ railpack: PINNED_VERSIONS.railpack,
+ buildpacks: PINNED_VERSIONS.buildpacks,
+ rclone: PINNED_VERSIONS.rclone,
+};
+
+export const COMPONENT_CHECK_SCRIPT = `
+if [ "$(id -u)" -eq 0 ]; then SUDO_CMD=""; else SUDO_CMD="sudo"; fi
+echo "docker=$($SUDO_CMD docker version --format '{{.Server.Version}}' 2>/dev/null || true)"
+echo "traefik=$($SUDO_CMD docker inspect -f '{{.Config.Image}}' dokploy-traefik 2>/dev/null || true)"
+echo "nixpacks=$(nixpacks --version 2>/dev/null || true)"
+echo "railpack=$(railpack --version 2>/dev/null || true)"
+echo "buildpacks=$(pack --version 2>/dev/null || true)"
+echo "rclone=$(rclone --version 2>/dev/null | head -n 1 || true)"
+`;
+
+const SEMVER = /\d+\.\d+\.\d+/;
+
+const isComponentName = (value: string): value is ComponentName =>
+ (COMPONENT_NAMES as readonly string[]).includes(value);
+
+export const parseComponentVersions = (output: string) => {
+ const versions: Partial> = {};
+ for (const line of output.split("\n")) {
+ const separator = line.indexOf("=");
+ if (separator < 0) {
+ continue;
+ }
+ const name = line.slice(0, separator).trim();
+ if (!isComponentName(name)) {
+ continue;
+ }
+ versions[name] = line.slice(separator + 1).match(SEMVER)?.[0] ?? null;
+ }
+ return versions;
+};
+
+export type ComponentStatus = {
+ name: ComponentName;
+ installed: string | null;
+ target: string | null;
+ outdated: boolean;
+};
+
+export const buildComponentStatuses = (
+ installed: Partial>,
+): ComponentStatus[] =>
+ COMPONENT_NAMES.map((name) => {
+ const current = installed[name] ?? null;
+ const target = componentTargets[name];
+ return {
+ name,
+ installed: current,
+ target,
+ outdated: target !== null && current !== target,
+ };
+ });
+
+const updateStepFor = (component: UpdatableComponent) => {
+ switch (component) {
+ case "docker":
+ return `
+echo "Updating Docker to ${PINNED_VERSIONS.docker}"
+curl -fsSL https://get.docker.com -o /tmp/get-docker.sh
+$SUDO_CMD sh /tmp/get-docker.sh --version ${PINNED_VERSIONS.docker}
+rm -f /tmp/get-docker.sh
+echo "Docker version ${PINNED_VERSIONS.docker} installed ✅"
+`;
+ case "rclone":
+ return `
+${rcloneInstallCommand()}
+`;
+ case "traefik":
+ return `
+echo "Updating Traefik to ${TRAEFIK_VERSION}"
+$SUDO_CMD docker pull traefik:v${TRAEFIK_VERSION}
+$SUDO_CMD docker rm -f dokploy-traefik >/dev/null 2>&1 || true
+${buildTraefikRunCommand(TRAEFIK_VERSION)}
+echo "Traefik version ${TRAEFIK_VERSION} installed ✅"
+`;
+ case "nixpacks":
+ return `
+echo "Updating Nixpacks to ${PINNED_VERSIONS.nixpacks}"
+$SUDO_CMD env NIXPACKS_VERSION=${PINNED_VERSIONS.nixpacks} bash -c "$(curl -fsSL https://nixpacks.com/install.sh)"
+echo "Nixpacks version ${PINNED_VERSIONS.nixpacks} installed ✅"
+`;
+ case "railpack":
+ return `
+echo "Updating Railpack to ${PINNED_VERSIONS.railpack}"
+$SUDO_CMD env RAILPACK_VERSION=${PINNED_VERSIONS.railpack} bash -c "$(curl -fsSL https://railpack.com/install.sh)"
+echo "Railpack version ${PINNED_VERSIONS.railpack} installed ✅"
+`;
+ case "buildpacks":
+ return `
+SYS_ARCH=$(uname -m)
+SUFFIX=""
+if [ "$SYS_ARCH" = "aarch64" ] || [ "$SYS_ARCH" = "arm64" ]; then
+ SUFFIX="-arm64"
+fi
+echo "Updating Buildpacks (pack) to ${PINNED_VERSIONS.buildpacks}"
+curl -sSL "https://github.com/buildpacks/pack/releases/download/v${PINNED_VERSIONS.buildpacks}/pack-v${PINNED_VERSIONS.buildpacks}-linux$SUFFIX.tgz" | $SUDO_CMD tar -C /usr/local/bin/ --no-same-owner -xz pack
+echo "Buildpacks version ${PINNED_VERSIONS.buildpacks} installed ✅"
+`;
+ }
+};
+
+export const buildComponentUpdateScript = (
+ components: UpdatableComponent[],
+) => `
+set -e
+if [ "$(id -u)" -eq 0 ]; then SUDO_CMD=""; else SUDO_CMD="sudo"; fi
+${components.map(updateStepFor).join("\n")}
+echo "${COMPONENTS_UPDATE_DONE}"
+`;
+
+const runSshCommand = async (
+ serverId: string,
+ command: string,
+ onData?: (data: string) => void,
+) => {
+ const server = await findServerById(serverId);
+ if (!server.sshKeyId) {
+ throw new Error("No SSH Key found");
+ }
+ await assertServerDestinationAllowed(server);
+ const host = await resolveServerDestinationHost(server);
+ const client = new Client();
+
+ return new Promise<{ stdout: string; exitCode: number | null }>(
+ (resolve, reject) => {
+ client
+ .once("ready", () => {
+ client.exec(command, (err, stream) => {
+ if (err) {
+ client.end();
+ reject(err);
+ return;
+ }
+ let stdout = "";
+ stream
+ .on("close", (exitCode: number | null) => {
+ client.end();
+ resolve({ stdout, exitCode });
+ })
+ .on("data", (data: Buffer) => {
+ const text = data.toString();
+ stdout += text;
+ onData?.(text);
+ })
+ .stderr.on("data", (data: Buffer) => {
+ onData?.(data.toString());
+ });
+ });
+ })
+ .on("error", (err) => {
+ client.end();
+ reject(new Error(`SSH connection error: ${err.message}`));
+ })
+ .connect({
+ host,
+ port: server.port,
+ username: server.username,
+ privateKey: server.sshKey?.privateKey,
+ });
+ },
+ );
+};
+
+export const serverComponentsStatus = async (serverId: string) => {
+ const { stdout } = await runSshCommand(serverId, COMPONENT_CHECK_SCRIPT);
+ return buildComponentStatuses(parseComponentVersions(stdout));
+};
+
+export const updateServerComponents = async (
+ serverId: string,
+ components: UpdatableComponent[],
+ onData?: (data: string) => void,
+) => {
+ const { exitCode } = await runSshCommand(
+ serverId,
+ buildComponentUpdateScript(components),
+ onData,
+ );
+ if (exitCode !== 0) {
+ throw new Error(`Component update failed with exit code ${exitCode}`);
+ }
+};
diff --git a/packages/server/src/setup/server-setup.ts b/packages/server/src/setup/server-setup.ts
index 9f80f70a96d..f7c57ea1e8b 100644
--- a/packages/server/src/setup/server-setup.ts
+++ b/packages/server/src/setup/server-setup.ts
@@ -10,6 +10,7 @@ import {
updateServerById,
} from "@dokploy/server/services/server";
import {
+ buildTraefikRunCommand,
getDefaultMiddlewares,
getDefaultServerTraefikConfig,
TRAEFIK_HTTP3_PORT,
@@ -24,6 +25,12 @@ import {
assertServerDestinationAllowed,
resolveServerDestinationHost,
} from "../utils/servers/destination";
+import {
+ BUILDPACKS_VERSION,
+ NIXPACKS_VERSION,
+ PINNED_VERSIONS,
+ RAILPACK_VERSION,
+} from "./component-versions";
import { setupMonitoring } from "./monitoring-setup";
const generateToken = () => {
@@ -127,10 +134,105 @@ else
fi
`;
+type ServerHardening = {
+ ssh: boolean;
+ ufw: boolean;
+ fail2ban: boolean;
+};
+
+const getServerHardening = (): ServerHardening => {
+ const requested = (process.env.DOKPLOY_SERVER_HARDENING ?? "")
+ .split(",")
+ .map((item) => item.trim().toLowerCase());
+ const all = requested.includes("all");
+
+ return {
+ ssh: all || requested.includes("ssh"),
+ ufw: all || requested.includes("ufw"),
+ fail2ban: all || requested.includes("fail2ban"),
+ };
+};
+
+// The Security tab reads the main sshd_config, not drop-ins, so the options are
+// written there. They go on the first line because sshd keeps the first value it reads.
+const hardenSshCommand = () => `
+ if [ -f /etc/ssh/sshd_config ]; then
+ if [ ! -f /etc/ssh/sshd_config.dokploy-backup ]; then
+ $SUDO_CMD cp -p /etc/ssh/sshd_config /etc/ssh/sshd_config.dokploy-backup
+ fi
+ for option in "PubkeyAuthentication yes" "PasswordAuthentication no" "KbdInteractiveAuthentication no" "PermitRootLogin prohibit-password" "UsePAM no"; do
+ key=$(echo "$option" | awk '{print $1}')
+ $SUDO_CMD sed -i -E "/^[#[:space:]]*$key[[:space:]]/Id" /etc/ssh/sshd_config
+ $SUDO_CMD sed -i "1i $option" /etc/ssh/sshd_config
+ done
+ if ! $SUDO_CMD sshd -t; then
+ $SUDO_CMD cp -p /etc/ssh/sshd_config.dokploy-backup /etc/ssh/sshd_config
+ echo "sshd rejected the hardened config, the original was restored ❌" >&2
+ exit 1
+ fi
+ $SUDO_CMD systemctl reload ssh 2>/dev/null || $SUDO_CMD systemctl reload sshd
+ echo "SSH allows keys only ✅"
+ fi
+`;
+
+// ufw drops forwarded traffic by default, which breaks container networking,
+// so forwarding is accepted while incoming traffic to the host stays denied.
+const hardenUfwCommand = () => `
+ if [ "$OS_TYPE" = "ubuntu" ] || [ "$OS_TYPE" = "debian" ] || [ "$OS_TYPE" = "raspbian" ]; then
+ $SUDO_CMD apt-get install -y ufw >/dev/null
+ SSH_PORT=$($SUDO_CMD sshd -T 2>/dev/null | awk '$1 == "port" { print $2; exit }') || true
+ if [ -z "$SSH_PORT" ]; then
+ SSH_PORT=22
+ fi
+ $SUDO_CMD ufw default deny incoming >/dev/null
+ $SUDO_CMD ufw default allow outgoing >/dev/null
+ $SUDO_CMD sed -i 's/^DEFAULT_FORWARD_POLICY=.*/DEFAULT_FORWARD_POLICY="ACCEPT"/' /etc/default/ufw
+ $SUDO_CMD ufw allow "$SSH_PORT/tcp" comment 'SSH' >/dev/null
+ $SUDO_CMD ufw allow ${TRAEFIK_PORT}/tcp comment 'HTTP' >/dev/null
+ $SUDO_CMD ufw allow ${TRAEFIK_SSL_PORT}/tcp comment 'HTTPS' >/dev/null
+ $SUDO_CMD ufw allow ${TRAEFIK_HTTP3_PORT}/udp comment 'HTTP/3' >/dev/null
+ $SUDO_CMD ufw --force enable >/dev/null
+ echo "UFW is active ✅"
+ else
+ echo "UFW hardening supports Debian and Ubuntu only, skipped."
+ fi
+`;
+
+// The Security tab only reads /etc/fail2ban/jail.local, so the sshd jail goes there.
+const hardenFail2banCommand = () => `
+ if [ "$OS_TYPE" = "ubuntu" ] || [ "$OS_TYPE" = "debian" ] || [ "$OS_TYPE" = "raspbian" ]; then
+ $SUDO_CMD apt-get install -y fail2ban >/dev/null
+ if [ -f /etc/fail2ban/jail.local ]; then
+ echo "Fail2Ban: /etc/fail2ban/jail.local already exists, left unchanged."
+ else
+ printf '[sshd]\\nenabled = true\\nmode = aggressive\\n' | $SUDO_CMD tee /etc/fail2ban/jail.local >/dev/null
+ fi
+ $SUDO_CMD systemctl enable --now fail2ban >/dev/null 2>&1
+ $SUDO_CMD systemctl restart fail2ban
+ echo "Fail2Ban is active ✅"
+ else
+ echo "Fail2Ban hardening supports Debian and Ubuntu only, skipped."
+ fi
+`;
+
+const serverHardeningStep = (hardening: ServerHardening, step: number) => {
+ if (!hardening.ssh && !hardening.ufw && !hardening.fail2ban) {
+ return "";
+ }
+
+ return `
+echo -e "${step}. Applying security hardening"
+${hardening.ssh ? hardenSshCommand() : ""}
+${hardening.ufw ? hardenUfwCommand() : ""}
+${hardening.fail2ban ? hardenFail2banCommand() : ""}
+`;
+};
+
export const defaultCommand = (isBuildServer = false) => {
+ const hardening = getServerHardening();
const bashCommand = `
set -e;
-DOCKER_VERSION=28.5.0
+DOCKER_VERSION=${PINNED_VERSIONS.docker}
OS_TYPE=$(grep -w "ID" /etc/os-release | cut -d "=" -f 2 | tr -d '"')
SYS_ARCH=$(uname -m)
CURRENT_USER=$USER
@@ -183,17 +285,6 @@ else
OS_VERSION=$(grep -w "VERSION_ID" /etc/os-release | cut -d "=" -f 2 | tr -d '"')
fi
-# Ubuntu 26.04 (resolute) ships a docker-ce repo that has no 28.5.0, so the
-# default pin is absent from apt-cache madison and get.docker.com aborts before
-# installing Docker. Repin to a version present on every architecture Docker
-# ships resolute for: amd64/arm64/armhf start at 29.3.1, but s390x only carries
-# 29.4.0-29.4.2, so 29.4.2 is the newest version common to all of them. This
-# must run after OS_VERSION is resolved and before the banner so the reported
-# Docker version stays accurate.
-if [ "$OS_TYPE" = "ubuntu" ] && [ "$OS_VERSION" = "26.04" ]; then
- DOCKER_VERSION=29.4.2
-fi
-
if [ "$OS_TYPE" = 'amzn' ]; then
$SUDO_CMD dnf install -y findutils >/dev/null
fi
@@ -265,6 +356,7 @@ ${installRailpack()}
echo -e "14. Configuring permissions"
${setupPermissions()}
+${serverHardeningStep(hardening, 15)}
`
: `
echo -e "2. Installing Docker. "
@@ -285,6 +377,7 @@ ${installRailpack()}
echo -e "7. Configuring permissions"
${setupPermissions()}
+${serverHardeningStep(hardening, 8)}
`
}
`;
@@ -697,13 +790,22 @@ const createDefaultMiddlewares = () => {
return command;
};
+export const rcloneInstallCommand = () => `
+ RCLONE_RELEASE_ARCH=amd64
+ case "$(uname -m)" in aarch64 | arm64) RCLONE_RELEASE_ARCH=arm64 ;; esac
+ RCLONE_DIR=$(mktemp -d)
+ curl -fsSL "https://downloads.rclone.org/v${PINNED_VERSIONS.rclone}/rclone-v${PINNED_VERSIONS.rclone}-linux-$RCLONE_RELEASE_ARCH.zip" -o "$RCLONE_DIR/rclone.zip"
+ unzip -q "$RCLONE_DIR/rclone.zip" -d "$RCLONE_DIR"
+ $SUDO_CMD install -m 0755 "$RCLONE_DIR/rclone-v${PINNED_VERSIONS.rclone}-linux-$RCLONE_RELEASE_ARCH/rclone" /usr/bin/rclone
+ rm -rf "$RCLONE_DIR"
+ echo "RClone version ${PINNED_VERSIONS.rclone} installed ✅"
+`;
+
export const installRClone = () => `
if command_exists rclone; then
echo "RClone already installed ✅"
else
- curl https://rclone.org/install.sh | $SUDO_CMD bash
- RCLONE_VERSION=$(rclone --version | head -n 1 | awk '{print $2}' | sed 's/^v//')
- echo "RClone version $RCLONE_VERSION installed ✅"
+ ${rcloneInstallCommand()}
fi
`;
@@ -722,18 +824,7 @@ export const createTraefikInstance = () => {
else
# Create the dokploy-traefik container
TRAEFIK_VERSION=${TRAEFIK_VERSION}
- $SUDO_CMD docker run -d \
- --name dokploy-traefik \
- --restart always \
- -v /etc/dokploy/traefik/traefik.yml:/etc/traefik/traefik.yml \
- -v /etc/dokploy/traefik/dynamic:/etc/dokploy/traefik/dynamic \
- -v /var/run/docker.sock:/var/run/docker.sock \
- -p ${TRAEFIK_SSL_PORT}:${TRAEFIK_SSL_PORT} \
- -p ${TRAEFIK_PORT}:${TRAEFIK_PORT} \
- -p ${TRAEFIK_HTTP3_PORT}:${TRAEFIK_HTTP3_PORT}/udp \
- traefik:v$TRAEFIK_VERSION
-
- $SUDO_CMD docker network connect dokploy-network dokploy-traefik;
+ ${buildTraefikRunCommand(TRAEFIK_VERSION)}
echo "Traefik version $TRAEFIK_VERSION installed ✅"
fi
`;
@@ -745,8 +836,8 @@ const installNixpacks = () => `
if command_exists nixpacks; then
echo "Nixpacks already installed ✅"
else
- export NIXPACKS_VERSION=1.41.0
- $SUDO_CMD bash -c "$(curl -fsSL https://nixpacks.com/install.sh)"
+ export NIXPACKS_VERSION=${NIXPACKS_VERSION}
+ $SUDO_CMD env NIXPACKS_VERSION=${NIXPACKS_VERSION} bash -c "$(curl -fsSL https://nixpacks.com/install.sh)"
echo "Nixpacks version $NIXPACKS_VERSION installed ✅"
fi
`;
@@ -755,8 +846,8 @@ const installRailpack = () => `
if command_exists railpack; then
echo "Railpack already installed ✅"
else
- export RAILPACK_VERSION=0.15.4
- $SUDO_CMD bash -c "$(curl -fsSL https://railpack.com/install.sh)"
+ export RAILPACK_VERSION=${RAILPACK_VERSION}
+ $SUDO_CMD env RAILPACK_VERSION=${RAILPACK_VERSION} bash -c "$(curl -fsSL https://railpack.com/install.sh)"
echo "Railpack version $RAILPACK_VERSION installed ✅"
fi
`;
@@ -786,8 +877,8 @@ const installBuildpacks = () => `
if command_exists pack; then
echo "Buildpacks already installed ✅"
else
- BUILDPACKS_VERSION=0.39.1
- curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.39.1/pack-v$BUILDPACKS_VERSION-linux$SUFFIX.tgz" | $SUDO_CMD tar -C /usr/local/bin/ --no-same-owner -xzv pack
+ BUILDPACKS_VERSION=${BUILDPACKS_VERSION}
+ curl -sSL "https://github.com/buildpacks/pack/releases/download/v${BUILDPACKS_VERSION}/pack-v$BUILDPACKS_VERSION-linux$SUFFIX.tgz" | $SUDO_CMD tar -C /usr/local/bin/ --no-same-owner -xzv pack
echo "Buildpacks version $BUILDPACKS_VERSION installed ✅"
fi
`;
diff --git a/packages/server/src/setup/traefik-setup.ts b/packages/server/src/setup/traefik-setup.ts
index 17a9a284e95..012996bc69e 100644
--- a/packages/server/src/setup/traefik-setup.ts
+++ b/packages/server/src/setup/traefik-setup.ts
@@ -13,6 +13,7 @@ import { paths } from "../constants";
import { getRemoteDocker } from "../utils/servers/remote-docker";
import type { FileConfig } from "../utils/traefik/file-types";
import type { MainTraefikConfig } from "../utils/traefik/types";
+import { PINNED_VERSIONS } from "./component-versions";
export const TRAEFIK_SSL_PORT =
Number.parseInt(process.env.TRAEFIK_SSL_PORT!, 10) || 443;
@@ -20,7 +21,22 @@ export const TRAEFIK_PORT =
Number.parseInt(process.env.TRAEFIK_PORT!, 10) || 80;
export const TRAEFIK_HTTP3_PORT =
Number.parseInt(process.env.TRAEFIK_HTTP3_PORT!, 10) || 443;
-export const TRAEFIK_VERSION = process.env.TRAEFIK_VERSION || "3.6.25";
+export const TRAEFIK_VERSION =
+ process.env.TRAEFIK_VERSION || PINNED_VERSIONS.traefik;
+
+export const buildTraefikRunCommand = (version: string) => `
+ $SUDO_CMD docker run -d \
+ --name dokploy-traefik \
+ --restart always \
+ -v /etc/dokploy/traefik/traefik.yml:/etc/traefik/traefik.yml \
+ -v /etc/dokploy/traefik/dynamic:/etc/dokploy/traefik/dynamic \
+ -v /var/run/docker.sock:/var/run/docker.sock \
+ -p ${TRAEFIK_SSL_PORT}:${TRAEFIK_SSL_PORT} \
+ -p ${TRAEFIK_PORT}:${TRAEFIK_PORT} \
+ -p ${TRAEFIK_HTTP3_PORT}:${TRAEFIK_HTTP3_PORT}/udp \
+ traefik:v${version}
+ $SUDO_CMD docker network connect dokploy-network dokploy-traefik
+`;
export interface TraefikOptions {
env?: string[];
From 8a78f36632508d3b0f670be8d4f2095f88d07968 Mon Sep 17 00:00:00 2001
From: merge-check <140916359+agentHits@users.noreply.github.com>
Date: Thu, 8 Oct 2026 07:58:18 -0400
Subject: [PATCH 02/33] docs: describe the panel machine generically in the
hardening runbook
---
docs/server-hardening.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/server-hardening.md b/docs/server-hardening.md
index fbcaad17f2a..5c6b9e74cc3 100644
--- a/docs/server-hardening.md
+++ b/docs/server-hardening.md
@@ -107,7 +107,7 @@ orb -m dokploy -u root docker service update --env-rm DOKPLOY_SERVER_HARDENING d
2. С VPS:
- откат, описанный выше, если сервер нужен дальше;
- или переустановка ОС в панели хостинга. **Необратимо**: стирает весь диск, включая Docker, ключи и данные. Сначала сохрани бэкап.
-3. Панель на Mac mini целиком:
+3. Панель целиком (машина OrbStack):
```bash
orb delete dokploy
From 0f756322200093b91594e23d5d29dc3d00268bcb Mon Sep 17 00:00:00 2001
From: merge-check <140916359+agentHits@users.noreply.github.com>
Date: Thu, 8 Oct 2026 08:14:00 -0400
Subject: [PATCH 03/33] feat(db): default Railpack version for new applications
to 0.40.1
New applications now store Railpack 0.40.1 as their default version, matching
the pinned version the installers and the remote setup use. Migration 0207
only changes the column default: existing applications keep the version they
have stored.
Checked on a throwaway Postgres 18.6 instance: a row inserted before the
migration keeps 0.15.4, and a row inserted after it without the column gets
0.40.1. drizzle-kit reports no further schema changes.
Co-Authored-By: Claude Haiku 5.5
---
.../drizzle/0207_railpack_default_0_40_1.sql | 1 +
apps/dokploy/drizzle/meta/0207_snapshot.json | 11015 ++++++++++++++++
apps/dokploy/drizzle/meta/_journal.json | 7 +
docs/server-components-update.md | 2 +-
packages/server/src/db/schema/application.ts | 2 +-
5 files changed, 11025 insertions(+), 2 deletions(-)
create mode 100644 apps/dokploy/drizzle/0207_railpack_default_0_40_1.sql
create mode 100644 apps/dokploy/drizzle/meta/0207_snapshot.json
diff --git a/apps/dokploy/drizzle/0207_railpack_default_0_40_1.sql b/apps/dokploy/drizzle/0207_railpack_default_0_40_1.sql
new file mode 100644
index 00000000000..7ebcabe94f1
--- /dev/null
+++ b/apps/dokploy/drizzle/0207_railpack_default_0_40_1.sql
@@ -0,0 +1 @@
+ALTER TABLE "application" ALTER COLUMN "railpackVersion" SET DEFAULT '0.40.1';
\ No newline at end of file
diff --git a/apps/dokploy/drizzle/meta/0207_snapshot.json b/apps/dokploy/drizzle/meta/0207_snapshot.json
new file mode 100644
index 00000000000..be8495f85c2
--- /dev/null
+++ b/apps/dokploy/drizzle/meta/0207_snapshot.json
@@ -0,0 +1,11015 @@
+{
+ "id": "a972fd0d-a7c6-4845-91b5-a25ec5622fda",
+ "prevId": "6ce025aa-d986-48b8-9ce3-ce8fc0f169c4",
+ "version": "7",
+ "dialect": "postgresql",
+ "tables": {
+ "public.account": {
+ "name": "account",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "account_id": {
+ "name": "account_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "provider_id": {
+ "name": "provider_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "access_token": {
+ "name": "access_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "refresh_token": {
+ "name": "refresh_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "id_token": {
+ "name": "id_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "access_token_expires_at": {
+ "name": "access_token_expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "refresh_token_expires_at": {
+ "name": "refresh_token_expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "scope": {
+ "name": "scope",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "password": {
+ "name": "password",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "is2FAEnabled": {
+ "name": "is2FAEnabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "resetPasswordToken": {
+ "name": "resetPasswordToken",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "resetPasswordExpiresAt": {
+ "name": "resetPasswordExpiresAt",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "confirmationToken": {
+ "name": "confirmationToken",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "confirmationExpiresAt": {
+ "name": "confirmationExpiresAt",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "account_user_id_user_id_fk": {
+ "name": "account_user_id_user_id_fk",
+ "tableFrom": "account",
+ "tableTo": "user",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.apikey": {
+ "name": "apikey",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "start": {
+ "name": "start",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "prefix": {
+ "name": "prefix",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "key": {
+ "name": "key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "config_id": {
+ "name": "config_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'default'"
+ },
+ "reference_id": {
+ "name": "reference_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "refill_interval": {
+ "name": "refill_interval",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "refill_amount": {
+ "name": "refill_amount",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_refill_at": {
+ "name": "last_refill_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "enabled": {
+ "name": "enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "rate_limit_enabled": {
+ "name": "rate_limit_enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "rate_limit_time_window": {
+ "name": "rate_limit_time_window",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "rate_limit_max": {
+ "name": "rate_limit_max",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "request_count": {
+ "name": "request_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "remaining": {
+ "name": "remaining",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_request": {
+ "name": "last_request",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "permissions": {
+ "name": "permissions",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "metadata": {
+ "name": "metadata",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "apikey_reference_id_user_id_fk": {
+ "name": "apikey_reference_id_user_id_fk",
+ "tableFrom": "apikey",
+ "tableTo": "user",
+ "columnsFrom": [
+ "reference_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.apikey_secret": {
+ "name": "apikey_secret",
+ "schema": "",
+ "columns": {
+ "apikey_id": {
+ "name": "apikey_id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "key": {
+ "name": "key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "apikey_secret_apikey_id_apikey_id_fk": {
+ "name": "apikey_secret_apikey_id_apikey_id_fk",
+ "tableFrom": "apikey_secret",
+ "tableTo": "apikey",
+ "columnsFrom": [
+ "apikey_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.invitation": {
+ "name": "invitation",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "email": {
+ "name": "email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "role": {
+ "name": "role",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "inviter_id": {
+ "name": "inviter_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "team_id": {
+ "name": "team_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "invitation_organization_id_organization_id_fk": {
+ "name": "invitation_organization_id_organization_id_fk",
+ "tableFrom": "invitation",
+ "tableTo": "organization",
+ "columnsFrom": [
+ "organization_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "invitation_inviter_id_user_id_fk": {
+ "name": "invitation_inviter_id_user_id_fk",
+ "tableFrom": "invitation",
+ "tableTo": "user",
+ "columnsFrom": [
+ "inviter_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.member": {
+ "name": "member",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "role": {
+ "name": "role",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "team_id": {
+ "name": "team_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "is_default": {
+ "name": "is_default",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "canCreateProjects": {
+ "name": "canCreateProjects",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "canAccessToSSHKeys": {
+ "name": "canAccessToSSHKeys",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "canCreateServices": {
+ "name": "canCreateServices",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "canDeleteProjects": {
+ "name": "canDeleteProjects",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "canDeleteServices": {
+ "name": "canDeleteServices",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "canAccessToDocker": {
+ "name": "canAccessToDocker",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "canAccessToAPI": {
+ "name": "canAccessToAPI",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "canAccessToGitProviders": {
+ "name": "canAccessToGitProviders",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "canAccessToTraefikFiles": {
+ "name": "canAccessToTraefikFiles",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "canDeleteEnvironments": {
+ "name": "canDeleteEnvironments",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "canCreateEnvironments": {
+ "name": "canCreateEnvironments",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "accesedProjects": {
+ "name": "accesedProjects",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "ARRAY[]::text[]"
+ },
+ "accessedEnvironments": {
+ "name": "accessedEnvironments",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "ARRAY[]::text[]"
+ },
+ "accesedServices": {
+ "name": "accesedServices",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "ARRAY[]::text[]"
+ },
+ "accessedGitProviders": {
+ "name": "accessedGitProviders",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "ARRAY[]::text[]"
+ },
+ "accessedServers": {
+ "name": "accessedServers",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "ARRAY[]::text[]"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "member_organization_id_organization_id_fk": {
+ "name": "member_organization_id_organization_id_fk",
+ "tableFrom": "member",
+ "tableTo": "organization",
+ "columnsFrom": [
+ "organization_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "member_user_id_user_id_fk": {
+ "name": "member_user_id_user_id_fk",
+ "tableFrom": "member",
+ "tableTo": "user",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.organization": {
+ "name": "organization",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "slug": {
+ "name": "slug",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "logo": {
+ "name": "logo",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "metadata": {
+ "name": "metadata",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "default_role": {
+ "name": "default_role",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "owner_id": {
+ "name": "owner_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "organization_owner_id_user_id_fk": {
+ "name": "organization_owner_id_user_id_fk",
+ "tableFrom": "organization",
+ "tableTo": "user",
+ "columnsFrom": [
+ "owner_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "organization_slug_unique": {
+ "name": "organization_slug_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "slug"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.organization_role": {
+ "name": "organization_role",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "role": {
+ "name": "role",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "permission": {
+ "name": "permission",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "organizationRole_organizationId_idx": {
+ "name": "organizationRole_organizationId_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "organizationRole_role_idx": {
+ "name": "organizationRole_role_idx",
+ "columns": [
+ {
+ "expression": "role",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "organization_role_organization_id_organization_id_fk": {
+ "name": "organization_role_organization_id_organization_id_fk",
+ "tableFrom": "organization_role",
+ "tableTo": "organization",
+ "columnsFrom": [
+ "organization_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.passkey": {
+ "name": "passkey",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "public_key": {
+ "name": "public_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "credential_id": {
+ "name": "credential_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "counter": {
+ "name": "counter",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "device_type": {
+ "name": "device_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "backed_up": {
+ "name": "backed_up",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "transports": {
+ "name": "transports",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "aaguid": {
+ "name": "aaguid",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "passkey_userId_idx": {
+ "name": "passkey_userId_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "passkey_credentialID_idx": {
+ "name": "passkey_credentialID_idx",
+ "columns": [
+ {
+ "expression": "credential_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "passkey_user_id_user_id_fk": {
+ "name": "passkey_user_id_user_id_fk",
+ "tableFrom": "passkey",
+ "tableTo": "user",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.two_factor": {
+ "name": "two_factor",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "secret": {
+ "name": "secret",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "backup_codes": {
+ "name": "backup_codes",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "verified": {
+ "name": "verified",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "failed_verification_count": {
+ "name": "failed_verification_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "locked_until": {
+ "name": "locked_until",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "two_factor_user_id_user_id_fk": {
+ "name": "two_factor_user_id_user_id_fk",
+ "tableFrom": "two_factor",
+ "tableTo": "user",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.verification": {
+ "name": "verification",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "identifier": {
+ "name": "identifier",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "value": {
+ "name": "value",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.ai": {
+ "name": "ai",
+ "schema": "",
+ "columns": {
+ "aiId": {
+ "name": "aiId",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "apiUrl": {
+ "name": "apiUrl",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "apiKey": {
+ "name": "apiKey",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "model": {
+ "name": "model",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "isEnabled": {
+ "name": "isEnabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "organizationId": {
+ "name": "organizationId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "ai_organizationId_organization_id_fk": {
+ "name": "ai_organizationId_organization_id_fk",
+ "tableFrom": "ai",
+ "tableTo": "organization",
+ "columnsFrom": [
+ "organizationId"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.application": {
+ "name": "application",
+ "schema": "",
+ "columns": {
+ "applicationId": {
+ "name": "applicationId",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "appName": {
+ "name": "appName",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "env": {
+ "name": "env",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "previewEnv": {
+ "name": "previewEnv",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "watchPaths": {
+ "name": "watchPaths",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "previewBuildArgs": {
+ "name": "previewBuildArgs",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "previewBuildSecrets": {
+ "name": "previewBuildSecrets",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "previewLabels": {
+ "name": "previewLabels",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "previewWildcard": {
+ "name": "previewWildcard",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "previewPort": {
+ "name": "previewPort",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "default": 3000
+ },
+ "previewHttps": {
+ "name": "previewHttps",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "previewPath": {
+ "name": "previewPath",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'/'"
+ },
+ "certificateType": {
+ "name": "certificateType",
+ "type": "certificateType",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'none'"
+ },
+ "previewCustomCertResolver": {
+ "name": "previewCustomCertResolver",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "previewLimit": {
+ "name": "previewLimit",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "default": 3
+ },
+ "isPreviewDeploymentsActive": {
+ "name": "isPreviewDeploymentsActive",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false,
+ "default": false
+ },
+ "previewRequireCollaboratorPermissions": {
+ "name": "previewRequireCollaboratorPermissions",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false,
+ "default": true
+ },
+ "rollbackActive": {
+ "name": "rollbackActive",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false,
+ "default": false
+ },
+ "buildArgs": {
+ "name": "buildArgs",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "buildSecrets": {
+ "name": "buildSecrets",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "memoryReservation": {
+ "name": "memoryReservation",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "memoryLimit": {
+ "name": "memoryLimit",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "cpuReservation": {
+ "name": "cpuReservation",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "cpuLimit": {
+ "name": "cpuLimit",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "enabled": {
+ "name": "enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "subtitle": {
+ "name": "subtitle",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "command": {
+ "name": "command",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "args": {
+ "name": "args",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "icon": {
+ "name": "icon",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "refreshToken": {
+ "name": "refreshToken",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "sourceType": {
+ "name": "sourceType",
+ "type": "sourceType",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'github'"
+ },
+ "cleanCache": {
+ "name": "cleanCache",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false,
+ "default": false
+ },
+ "repository": {
+ "name": "repository",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "owner": {
+ "name": "owner",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "branch": {
+ "name": "branch",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "buildPath": {
+ "name": "buildPath",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'/'"
+ },
+ "triggerType": {
+ "name": "triggerType",
+ "type": "triggerType",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'push'"
+ },
+ "autoDeploy": {
+ "name": "autoDeploy",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "gitlabProjectId": {
+ "name": "gitlabProjectId",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "gitlabRepository": {
+ "name": "gitlabRepository",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "gitlabOwner": {
+ "name": "gitlabOwner",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "gitlabBranch": {
+ "name": "gitlabBranch",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "gitlabBuildPath": {
+ "name": "gitlabBuildPath",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'/'"
+ },
+ "gitlabPathNamespace": {
+ "name": "gitlabPathNamespace",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "giteaRepository": {
+ "name": "giteaRepository",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "giteaOwner": {
+ "name": "giteaOwner",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "giteaBranch": {
+ "name": "giteaBranch",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "giteaBuildPath": {
+ "name": "giteaBuildPath",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'/'"
+ },
+ "bitbucketRepository": {
+ "name": "bitbucketRepository",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "bitbucketRepositorySlug": {
+ "name": "bitbucketRepositorySlug",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "bitbucketOwner": {
+ "name": "bitbucketOwner",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "bitbucketBranch": {
+ "name": "bitbucketBranch",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "bitbucketBuildPath": {
+ "name": "bitbucketBuildPath",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'/'"
+ },
+ "username": {
+ "name": "username",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "password": {
+ "name": "password",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "dockerImage": {
+ "name": "dockerImage",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "registryUrl": {
+ "name": "registryUrl",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "customGitUrl": {
+ "name": "customGitUrl",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "customGitBranch": {
+ "name": "customGitBranch",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "customGitBuildPath": {
+ "name": "customGitBuildPath",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "customGitSSHKeyId": {
+ "name": "customGitSSHKeyId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "enableSubmodules": {
+ "name": "enableSubmodules",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "dockerfile": {
+ "name": "dockerfile",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'Dockerfile'"
+ },
+ "dockerContextPath": {
+ "name": "dockerContextPath",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "dockerBuildStage": {
+ "name": "dockerBuildStage",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "dropBuildPath": {
+ "name": "dropBuildPath",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "healthCheckSwarm": {
+ "name": "healthCheckSwarm",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "restartPolicySwarm": {
+ "name": "restartPolicySwarm",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "placementSwarm": {
+ "name": "placementSwarm",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updateConfigSwarm": {
+ "name": "updateConfigSwarm",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "rollbackConfigSwarm": {
+ "name": "rollbackConfigSwarm",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "modeSwarm": {
+ "name": "modeSwarm",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "labelsSwarm": {
+ "name": "labelsSwarm",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "networkSwarm": {
+ "name": "networkSwarm",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "stopGracePeriodSwarm": {
+ "name": "stopGracePeriodSwarm",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "endpointSpecSwarm": {
+ "name": "endpointSpecSwarm",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "ulimitsSwarm": {
+ "name": "ulimitsSwarm",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "replicas": {
+ "name": "replicas",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 1
+ },
+ "applicationStatus": {
+ "name": "applicationStatus",
+ "type": "applicationStatus",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'idle'"
+ },
+ "buildType": {
+ "name": "buildType",
+ "type": "buildType",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'nixpacks'"
+ },
+ "railpackVersion": {
+ "name": "railpackVersion",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'0.40.1'"
+ },
+ "herokuVersion": {
+ "name": "herokuVersion",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'24'"
+ },
+ "publishDirectory": {
+ "name": "publishDirectory",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "isStaticSpa": {
+ "name": "isStaticSpa",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "createEnvFile": {
+ "name": "createEnvFile",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "registryId": {
+ "name": "registryId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "rollbackRegistryId": {
+ "name": "rollbackRegistryId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "environmentId": {
+ "name": "environmentId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "githubId": {
+ "name": "githubId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "gitlabId": {
+ "name": "gitlabId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "giteaId": {
+ "name": "giteaId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "bitbucketId": {
+ "name": "bitbucketId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "serverId": {
+ "name": "serverId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "buildServerId": {
+ "name": "buildServerId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "buildRegistryId": {
+ "name": "buildRegistryId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "networkIds": {
+ "name": "networkIds",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'{}'"
+ },
+ "detachDokployNetwork": {
+ "name": "detachDokployNetwork",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "application_customGitSSHKeyId_ssh-key_sshKeyId_fk": {
+ "name": "application_customGitSSHKeyId_ssh-key_sshKeyId_fk",
+ "tableFrom": "application",
+ "tableTo": "ssh-key",
+ "columnsFrom": [
+ "customGitSSHKeyId"
+ ],
+ "columnsTo": [
+ "sshKeyId"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "application_registryId_registry_registryId_fk": {
+ "name": "application_registryId_registry_registryId_fk",
+ "tableFrom": "application",
+ "tableTo": "registry",
+ "columnsFrom": [
+ "registryId"
+ ],
+ "columnsTo": [
+ "registryId"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "application_rollbackRegistryId_registry_registryId_fk": {
+ "name": "application_rollbackRegistryId_registry_registryId_fk",
+ "tableFrom": "application",
+ "tableTo": "registry",
+ "columnsFrom": [
+ "rollbackRegistryId"
+ ],
+ "columnsTo": [
+ "registryId"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "application_environmentId_environment_environmentId_fk": {
+ "name": "application_environmentId_environment_environmentId_fk",
+ "tableFrom": "application",
+ "tableTo": "environment",
+ "columnsFrom": [
+ "environmentId"
+ ],
+ "columnsTo": [
+ "environmentId"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "application_githubId_github_githubId_fk": {
+ "name": "application_githubId_github_githubId_fk",
+ "tableFrom": "application",
+ "tableTo": "github",
+ "columnsFrom": [
+ "githubId"
+ ],
+ "columnsTo": [
+ "githubId"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "application_gitlabId_gitlab_gitlabId_fk": {
+ "name": "application_gitlabId_gitlab_gitlabId_fk",
+ "tableFrom": "application",
+ "tableTo": "gitlab",
+ "columnsFrom": [
+ "gitlabId"
+ ],
+ "columnsTo": [
+ "gitlabId"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "application_giteaId_gitea_giteaId_fk": {
+ "name": "application_giteaId_gitea_giteaId_fk",
+ "tableFrom": "application",
+ "tableTo": "gitea",
+ "columnsFrom": [
+ "giteaId"
+ ],
+ "columnsTo": [
+ "giteaId"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "application_bitbucketId_bitbucket_bitbucketId_fk": {
+ "name": "application_bitbucketId_bitbucket_bitbucketId_fk",
+ "tableFrom": "application",
+ "tableTo": "bitbucket",
+ "columnsFrom": [
+ "bitbucketId"
+ ],
+ "columnsTo": [
+ "bitbucketId"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "application_serverId_server_serverId_fk": {
+ "name": "application_serverId_server_serverId_fk",
+ "tableFrom": "application",
+ "tableTo": "server",
+ "columnsFrom": [
+ "serverId"
+ ],
+ "columnsTo": [
+ "serverId"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "application_buildServerId_server_serverId_fk": {
+ "name": "application_buildServerId_server_serverId_fk",
+ "tableFrom": "application",
+ "tableTo": "server",
+ "columnsFrom": [
+ "buildServerId"
+ ],
+ "columnsTo": [
+ "serverId"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "application_buildRegistryId_registry_registryId_fk": {
+ "name": "application_buildRegistryId_registry_registryId_fk",
+ "tableFrom": "application",
+ "tableTo": "registry",
+ "columnsFrom": [
+ "buildRegistryId"
+ ],
+ "columnsTo": [
+ "registryId"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "application_appName_unique": {
+ "name": "application_appName_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "appName"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.audit_log": {
+ "name": "audit_log",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "user_email": {
+ "name": "user_email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_role": {
+ "name": "user_role",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "action": {
+ "name": "action",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "resource_type": {
+ "name": "resource_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "resource_id": {
+ "name": "resource_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "resource_name": {
+ "name": "resource_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "metadata": {
+ "name": "metadata",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "auditLog_organizationId_idx": {
+ "name": "auditLog_organizationId_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "auditLog_userId_idx": {
+ "name": "auditLog_userId_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "auditLog_createdAt_idx": {
+ "name": "auditLog_createdAt_idx",
+ "columns": [
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "audit_log_organization_id_organization_id_fk": {
+ "name": "audit_log_organization_id_organization_id_fk",
+ "tableFrom": "audit_log",
+ "tableTo": "organization",
+ "columnsFrom": [
+ "organization_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "audit_log_user_id_user_id_fk": {
+ "name": "audit_log_user_id_user_id_fk",
+ "tableFrom": "audit_log",
+ "tableTo": "user",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.backup": {
+ "name": "backup",
+ "schema": "",
+ "columns": {
+ "backupId": {
+ "name": "backupId",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "appName": {
+ "name": "appName",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "schedule": {
+ "name": "schedule",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "enabled": {
+ "name": "enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "database": {
+ "name": "database",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "prefix": {
+ "name": "prefix",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "serviceName": {
+ "name": "serviceName",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "destinationId": {
+ "name": "destinationId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "keepLatestCount": {
+ "name": "keepLatestCount",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "includeEncryptionKey": {
+ "name": "includeEncryptionKey",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "backupType": {
+ "name": "backupType",
+ "type": "backupType",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'database'"
+ },
+ "databaseType": {
+ "name": "databaseType",
+ "type": "databaseType",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "composeId": {
+ "name": "composeId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "postgresId": {
+ "name": "postgresId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "mariadbId": {
+ "name": "mariadbId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "mysqlId": {
+ "name": "mysqlId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "mongoId": {
+ "name": "mongoId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "libsqlId": {
+ "name": "libsqlId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "userId": {
+ "name": "userId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "metadata": {
+ "name": "metadata",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "backup_destinationId_destination_destinationId_fk": {
+ "name": "backup_destinationId_destination_destinationId_fk",
+ "tableFrom": "backup",
+ "tableTo": "destination",
+ "columnsFrom": [
+ "destinationId"
+ ],
+ "columnsTo": [
+ "destinationId"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "backup_composeId_compose_composeId_fk": {
+ "name": "backup_composeId_compose_composeId_fk",
+ "tableFrom": "backup",
+ "tableTo": "compose",
+ "columnsFrom": [
+ "composeId"
+ ],
+ "columnsTo": [
+ "composeId"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "backup_postgresId_postgres_postgresId_fk": {
+ "name": "backup_postgresId_postgres_postgresId_fk",
+ "tableFrom": "backup",
+ "tableTo": "postgres",
+ "columnsFrom": [
+ "postgresId"
+ ],
+ "columnsTo": [
+ "postgresId"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "backup_mariadbId_mariadb_mariadbId_fk": {
+ "name": "backup_mariadbId_mariadb_mariadbId_fk",
+ "tableFrom": "backup",
+ "tableTo": "mariadb",
+ "columnsFrom": [
+ "mariadbId"
+ ],
+ "columnsTo": [
+ "mariadbId"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "backup_mysqlId_mysql_mysqlId_fk": {
+ "name": "backup_mysqlId_mysql_mysqlId_fk",
+ "tableFrom": "backup",
+ "tableTo": "mysql",
+ "columnsFrom": [
+ "mysqlId"
+ ],
+ "columnsTo": [
+ "mysqlId"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "backup_mongoId_mongo_mongoId_fk": {
+ "name": "backup_mongoId_mongo_mongoId_fk",
+ "tableFrom": "backup",
+ "tableTo": "mongo",
+ "columnsFrom": [
+ "mongoId"
+ ],
+ "columnsTo": [
+ "mongoId"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "backup_libsqlId_libsql_libsqlId_fk": {
+ "name": "backup_libsqlId_libsql_libsqlId_fk",
+ "tableFrom": "backup",
+ "tableTo": "libsql",
+ "columnsFrom": [
+ "libsqlId"
+ ],
+ "columnsTo": [
+ "libsqlId"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "backup_userId_user_id_fk": {
+ "name": "backup_userId_user_id_fk",
+ "tableFrom": "backup",
+ "tableTo": "user",
+ "columnsFrom": [
+ "userId"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "backup_appName_unique": {
+ "name": "backup_appName_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "appName"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.bitbucket": {
+ "name": "bitbucket",
+ "schema": "",
+ "columns": {
+ "bitbucketId": {
+ "name": "bitbucketId",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "bitbucketUsername": {
+ "name": "bitbucketUsername",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "bitbucketEmail": {
+ "name": "bitbucketEmail",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "appPassword": {
+ "name": "appPassword",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "apiToken": {
+ "name": "apiToken",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "bitbucketWorkspaceName": {
+ "name": "bitbucketWorkspaceName",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "gitProviderId": {
+ "name": "gitProviderId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "bitbucket_gitProviderId_git_provider_gitProviderId_fk": {
+ "name": "bitbucket_gitProviderId_git_provider_gitProviderId_fk",
+ "tableFrom": "bitbucket",
+ "tableTo": "git_provider",
+ "columnsFrom": [
+ "gitProviderId"
+ ],
+ "columnsTo": [
+ "gitProviderId"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.certificate": {
+ "name": "certificate",
+ "schema": "",
+ "columns": {
+ "certificateId": {
+ "name": "certificateId",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "certificateData": {
+ "name": "certificateData",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "privateKey": {
+ "name": "privateKey",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "certificatePath": {
+ "name": "certificatePath",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "autoRenew": {
+ "name": "autoRenew",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "organizationId": {
+ "name": "organizationId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "serverId": {
+ "name": "serverId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "certificate_organizationId_organization_id_fk": {
+ "name": "certificate_organizationId_organization_id_fk",
+ "tableFrom": "certificate",
+ "tableTo": "organization",
+ "columnsFrom": [
+ "organizationId"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "certificate_serverId_server_serverId_fk": {
+ "name": "certificate_serverId_server_serverId_fk",
+ "tableFrom": "certificate",
+ "tableTo": "server",
+ "columnsFrom": [
+ "serverId"
+ ],
+ "columnsTo": [
+ "serverId"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "certificate_certificatePath_unique": {
+ "name": "certificate_certificatePath_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "certificatePath"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.compose": {
+ "name": "compose",
+ "schema": "",
+ "columns": {
+ "composeId": {
+ "name": "composeId",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "appName": {
+ "name": "appName",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "env": {
+ "name": "env",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "composeFile": {
+ "name": "composeFile",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "''"
+ },
+ "refreshToken": {
+ "name": "refreshToken",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "sourceType": {
+ "name": "sourceType",
+ "type": "sourceTypeCompose",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'github'"
+ },
+ "composeType": {
+ "name": "composeType",
+ "type": "composeType",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'docker-compose'"
+ },
+ "repository": {
+ "name": "repository",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "owner": {
+ "name": "owner",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "branch": {
+ "name": "branch",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "autoDeploy": {
+ "name": "autoDeploy",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "gitlabProjectId": {
+ "name": "gitlabProjectId",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "gitlabRepository": {
+ "name": "gitlabRepository",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "gitlabOwner": {
+ "name": "gitlabOwner",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "gitlabBranch": {
+ "name": "gitlabBranch",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "gitlabPathNamespace": {
+ "name": "gitlabPathNamespace",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "bitbucketRepository": {
+ "name": "bitbucketRepository",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "bitbucketRepositorySlug": {
+ "name": "bitbucketRepositorySlug",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "bitbucketOwner": {
+ "name": "bitbucketOwner",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "bitbucketBranch": {
+ "name": "bitbucketBranch",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "giteaRepository": {
+ "name": "giteaRepository",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "giteaOwner": {
+ "name": "giteaOwner",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "giteaBranch": {
+ "name": "giteaBranch",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "customGitUrl": {
+ "name": "customGitUrl",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "customGitBranch": {
+ "name": "customGitBranch",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "customGitSSHKeyId": {
+ "name": "customGitSSHKeyId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "command": {
+ "name": "command",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "''"
+ },
+ "createEnvFile": {
+ "name": "createEnvFile",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "enableSubmodules": {
+ "name": "enableSubmodules",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "composePath": {
+ "name": "composePath",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'./docker-compose.yml'"
+ },
+ "suffix": {
+ "name": "suffix",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "''"
+ },
+ "randomize": {
+ "name": "randomize",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "isolatedDeployment": {
+ "name": "isolatedDeployment",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "isolatedDeploymentsVolume": {
+ "name": "isolatedDeploymentsVolume",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "pullImages": {
+ "name": "pullImages",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "triggerType": {
+ "name": "triggerType",
+ "type": "triggerType",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'push'"
+ },
+ "composeStatus": {
+ "name": "composeStatus",
+ "type": "applicationStatus",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'idle'"
+ },
+ "icon": {
+ "name": "icon",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "environmentId": {
+ "name": "environmentId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "watchPaths": {
+ "name": "watchPaths",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "githubId": {
+ "name": "githubId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "gitlabId": {
+ "name": "gitlabId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "bitbucketId": {
+ "name": "bitbucketId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "giteaId": {
+ "name": "giteaId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "serverId": {
+ "name": "serverId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "serviceNetworks": {
+ "name": "serviceNetworks",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'[]'::jsonb"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "compose_customGitSSHKeyId_ssh-key_sshKeyId_fk": {
+ "name": "compose_customGitSSHKeyId_ssh-key_sshKeyId_fk",
+ "tableFrom": "compose",
+ "tableTo": "ssh-key",
+ "columnsFrom": [
+ "customGitSSHKeyId"
+ ],
+ "columnsTo": [
+ "sshKeyId"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "compose_environmentId_environment_environmentId_fk": {
+ "name": "compose_environmentId_environment_environmentId_fk",
+ "tableFrom": "compose",
+ "tableTo": "environment",
+ "columnsFrom": [
+ "environmentId"
+ ],
+ "columnsTo": [
+ "environmentId"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "compose_githubId_github_githubId_fk": {
+ "name": "compose_githubId_github_githubId_fk",
+ "tableFrom": "compose",
+ "tableTo": "github",
+ "columnsFrom": [
+ "githubId"
+ ],
+ "columnsTo": [
+ "githubId"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "compose_gitlabId_gitlab_gitlabId_fk": {
+ "name": "compose_gitlabId_gitlab_gitlabId_fk",
+ "tableFrom": "compose",
+ "tableTo": "gitlab",
+ "columnsFrom": [
+ "gitlabId"
+ ],
+ "columnsTo": [
+ "gitlabId"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "compose_bitbucketId_bitbucket_bitbucketId_fk": {
+ "name": "compose_bitbucketId_bitbucket_bitbucketId_fk",
+ "tableFrom": "compose",
+ "tableTo": "bitbucket",
+ "columnsFrom": [
+ "bitbucketId"
+ ],
+ "columnsTo": [
+ "bitbucketId"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "compose_giteaId_gitea_giteaId_fk": {
+ "name": "compose_giteaId_gitea_giteaId_fk",
+ "tableFrom": "compose",
+ "tableTo": "gitea",
+ "columnsFrom": [
+ "giteaId"
+ ],
+ "columnsTo": [
+ "giteaId"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "compose_serverId_server_serverId_fk": {
+ "name": "compose_serverId_server_serverId_fk",
+ "tableFrom": "compose",
+ "tableTo": "server",
+ "columnsFrom": [
+ "serverId"
+ ],
+ "columnsTo": [
+ "serverId"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.deploymentOperation": {
+ "name": "deploymentOperation",
+ "schema": "",
+ "columns": {
+ "operationId": {
+ "name": "operationId",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "composeId": {
+ "name": "composeId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "idempotencyKeyHash": {
+ "name": "idempotencyKeyHash",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "sourceRevision": {
+ "name": "sourceRevision",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "resolvedRevision": {
+ "name": "resolvedRevision",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "envRevision": {
+ "name": "envRevision",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "deploymentOperationStatus",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'accepted'"
+ },
+ "deploymentId": {
+ "name": "deploymentId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updatedAt": {
+ "name": "updatedAt",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "startedAt": {
+ "name": "startedAt",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "finishedAt": {
+ "name": "finishedAt",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "deploymentOperation_composeId_compose_composeId_fk": {
+ "name": "deploymentOperation_composeId_compose_composeId_fk",
+ "tableFrom": "deploymentOperation",
+ "tableTo": "compose",
+ "columnsFrom": [
+ "composeId"
+ ],
+ "columnsTo": [
+ "composeId"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "deploymentOperation_deploymentId_deployment_deploymentId_fk": {
+ "name": "deploymentOperation_deploymentId_deployment_deploymentId_fk",
+ "tableFrom": "deploymentOperation",
+ "tableTo": "deployment",
+ "columnsFrom": [
+ "deploymentId"
+ ],
+ "columnsTo": [
+ "deploymentId"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "deployment_operation_compose_key_unique": {
+ "name": "deployment_operation_compose_key_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "composeId",
+ "idempotencyKeyHash"
+ ]
+ },
+ "deployment_operation_deployment_unique": {
+ "name": "deployment_operation_deployment_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "deploymentId"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.deployment": {
+ "name": "deployment",
+ "schema": "",
+ "columns": {
+ "deploymentId": {
+ "name": "deploymentId",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "status": {
+ "name": "status",
+ "type": "deploymentStatus",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'running'"
+ },
+ "logPath": {
+ "name": "logPath",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "pid": {
+ "name": "pid",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "applicationId": {
+ "name": "applicationId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "composeId": {
+ "name": "composeId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "serverId": {
+ "name": "serverId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "isPreviewDeployment": {
+ "name": "isPreviewDeployment",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false,
+ "default": false
+ },
+ "previewDeploymentId": {
+ "name": "previewDeploymentId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "startedAt": {
+ "name": "startedAt",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "finishedAt": {
+ "name": "finishedAt",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "errorMessage": {
+ "name": "errorMessage",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "scheduleId": {
+ "name": "scheduleId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "backupId": {
+ "name": "backupId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "rollbackId": {
+ "name": "rollbackId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "volumeBackupId": {
+ "name": "volumeBackupId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "buildServerId": {
+ "name": "buildServerId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "deployment_applicationId_application_applicationId_fk": {
+ "name": "deployment_applicationId_application_applicationId_fk",
+ "tableFrom": "deployment",
+ "tableTo": "application",
+ "columnsFrom": [
+ "applicationId"
+ ],
+ "columnsTo": [
+ "applicationId"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "deployment_composeId_compose_composeId_fk": {
+ "name": "deployment_composeId_compose_composeId_fk",
+ "tableFrom": "deployment",
+ "tableTo": "compose",
+ "columnsFrom": [
+ "composeId"
+ ],
+ "columnsTo": [
+ "composeId"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "deployment_serverId_server_serverId_fk": {
+ "name": "deployment_serverId_server_serverId_fk",
+ "tableFrom": "deployment",
+ "tableTo": "server",
+ "columnsFrom": [
+ "serverId"
+ ],
+ "columnsTo": [
+ "serverId"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "deployment_previewDeploymentId_preview_deployments_previewDeploymentId_fk": {
+ "name": "deployment_previewDeploymentId_preview_deployments_previewDeploymentId_fk",
+ "tableFrom": "deployment",
+ "tableTo": "preview_deployments",
+ "columnsFrom": [
+ "previewDeploymentId"
+ ],
+ "columnsTo": [
+ "previewDeploymentId"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "deployment_scheduleId_schedule_scheduleId_fk": {
+ "name": "deployment_scheduleId_schedule_scheduleId_fk",
+ "tableFrom": "deployment",
+ "tableTo": "schedule",
+ "columnsFrom": [
+ "scheduleId"
+ ],
+ "columnsTo": [
+ "scheduleId"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "deployment_backupId_backup_backupId_fk": {
+ "name": "deployment_backupId_backup_backupId_fk",
+ "tableFrom": "deployment",
+ "tableTo": "backup",
+ "columnsFrom": [
+ "backupId"
+ ],
+ "columnsTo": [
+ "backupId"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "deployment_rollbackId_rollback_rollbackId_fk": {
+ "name": "deployment_rollbackId_rollback_rollbackId_fk",
+ "tableFrom": "deployment",
+ "tableTo": "rollback",
+ "columnsFrom": [
+ "rollbackId"
+ ],
+ "columnsTo": [
+ "rollbackId"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "deployment_volumeBackupId_volume_backup_volumeBackupId_fk": {
+ "name": "deployment_volumeBackupId_volume_backup_volumeBackupId_fk",
+ "tableFrom": "deployment",
+ "tableTo": "volume_backup",
+ "columnsFrom": [
+ "volumeBackupId"
+ ],
+ "columnsTo": [
+ "volumeBackupId"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "deployment_buildServerId_server_serverId_fk": {
+ "name": "deployment_buildServerId_server_serverId_fk",
+ "tableFrom": "deployment",
+ "tableTo": "server",
+ "columnsFrom": [
+ "buildServerId"
+ ],
+ "columnsTo": [
+ "serverId"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.destination": {
+ "name": "destination",
+ "schema": "",
+ "columns": {
+ "destinationId": {
+ "name": "destinationId",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "provider": {
+ "name": "provider",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "accessKey": {
+ "name": "accessKey",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "secretAccessKey": {
+ "name": "secretAccessKey",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "bucket": {
+ "name": "bucket",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "region": {
+ "name": "region",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "endpoint": {
+ "name": "endpoint",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "additionalFlags": {
+ "name": "additionalFlags",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "organizationId": {
+ "name": "organizationId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "destination_organizationId_organization_id_fk": {
+ "name": "destination_organizationId_organization_id_fk",
+ "tableFrom": "destination",
+ "tableTo": "organization",
+ "columnsFrom": [
+ "organizationId"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.dns_provider": {
+ "name": "dns_provider",
+ "schema": "",
+ "columns": {
+ "dnsProviderId": {
+ "name": "dnsProviderId",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "providerType": {
+ "name": "providerType",
+ "type": "DnsProviderType",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "config": {
+ "name": "config",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "organizationId": {
+ "name": "organizationId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "dns_provider_org_name_idx": {
+ "name": "dns_provider_org_name_idx",
+ "columns": [
+ {
+ "expression": "organizationId",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "dns_provider_organizationId_organization_id_fk": {
+ "name": "dns_provider_organizationId_organization_id_fk",
+ "tableFrom": "dns_provider",
+ "tableTo": "organization",
+ "columnsFrom": [
+ "organizationId"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.domain": {
+ "name": "domain",
+ "schema": "",
+ "columns": {
+ "domainId": {
+ "name": "domainId",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "host": {
+ "name": "host",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "https": {
+ "name": "https",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "port": {
+ "name": "port",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "default": 3000
+ },
+ "customEntrypoint": {
+ "name": "customEntrypoint",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "path": {
+ "name": "path",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'/'"
+ },
+ "serviceName": {
+ "name": "serviceName",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "domainType": {
+ "name": "domainType",
+ "type": "domainType",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'application'"
+ },
+ "uniqueConfigKey": {
+ "name": "uniqueConfigKey",
+ "type": "serial",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "composeId": {
+ "name": "composeId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "customCertResolver": {
+ "name": "customCertResolver",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "applicationId": {
+ "name": "applicationId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "previewDeploymentId": {
+ "name": "previewDeploymentId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "certificateType": {
+ "name": "certificateType",
+ "type": "certificateType",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'none'"
+ },
+ "internalPath": {
+ "name": "internalPath",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'/'"
+ },
+ "stripPath": {
+ "name": "stripPath",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "middlewares": {
+ "name": "middlewares",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "ARRAY[]::text[]"
+ },
+ "forwardAuthEnabled": {
+ "name": "forwardAuthEnabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "enabled": {
+ "name": "enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "domain_composeId_compose_composeId_fk": {
+ "name": "domain_composeId_compose_composeId_fk",
+ "tableFrom": "domain",
+ "tableTo": "compose",
+ "columnsFrom": [
+ "composeId"
+ ],
+ "columnsTo": [
+ "composeId"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "domain_applicationId_application_applicationId_fk": {
+ "name": "domain_applicationId_application_applicationId_fk",
+ "tableFrom": "domain",
+ "tableTo": "application",
+ "columnsFrom": [
+ "applicationId"
+ ],
+ "columnsTo": [
+ "applicationId"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "domain_previewDeploymentId_preview_deployments_previewDeploymentId_fk": {
+ "name": "domain_previewDeploymentId_preview_deployments_previewDeploymentId_fk",
+ "tableFrom": "domain",
+ "tableTo": "preview_deployments",
+ "columnsFrom": [
+ "previewDeploymentId"
+ ],
+ "columnsTo": [
+ "previewDeploymentId"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.environment": {
+ "name": "environment",
+ "schema": "",
+ "columns": {
+ "environmentId": {
+ "name": "environmentId",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "env": {
+ "name": "env",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "''"
+ },
+ "projectId": {
+ "name": "projectId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "isDefault": {
+ "name": "isDefault",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "environment_projectId_project_projectId_fk": {
+ "name": "environment_projectId_project_projectId_fk",
+ "tableFrom": "environment",
+ "tableTo": "project",
+ "columnsFrom": [
+ "projectId"
+ ],
+ "columnsTo": [
+ "projectId"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.forward_auth_settings": {
+ "name": "forward_auth_settings",
+ "schema": "",
+ "columns": {
+ "forwardAuthSettingsId": {
+ "name": "forwardAuthSettingsId",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "authDomain": {
+ "name": "authDomain",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "baseDomain": {
+ "name": "baseDomain",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "https": {
+ "name": "https",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "certificateType": {
+ "name": "certificateType",
+ "type": "certificateType",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'letsencrypt'"
+ },
+ "customCertResolver": {
+ "name": "customCertResolver",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "providerId": {
+ "name": "providerId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "serverId": {
+ "name": "serverId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "forward_auth_settings_providerId_sso_provider_provider_id_fk": {
+ "name": "forward_auth_settings_providerId_sso_provider_provider_id_fk",
+ "tableFrom": "forward_auth_settings",
+ "tableTo": "sso_provider",
+ "columnsFrom": [
+ "providerId"
+ ],
+ "columnsTo": [
+ "provider_id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "forward_auth_settings_serverId_server_serverId_fk": {
+ "name": "forward_auth_settings_serverId_server_serverId_fk",
+ "tableFrom": "forward_auth_settings",
+ "tableTo": "server",
+ "columnsFrom": [
+ "serverId"
+ ],
+ "columnsTo": [
+ "serverId"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "forward_auth_settings_serverId_unique": {
+ "name": "forward_auth_settings_serverId_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "serverId"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.git_provider": {
+ "name": "git_provider",
+ "schema": "",
+ "columns": {
+ "gitProviderId": {
+ "name": "gitProviderId",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "providerType": {
+ "name": "providerType",
+ "type": "gitProviderType",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'github'"
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "organizationId": {
+ "name": "organizationId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "userId": {
+ "name": "userId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "sharedWithOrganization": {
+ "name": "sharedWithOrganization",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "git_provider_organizationId_organization_id_fk": {
+ "name": "git_provider_organizationId_organization_id_fk",
+ "tableFrom": "git_provider",
+ "tableTo": "organization",
+ "columnsFrom": [
+ "organizationId"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "git_provider_userId_user_id_fk": {
+ "name": "git_provider_userId_user_id_fk",
+ "tableFrom": "git_provider",
+ "tableTo": "user",
+ "columnsFrom": [
+ "userId"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.gitea": {
+ "name": "gitea",
+ "schema": "",
+ "columns": {
+ "giteaId": {
+ "name": "giteaId",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "giteaUrl": {
+ "name": "giteaUrl",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'https://gitea.com'"
+ },
+ "giteaInternalUrl": {
+ "name": "giteaInternalUrl",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "redirect_uri": {
+ "name": "redirect_uri",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "client_id": {
+ "name": "client_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "client_secret": {
+ "name": "client_secret",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "organization_name": {
+ "name": "organization_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "gitProviderId": {
+ "name": "gitProviderId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "access_token": {
+ "name": "access_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "refresh_token": {
+ "name": "refresh_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "scopes": {
+ "name": "scopes",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'repo,repo:status,read:user,read:org'"
+ },
+ "last_authenticated_at": {
+ "name": "last_authenticated_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "gitea_gitProviderId_git_provider_gitProviderId_fk": {
+ "name": "gitea_gitProviderId_git_provider_gitProviderId_fk",
+ "tableFrom": "gitea",
+ "tableTo": "git_provider",
+ "columnsFrom": [
+ "gitProviderId"
+ ],
+ "columnsTo": [
+ "gitProviderId"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.github": {
+ "name": "github",
+ "schema": "",
+ "columns": {
+ "githubId": {
+ "name": "githubId",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "githubAppName": {
+ "name": "githubAppName",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "githubAppId": {
+ "name": "githubAppId",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "githubClientId": {
+ "name": "githubClientId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "githubClientSecret": {
+ "name": "githubClientSecret",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "githubInstallationId": {
+ "name": "githubInstallationId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "githubPrivateKey": {
+ "name": "githubPrivateKey",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "githubWebhookSecret": {
+ "name": "githubWebhookSecret",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "githubUrl": {
+ "name": "githubUrl",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'https://github.com'"
+ },
+ "gitProviderId": {
+ "name": "gitProviderId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "github_gitProviderId_git_provider_gitProviderId_fk": {
+ "name": "github_gitProviderId_git_provider_gitProviderId_fk",
+ "tableFrom": "github",
+ "tableTo": "git_provider",
+ "columnsFrom": [
+ "gitProviderId"
+ ],
+ "columnsTo": [
+ "gitProviderId"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.gitlab": {
+ "name": "gitlab",
+ "schema": "",
+ "columns": {
+ "gitlabId": {
+ "name": "gitlabId",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "gitlabUrl": {
+ "name": "gitlabUrl",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'https://gitlab.com'"
+ },
+ "gitlabInternalUrl": {
+ "name": "gitlabInternalUrl",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "application_id": {
+ "name": "application_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "redirect_uri": {
+ "name": "redirect_uri",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "secret": {
+ "name": "secret",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "webhook_secret": {
+ "name": "webhook_secret",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "access_token": {
+ "name": "access_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "refresh_token": {
+ "name": "refresh_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "group_name": {
+ "name": "group_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "gitProviderId": {
+ "name": "gitProviderId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "gitlab_gitProviderId_git_provider_gitProviderId_fk": {
+ "name": "gitlab_gitProviderId_git_provider_gitProviderId_fk",
+ "tableFrom": "gitlab",
+ "tableTo": "git_provider",
+ "columnsFrom": [
+ "gitProviderId"
+ ],
+ "columnsTo": [
+ "gitProviderId"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.libsql": {
+ "name": "libsql",
+ "schema": "",
+ "columns": {
+ "libsqlId": {
+ "name": "libsqlId",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "appName": {
+ "name": "appName",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "databaseUser": {
+ "name": "databaseUser",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "databasePassword": {
+ "name": "databasePassword",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "sqldNode": {
+ "name": "sqldNode",
+ "type": "sqldNode",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'primary'"
+ },
+ "sqldPrimaryUrl": {
+ "name": "sqldPrimaryUrl",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "enableNamespaces": {
+ "name": "enableNamespaces",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "dockerImage": {
+ "name": "dockerImage",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "command": {
+ "name": "command",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "env": {
+ "name": "env",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "memoryReservation": {
+ "name": "memoryReservation",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "memoryLimit": {
+ "name": "memoryLimit",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "cpuReservation": {
+ "name": "cpuReservation",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "cpuLimit": {
+ "name": "cpuLimit",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "externalPort": {
+ "name": "externalPort",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "externalGRPCPort": {
+ "name": "externalGRPCPort",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "externalAdminPort": {
+ "name": "externalAdminPort",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "applicationStatus": {
+ "name": "applicationStatus",
+ "type": "applicationStatus",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'idle'"
+ },
+ "healthCheckSwarm": {
+ "name": "healthCheckSwarm",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "restartPolicySwarm": {
+ "name": "restartPolicySwarm",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "placementSwarm": {
+ "name": "placementSwarm",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updateConfigSwarm": {
+ "name": "updateConfigSwarm",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "rollbackConfigSwarm": {
+ "name": "rollbackConfigSwarm",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "modeSwarm": {
+ "name": "modeSwarm",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "labelsSwarm": {
+ "name": "labelsSwarm",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "networkSwarm": {
+ "name": "networkSwarm",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "stopGracePeriodSwarm": {
+ "name": "stopGracePeriodSwarm",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "endpointSpecSwarm": {
+ "name": "endpointSpecSwarm",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "replicas": {
+ "name": "replicas",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 1
+ },
+ "networkIds": {
+ "name": "networkIds",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'{}'"
+ },
+ "detachDokployNetwork": {
+ "name": "detachDokployNetwork",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "environmentId": {
+ "name": "environmentId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "serverId": {
+ "name": "serverId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "libsql_environmentId_environment_environmentId_fk": {
+ "name": "libsql_environmentId_environment_environmentId_fk",
+ "tableFrom": "libsql",
+ "tableTo": "environment",
+ "columnsFrom": [
+ "environmentId"
+ ],
+ "columnsTo": [
+ "environmentId"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "libsql_serverId_server_serverId_fk": {
+ "name": "libsql_serverId_server_serverId_fk",
+ "tableFrom": "libsql",
+ "tableTo": "server",
+ "columnsFrom": [
+ "serverId"
+ ],
+ "columnsTo": [
+ "serverId"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "libsql_appName_unique": {
+ "name": "libsql_appName_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "appName"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.mariadb": {
+ "name": "mariadb",
+ "schema": "",
+ "columns": {
+ "mariadbId": {
+ "name": "mariadbId",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "appName": {
+ "name": "appName",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "databaseName": {
+ "name": "databaseName",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "databaseUser": {
+ "name": "databaseUser",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "databasePassword": {
+ "name": "databasePassword",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "rootPassword": {
+ "name": "rootPassword",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "dockerImage": {
+ "name": "dockerImage",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "command": {
+ "name": "command",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "args": {
+ "name": "args",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "env": {
+ "name": "env",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "memoryReservation": {
+ "name": "memoryReservation",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "memoryLimit": {
+ "name": "memoryLimit",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "cpuReservation": {
+ "name": "cpuReservation",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "cpuLimit": {
+ "name": "cpuLimit",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "externalPort": {
+ "name": "externalPort",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "applicationStatus": {
+ "name": "applicationStatus",
+ "type": "applicationStatus",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'idle'"
+ },
+ "healthCheckSwarm": {
+ "name": "healthCheckSwarm",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "restartPolicySwarm": {
+ "name": "restartPolicySwarm",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "placementSwarm": {
+ "name": "placementSwarm",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updateConfigSwarm": {
+ "name": "updateConfigSwarm",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "rollbackConfigSwarm": {
+ "name": "rollbackConfigSwarm",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "modeSwarm": {
+ "name": "modeSwarm",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "labelsSwarm": {
+ "name": "labelsSwarm",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "networkSwarm": {
+ "name": "networkSwarm",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "stopGracePeriodSwarm": {
+ "name": "stopGracePeriodSwarm",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "endpointSpecSwarm": {
+ "name": "endpointSpecSwarm",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "ulimitsSwarm": {
+ "name": "ulimitsSwarm",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "replicas": {
+ "name": "replicas",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 1
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "environmentId": {
+ "name": "environmentId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "serverId": {
+ "name": "serverId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "networkIds": {
+ "name": "networkIds",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'{}'"
+ },
+ "detachDokployNetwork": {
+ "name": "detachDokployNetwork",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "mariadb_environmentId_environment_environmentId_fk": {
+ "name": "mariadb_environmentId_environment_environmentId_fk",
+ "tableFrom": "mariadb",
+ "tableTo": "environment",
+ "columnsFrom": [
+ "environmentId"
+ ],
+ "columnsTo": [
+ "environmentId"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "mariadb_serverId_server_serverId_fk": {
+ "name": "mariadb_serverId_server_serverId_fk",
+ "tableFrom": "mariadb",
+ "tableTo": "server",
+ "columnsFrom": [
+ "serverId"
+ ],
+ "columnsTo": [
+ "serverId"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "mariadb_appName_unique": {
+ "name": "mariadb_appName_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "appName"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.mongo": {
+ "name": "mongo",
+ "schema": "",
+ "columns": {
+ "mongoId": {
+ "name": "mongoId",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "appName": {
+ "name": "appName",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "databaseUser": {
+ "name": "databaseUser",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "databasePassword": {
+ "name": "databasePassword",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "dockerImage": {
+ "name": "dockerImage",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'mongo:8'"
+ },
+ "command": {
+ "name": "command",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "args": {
+ "name": "args",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "env": {
+ "name": "env",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "memoryReservation": {
+ "name": "memoryReservation",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "memoryLimit": {
+ "name": "memoryLimit",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "cpuReservation": {
+ "name": "cpuReservation",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "cpuLimit": {
+ "name": "cpuLimit",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "externalPort": {
+ "name": "externalPort",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "applicationStatus": {
+ "name": "applicationStatus",
+ "type": "applicationStatus",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'idle'"
+ },
+ "healthCheckSwarm": {
+ "name": "healthCheckSwarm",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "restartPolicySwarm": {
+ "name": "restartPolicySwarm",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "placementSwarm": {
+ "name": "placementSwarm",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updateConfigSwarm": {
+ "name": "updateConfigSwarm",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "rollbackConfigSwarm": {
+ "name": "rollbackConfigSwarm",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "modeSwarm": {
+ "name": "modeSwarm",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "labelsSwarm": {
+ "name": "labelsSwarm",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "networkSwarm": {
+ "name": "networkSwarm",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "stopGracePeriodSwarm": {
+ "name": "stopGracePeriodSwarm",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "endpointSpecSwarm": {
+ "name": "endpointSpecSwarm",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "ulimitsSwarm": {
+ "name": "ulimitsSwarm",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "replicas": {
+ "name": "replicas",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 1
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "environmentId": {
+ "name": "environmentId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "serverId": {
+ "name": "serverId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "replicaSets": {
+ "name": "replicaSets",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false,
+ "default": false
+ },
+ "networkIds": {
+ "name": "networkIds",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'{}'"
+ },
+ "detachDokployNetwork": {
+ "name": "detachDokployNetwork",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "mongo_environmentId_environment_environmentId_fk": {
+ "name": "mongo_environmentId_environment_environmentId_fk",
+ "tableFrom": "mongo",
+ "tableTo": "environment",
+ "columnsFrom": [
+ "environmentId"
+ ],
+ "columnsTo": [
+ "environmentId"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "mongo_serverId_server_serverId_fk": {
+ "name": "mongo_serverId_server_serverId_fk",
+ "tableFrom": "mongo",
+ "tableTo": "server",
+ "columnsFrom": [
+ "serverId"
+ ],
+ "columnsTo": [
+ "serverId"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "mongo_appName_unique": {
+ "name": "mongo_appName_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "appName"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.mount": {
+ "name": "mount",
+ "schema": "",
+ "columns": {
+ "mountId": {
+ "name": "mountId",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "type": {
+ "name": "type",
+ "type": "mountType",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "hostPath": {
+ "name": "hostPath",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "volumeName": {
+ "name": "volumeName",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "filePath": {
+ "name": "filePath",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "content": {
+ "name": "content",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "serviceType": {
+ "name": "serviceType",
+ "type": "serviceType",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'application'"
+ },
+ "mountPath": {
+ "name": "mountPath",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "applicationId": {
+ "name": "applicationId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "composeId": {
+ "name": "composeId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "libsqlId": {
+ "name": "libsqlId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "mariadbId": {
+ "name": "mariadbId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "mongoId": {
+ "name": "mongoId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "mysqlId": {
+ "name": "mysqlId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "postgresId": {
+ "name": "postgresId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "redisId": {
+ "name": "redisId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "mount_applicationId_application_applicationId_fk": {
+ "name": "mount_applicationId_application_applicationId_fk",
+ "tableFrom": "mount",
+ "tableTo": "application",
+ "columnsFrom": [
+ "applicationId"
+ ],
+ "columnsTo": [
+ "applicationId"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "mount_composeId_compose_composeId_fk": {
+ "name": "mount_composeId_compose_composeId_fk",
+ "tableFrom": "mount",
+ "tableTo": "compose",
+ "columnsFrom": [
+ "composeId"
+ ],
+ "columnsTo": [
+ "composeId"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "mount_libsqlId_libsql_libsqlId_fk": {
+ "name": "mount_libsqlId_libsql_libsqlId_fk",
+ "tableFrom": "mount",
+ "tableTo": "libsql",
+ "columnsFrom": [
+ "libsqlId"
+ ],
+ "columnsTo": [
+ "libsqlId"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "mount_mariadbId_mariadb_mariadbId_fk": {
+ "name": "mount_mariadbId_mariadb_mariadbId_fk",
+ "tableFrom": "mount",
+ "tableTo": "mariadb",
+ "columnsFrom": [
+ "mariadbId"
+ ],
+ "columnsTo": [
+ "mariadbId"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "mount_mongoId_mongo_mongoId_fk": {
+ "name": "mount_mongoId_mongo_mongoId_fk",
+ "tableFrom": "mount",
+ "tableTo": "mongo",
+ "columnsFrom": [
+ "mongoId"
+ ],
+ "columnsTo": [
+ "mongoId"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "mount_mysqlId_mysql_mysqlId_fk": {
+ "name": "mount_mysqlId_mysql_mysqlId_fk",
+ "tableFrom": "mount",
+ "tableTo": "mysql",
+ "columnsFrom": [
+ "mysqlId"
+ ],
+ "columnsTo": [
+ "mysqlId"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "mount_postgresId_postgres_postgresId_fk": {
+ "name": "mount_postgresId_postgres_postgresId_fk",
+ "tableFrom": "mount",
+ "tableTo": "postgres",
+ "columnsFrom": [
+ "postgresId"
+ ],
+ "columnsTo": [
+ "postgresId"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "mount_redisId_redis_redisId_fk": {
+ "name": "mount_redisId_redis_redisId_fk",
+ "tableFrom": "mount",
+ "tableTo": "redis",
+ "columnsFrom": [
+ "redisId"
+ ],
+ "columnsTo": [
+ "redisId"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.mysql": {
+ "name": "mysql",
+ "schema": "",
+ "columns": {
+ "mysqlId": {
+ "name": "mysqlId",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "appName": {
+ "name": "appName",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "databaseName": {
+ "name": "databaseName",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "databaseUser": {
+ "name": "databaseUser",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "databasePassword": {
+ "name": "databasePassword",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "rootPassword": {
+ "name": "rootPassword",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "dockerImage": {
+ "name": "dockerImage",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "command": {
+ "name": "command",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "args": {
+ "name": "args",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "env": {
+ "name": "env",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "memoryReservation": {
+ "name": "memoryReservation",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "memoryLimit": {
+ "name": "memoryLimit",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "cpuReservation": {
+ "name": "cpuReservation",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "cpuLimit": {
+ "name": "cpuLimit",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "externalPort": {
+ "name": "externalPort",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "applicationStatus": {
+ "name": "applicationStatus",
+ "type": "applicationStatus",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'idle'"
+ },
+ "healthCheckSwarm": {
+ "name": "healthCheckSwarm",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "restartPolicySwarm": {
+ "name": "restartPolicySwarm",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "placementSwarm": {
+ "name": "placementSwarm",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updateConfigSwarm": {
+ "name": "updateConfigSwarm",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "rollbackConfigSwarm": {
+ "name": "rollbackConfigSwarm",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "modeSwarm": {
+ "name": "modeSwarm",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "labelsSwarm": {
+ "name": "labelsSwarm",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "networkSwarm": {
+ "name": "networkSwarm",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "stopGracePeriodSwarm": {
+ "name": "stopGracePeriodSwarm",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "endpointSpecSwarm": {
+ "name": "endpointSpecSwarm",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "ulimitsSwarm": {
+ "name": "ulimitsSwarm",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "replicas": {
+ "name": "replicas",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 1
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "environmentId": {
+ "name": "environmentId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "serverId": {
+ "name": "serverId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "networkIds": {
+ "name": "networkIds",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'{}'"
+ },
+ "detachDokployNetwork": {
+ "name": "detachDokployNetwork",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "mysql_environmentId_environment_environmentId_fk": {
+ "name": "mysql_environmentId_environment_environmentId_fk",
+ "tableFrom": "mysql",
+ "tableTo": "environment",
+ "columnsFrom": [
+ "environmentId"
+ ],
+ "columnsTo": [
+ "environmentId"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "mysql_serverId_server_serverId_fk": {
+ "name": "mysql_serverId_server_serverId_fk",
+ "tableFrom": "mysql",
+ "tableTo": "server",
+ "columnsFrom": [
+ "serverId"
+ ],
+ "columnsTo": [
+ "serverId"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "mysql_appName_unique": {
+ "name": "mysql_appName_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "appName"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.network": {
+ "name": "network",
+ "schema": "",
+ "columns": {
+ "networkId": {
+ "name": "networkId",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "dockerId": {
+ "name": "dockerId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "driver": {
+ "name": "driver",
+ "type": "networkDriver",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'bridge'"
+ },
+ "internal": {
+ "name": "internal",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "attachable": {
+ "name": "attachable",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "enableIPv4": {
+ "name": "enableIPv4",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "enableIPv6": {
+ "name": "enableIPv6",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "mtu": {
+ "name": "mtu",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "ipam": {
+ "name": "ipam",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'{}'::jsonb"
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "organizationId": {
+ "name": "organizationId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "serverId": {
+ "name": "serverId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "network_organizationId_organization_id_fk": {
+ "name": "network_organizationId_organization_id_fk",
+ "tableFrom": "network",
+ "tableTo": "organization",
+ "columnsFrom": [
+ "organizationId"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "network_serverId_server_serverId_fk": {
+ "name": "network_serverId_server_serverId_fk",
+ "tableFrom": "network",
+ "tableTo": "server",
+ "columnsFrom": [
+ "serverId"
+ ],
+ "columnsTo": [
+ "serverId"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.custom": {
+ "name": "custom",
+ "schema": "",
+ "columns": {
+ "customId": {
+ "name": "customId",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "endpoint": {
+ "name": "endpoint",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "headers": {
+ "name": "headers",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.discord": {
+ "name": "discord",
+ "schema": "",
+ "columns": {
+ "discordId": {
+ "name": "discordId",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "webhookUrl": {
+ "name": "webhookUrl",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "decoration": {
+ "name": "decoration",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.email": {
+ "name": "email",
+ "schema": "",
+ "columns": {
+ "emailId": {
+ "name": "emailId",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "smtpServer": {
+ "name": "smtpServer",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "smtpPort": {
+ "name": "smtpPort",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "username": {
+ "name": "username",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "password": {
+ "name": "password",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "fromAddress": {
+ "name": "fromAddress",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "toAddress": {
+ "name": "toAddress",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.gotify": {
+ "name": "gotify",
+ "schema": "",
+ "columns": {
+ "gotifyId": {
+ "name": "gotifyId",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "serverUrl": {
+ "name": "serverUrl",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "appToken": {
+ "name": "appToken",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "priority": {
+ "name": "priority",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 5
+ },
+ "decoration": {
+ "name": "decoration",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.lark": {
+ "name": "lark",
+ "schema": "",
+ "columns": {
+ "larkId": {
+ "name": "larkId",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "webhookUrl": {
+ "name": "webhookUrl",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.mattermost": {
+ "name": "mattermost",
+ "schema": "",
+ "columns": {
+ "mattermostId": {
+ "name": "mattermostId",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "webhookUrl": {
+ "name": "webhookUrl",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "channel": {
+ "name": "channel",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "username": {
+ "name": "username",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.notification": {
+ "name": "notification",
+ "schema": "",
+ "columns": {
+ "notificationId": {
+ "name": "notificationId",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "appDeploy": {
+ "name": "appDeploy",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "appBuildError": {
+ "name": "appBuildError",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "databaseBackup": {
+ "name": "databaseBackup",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "volumeBackup": {
+ "name": "volumeBackup",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "dokployRestart": {
+ "name": "dokployRestart",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "dokployBackup": {
+ "name": "dokployBackup",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "dockerCleanup": {
+ "name": "dockerCleanup",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "serverThreshold": {
+ "name": "serverThreshold",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "superPassword": {
+ "name": "superPassword",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "notificationType": {
+ "name": "notificationType",
+ "type": "notificationType",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "slackId": {
+ "name": "slackId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "telegramId": {
+ "name": "telegramId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "discordId": {
+ "name": "discordId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "emailId": {
+ "name": "emailId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "resendId": {
+ "name": "resendId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "gotifyId": {
+ "name": "gotifyId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "ntfyId": {
+ "name": "ntfyId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "mattermostId": {
+ "name": "mattermostId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "customId": {
+ "name": "customId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "larkId": {
+ "name": "larkId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "pushoverId": {
+ "name": "pushoverId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "teamsId": {
+ "name": "teamsId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "organizationId": {
+ "name": "organizationId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "notification_slackId_slack_slackId_fk": {
+ "name": "notification_slackId_slack_slackId_fk",
+ "tableFrom": "notification",
+ "tableTo": "slack",
+ "columnsFrom": [
+ "slackId"
+ ],
+ "columnsTo": [
+ "slackId"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "notification_telegramId_telegram_telegramId_fk": {
+ "name": "notification_telegramId_telegram_telegramId_fk",
+ "tableFrom": "notification",
+ "tableTo": "telegram",
+ "columnsFrom": [
+ "telegramId"
+ ],
+ "columnsTo": [
+ "telegramId"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "notification_discordId_discord_discordId_fk": {
+ "name": "notification_discordId_discord_discordId_fk",
+ "tableFrom": "notification",
+ "tableTo": "discord",
+ "columnsFrom": [
+ "discordId"
+ ],
+ "columnsTo": [
+ "discordId"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "notification_emailId_email_emailId_fk": {
+ "name": "notification_emailId_email_emailId_fk",
+ "tableFrom": "notification",
+ "tableTo": "email",
+ "columnsFrom": [
+ "emailId"
+ ],
+ "columnsTo": [
+ "emailId"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "notification_resendId_resend_resendId_fk": {
+ "name": "notification_resendId_resend_resendId_fk",
+ "tableFrom": "notification",
+ "tableTo": "resend",
+ "columnsFrom": [
+ "resendId"
+ ],
+ "columnsTo": [
+ "resendId"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "notification_gotifyId_gotify_gotifyId_fk": {
+ "name": "notification_gotifyId_gotify_gotifyId_fk",
+ "tableFrom": "notification",
+ "tableTo": "gotify",
+ "columnsFrom": [
+ "gotifyId"
+ ],
+ "columnsTo": [
+ "gotifyId"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "notification_ntfyId_ntfy_ntfyId_fk": {
+ "name": "notification_ntfyId_ntfy_ntfyId_fk",
+ "tableFrom": "notification",
+ "tableTo": "ntfy",
+ "columnsFrom": [
+ "ntfyId"
+ ],
+ "columnsTo": [
+ "ntfyId"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "notification_mattermostId_mattermost_mattermostId_fk": {
+ "name": "notification_mattermostId_mattermost_mattermostId_fk",
+ "tableFrom": "notification",
+ "tableTo": "mattermost",
+ "columnsFrom": [
+ "mattermostId"
+ ],
+ "columnsTo": [
+ "mattermostId"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "notification_customId_custom_customId_fk": {
+ "name": "notification_customId_custom_customId_fk",
+ "tableFrom": "notification",
+ "tableTo": "custom",
+ "columnsFrom": [
+ "customId"
+ ],
+ "columnsTo": [
+ "customId"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "notification_larkId_lark_larkId_fk": {
+ "name": "notification_larkId_lark_larkId_fk",
+ "tableFrom": "notification",
+ "tableTo": "lark",
+ "columnsFrom": [
+ "larkId"
+ ],
+ "columnsTo": [
+ "larkId"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "notification_pushoverId_pushover_pushoverId_fk": {
+ "name": "notification_pushoverId_pushover_pushoverId_fk",
+ "tableFrom": "notification",
+ "tableTo": "pushover",
+ "columnsFrom": [
+ "pushoverId"
+ ],
+ "columnsTo": [
+ "pushoverId"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "notification_teamsId_teams_teamsId_fk": {
+ "name": "notification_teamsId_teams_teamsId_fk",
+ "tableFrom": "notification",
+ "tableTo": "teams",
+ "columnsFrom": [
+ "teamsId"
+ ],
+ "columnsTo": [
+ "teamsId"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "notification_organizationId_organization_id_fk": {
+ "name": "notification_organizationId_organization_id_fk",
+ "tableFrom": "notification",
+ "tableTo": "organization",
+ "columnsFrom": [
+ "organizationId"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.ntfy": {
+ "name": "ntfy",
+ "schema": "",
+ "columns": {
+ "ntfyId": {
+ "name": "ntfyId",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "serverUrl": {
+ "name": "serverUrl",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "topic": {
+ "name": "topic",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "accessToken": {
+ "name": "accessToken",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "priority": {
+ "name": "priority",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 3
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.pushover": {
+ "name": "pushover",
+ "schema": "",
+ "columns": {
+ "pushoverId": {
+ "name": "pushoverId",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "userKey": {
+ "name": "userKey",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "apiToken": {
+ "name": "apiToken",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "priority": {
+ "name": "priority",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "retry": {
+ "name": "retry",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "expire": {
+ "name": "expire",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.resend": {
+ "name": "resend",
+ "schema": "",
+ "columns": {
+ "resendId": {
+ "name": "resendId",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "apiKey": {
+ "name": "apiKey",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "fromAddress": {
+ "name": "fromAddress",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "toAddress": {
+ "name": "toAddress",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.slack": {
+ "name": "slack",
+ "schema": "",
+ "columns": {
+ "slackId": {
+ "name": "slackId",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "webhookUrl": {
+ "name": "webhookUrl",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "channel": {
+ "name": "channel",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.teams": {
+ "name": "teams",
+ "schema": "",
+ "columns": {
+ "teamsId": {
+ "name": "teamsId",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "webhookUrl": {
+ "name": "webhookUrl",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.telegram": {
+ "name": "telegram",
+ "schema": "",
+ "columns": {
+ "telegramId": {
+ "name": "telegramId",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "botToken": {
+ "name": "botToken",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "chatId": {
+ "name": "chatId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "messageThreadId": {
+ "name": "messageThreadId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.patch": {
+ "name": "patch",
+ "schema": "",
+ "columns": {
+ "patchId": {
+ "name": "patchId",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "type": {
+ "name": "type",
+ "type": "patchType",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'update'"
+ },
+ "filePath": {
+ "name": "filePath",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "enabled": {
+ "name": "enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "content": {
+ "name": "content",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updatedAt": {
+ "name": "updatedAt",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "applicationId": {
+ "name": "applicationId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "composeId": {
+ "name": "composeId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "patch_applicationId_application_applicationId_fk": {
+ "name": "patch_applicationId_application_applicationId_fk",
+ "tableFrom": "patch",
+ "tableTo": "application",
+ "columnsFrom": [
+ "applicationId"
+ ],
+ "columnsTo": [
+ "applicationId"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "patch_composeId_compose_composeId_fk": {
+ "name": "patch_composeId_compose_composeId_fk",
+ "tableFrom": "patch",
+ "tableTo": "compose",
+ "columnsFrom": [
+ "composeId"
+ ],
+ "columnsTo": [
+ "composeId"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "patch_filepath_application_unique": {
+ "name": "patch_filepath_application_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "filePath",
+ "applicationId"
+ ]
+ },
+ "patch_filepath_compose_unique": {
+ "name": "patch_filepath_compose_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "filePath",
+ "composeId"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.port": {
+ "name": "port",
+ "schema": "",
+ "columns": {
+ "portId": {
+ "name": "portId",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "publishedPort": {
+ "name": "publishedPort",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "publishMode": {
+ "name": "publishMode",
+ "type": "publishModeType",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'host'"
+ },
+ "targetPort": {
+ "name": "targetPort",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "protocol": {
+ "name": "protocol",
+ "type": "protocolType",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "applicationId": {
+ "name": "applicationId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "port_applicationId_application_applicationId_fk": {
+ "name": "port_applicationId_application_applicationId_fk",
+ "tableFrom": "port",
+ "tableTo": "application",
+ "columnsFrom": [
+ "applicationId"
+ ],
+ "columnsTo": [
+ "applicationId"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.postgres": {
+ "name": "postgres",
+ "schema": "",
+ "columns": {
+ "postgresId": {
+ "name": "postgresId",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "appName": {
+ "name": "appName",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "databaseName": {
+ "name": "databaseName",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "databaseUser": {
+ "name": "databaseUser",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "databasePassword": {
+ "name": "databasePassword",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "dockerImage": {
+ "name": "dockerImage",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "command": {
+ "name": "command",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "args": {
+ "name": "args",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "env": {
+ "name": "env",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "memoryReservation": {
+ "name": "memoryReservation",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "externalPort": {
+ "name": "externalPort",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "memoryLimit": {
+ "name": "memoryLimit",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "cpuReservation": {
+ "name": "cpuReservation",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "cpuLimit": {
+ "name": "cpuLimit",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "applicationStatus": {
+ "name": "applicationStatus",
+ "type": "applicationStatus",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'idle'"
+ },
+ "healthCheckSwarm": {
+ "name": "healthCheckSwarm",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "restartPolicySwarm": {
+ "name": "restartPolicySwarm",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "placementSwarm": {
+ "name": "placementSwarm",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updateConfigSwarm": {
+ "name": "updateConfigSwarm",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "rollbackConfigSwarm": {
+ "name": "rollbackConfigSwarm",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "modeSwarm": {
+ "name": "modeSwarm",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "labelsSwarm": {
+ "name": "labelsSwarm",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "networkSwarm": {
+ "name": "networkSwarm",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "stopGracePeriodSwarm": {
+ "name": "stopGracePeriodSwarm",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "endpointSpecSwarm": {
+ "name": "endpointSpecSwarm",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "ulimitsSwarm": {
+ "name": "ulimitsSwarm",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "replicas": {
+ "name": "replicas",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 1
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "environmentId": {
+ "name": "environmentId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "serverId": {
+ "name": "serverId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "networkIds": {
+ "name": "networkIds",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'{}'"
+ },
+ "detachDokployNetwork": {
+ "name": "detachDokployNetwork",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "postgres_environmentId_environment_environmentId_fk": {
+ "name": "postgres_environmentId_environment_environmentId_fk",
+ "tableFrom": "postgres",
+ "tableTo": "environment",
+ "columnsFrom": [
+ "environmentId"
+ ],
+ "columnsTo": [
+ "environmentId"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "postgres_serverId_server_serverId_fk": {
+ "name": "postgres_serverId_server_serverId_fk",
+ "tableFrom": "postgres",
+ "tableTo": "server",
+ "columnsFrom": [
+ "serverId"
+ ],
+ "columnsTo": [
+ "serverId"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "postgres_appName_unique": {
+ "name": "postgres_appName_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "appName"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.preview_deployments": {
+ "name": "preview_deployments",
+ "schema": "",
+ "columns": {
+ "previewDeploymentId": {
+ "name": "previewDeploymentId",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "branch": {
+ "name": "branch",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "pullRequestId": {
+ "name": "pullRequestId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "pullRequestNumber": {
+ "name": "pullRequestNumber",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "pullRequestURL": {
+ "name": "pullRequestURL",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "pullRequestTitle": {
+ "name": "pullRequestTitle",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "pullRequestCommentId": {
+ "name": "pullRequestCommentId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "previewStatus": {
+ "name": "previewStatus",
+ "type": "applicationStatus",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'idle'"
+ },
+ "appName": {
+ "name": "appName",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "applicationId": {
+ "name": "applicationId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "domainId": {
+ "name": "domainId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "expiresAt": {
+ "name": "expiresAt",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "preview_deployments_applicationId_application_applicationId_fk": {
+ "name": "preview_deployments_applicationId_application_applicationId_fk",
+ "tableFrom": "preview_deployments",
+ "tableTo": "application",
+ "columnsFrom": [
+ "applicationId"
+ ],
+ "columnsTo": [
+ "applicationId"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "preview_deployments_domainId_domain_domainId_fk": {
+ "name": "preview_deployments_domainId_domain_domainId_fk",
+ "tableFrom": "preview_deployments",
+ "tableTo": "domain",
+ "columnsFrom": [
+ "domainId"
+ ],
+ "columnsTo": [
+ "domainId"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "preview_deployments_appName_unique": {
+ "name": "preview_deployments_appName_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "appName"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.project": {
+ "name": "project",
+ "schema": "",
+ "columns": {
+ "projectId": {
+ "name": "projectId",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "organizationId": {
+ "name": "organizationId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "env": {
+ "name": "env",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "''"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "project_organizationId_organization_id_fk": {
+ "name": "project_organizationId_organization_id_fk",
+ "tableFrom": "project",
+ "tableTo": "organization",
+ "columnsFrom": [
+ "organizationId"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.redirect": {
+ "name": "redirect",
+ "schema": "",
+ "columns": {
+ "redirectId": {
+ "name": "redirectId",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "regex": {
+ "name": "regex",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "replacement": {
+ "name": "replacement",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "permanent": {
+ "name": "permanent",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "uniqueConfigKey": {
+ "name": "uniqueConfigKey",
+ "type": "serial",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "applicationId": {
+ "name": "applicationId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "redirect_applicationId_application_applicationId_fk": {
+ "name": "redirect_applicationId_application_applicationId_fk",
+ "tableFrom": "redirect",
+ "tableTo": "application",
+ "columnsFrom": [
+ "applicationId"
+ ],
+ "columnsTo": [
+ "applicationId"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.redis": {
+ "name": "redis",
+ "schema": "",
+ "columns": {
+ "redisId": {
+ "name": "redisId",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "appName": {
+ "name": "appName",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "password": {
+ "name": "password",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "dockerImage": {
+ "name": "dockerImage",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "command": {
+ "name": "command",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "args": {
+ "name": "args",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "env": {
+ "name": "env",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "memoryReservation": {
+ "name": "memoryReservation",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "memoryLimit": {
+ "name": "memoryLimit",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "cpuReservation": {
+ "name": "cpuReservation",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "cpuLimit": {
+ "name": "cpuLimit",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "externalPort": {
+ "name": "externalPort",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "applicationStatus": {
+ "name": "applicationStatus",
+ "type": "applicationStatus",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'idle'"
+ },
+ "healthCheckSwarm": {
+ "name": "healthCheckSwarm",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "restartPolicySwarm": {
+ "name": "restartPolicySwarm",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "placementSwarm": {
+ "name": "placementSwarm",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updateConfigSwarm": {
+ "name": "updateConfigSwarm",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "rollbackConfigSwarm": {
+ "name": "rollbackConfigSwarm",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "modeSwarm": {
+ "name": "modeSwarm",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "labelsSwarm": {
+ "name": "labelsSwarm",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "networkSwarm": {
+ "name": "networkSwarm",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "stopGracePeriodSwarm": {
+ "name": "stopGracePeriodSwarm",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "endpointSpecSwarm": {
+ "name": "endpointSpecSwarm",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "ulimitsSwarm": {
+ "name": "ulimitsSwarm",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "replicas": {
+ "name": "replicas",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 1
+ },
+ "environmentId": {
+ "name": "environmentId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "serverId": {
+ "name": "serverId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "networkIds": {
+ "name": "networkIds",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'{}'"
+ },
+ "detachDokployNetwork": {
+ "name": "detachDokployNetwork",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "redis_environmentId_environment_environmentId_fk": {
+ "name": "redis_environmentId_environment_environmentId_fk",
+ "tableFrom": "redis",
+ "tableTo": "environment",
+ "columnsFrom": [
+ "environmentId"
+ ],
+ "columnsTo": [
+ "environmentId"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "redis_serverId_server_serverId_fk": {
+ "name": "redis_serverId_server_serverId_fk",
+ "tableFrom": "redis",
+ "tableTo": "server",
+ "columnsFrom": [
+ "serverId"
+ ],
+ "columnsTo": [
+ "serverId"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "redis_appName_unique": {
+ "name": "redis_appName_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "appName"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.registry": {
+ "name": "registry",
+ "schema": "",
+ "columns": {
+ "registryId": {
+ "name": "registryId",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "registryName": {
+ "name": "registryName",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "imagePrefix": {
+ "name": "imagePrefix",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "username": {
+ "name": "username",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "password": {
+ "name": "password",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "registryUrl": {
+ "name": "registryUrl",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "''"
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "selfHosted": {
+ "name": "selfHosted",
+ "type": "RegistryType",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'cloud'"
+ },
+ "organizationId": {
+ "name": "organizationId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "registry_organizationId_organization_id_fk": {
+ "name": "registry_organizationId_organization_id_fk",
+ "tableFrom": "registry",
+ "tableTo": "organization",
+ "columnsFrom": [
+ "organizationId"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.rollback": {
+ "name": "rollback",
+ "schema": "",
+ "columns": {
+ "rollbackId": {
+ "name": "rollbackId",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "deploymentId": {
+ "name": "deploymentId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "version": {
+ "name": "version",
+ "type": "serial",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "image": {
+ "name": "image",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "fullContext": {
+ "name": "fullContext",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "rollback_deploymentId_deployment_deploymentId_fk": {
+ "name": "rollback_deploymentId_deployment_deploymentId_fk",
+ "tableFrom": "rollback",
+ "tableTo": "deployment",
+ "columnsFrom": [
+ "deploymentId"
+ ],
+ "columnsTo": [
+ "deploymentId"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.schedule": {
+ "name": "schedule",
+ "schema": "",
+ "columns": {
+ "scheduleId": {
+ "name": "scheduleId",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "cronExpression": {
+ "name": "cronExpression",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "appName": {
+ "name": "appName",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "serviceName": {
+ "name": "serviceName",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "shellType": {
+ "name": "shellType",
+ "type": "shellType",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'bash'"
+ },
+ "scheduleType": {
+ "name": "scheduleType",
+ "type": "scheduleType",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'application'"
+ },
+ "command": {
+ "name": "command",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "script": {
+ "name": "script",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "applicationId": {
+ "name": "applicationId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "composeId": {
+ "name": "composeId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "serverId": {
+ "name": "serverId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "organizationId": {
+ "name": "organizationId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "enabled": {
+ "name": "enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "timezone": {
+ "name": "timezone",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "schedule_applicationId_application_applicationId_fk": {
+ "name": "schedule_applicationId_application_applicationId_fk",
+ "tableFrom": "schedule",
+ "tableTo": "application",
+ "columnsFrom": [
+ "applicationId"
+ ],
+ "columnsTo": [
+ "applicationId"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "schedule_composeId_compose_composeId_fk": {
+ "name": "schedule_composeId_compose_composeId_fk",
+ "tableFrom": "schedule",
+ "tableTo": "compose",
+ "columnsFrom": [
+ "composeId"
+ ],
+ "columnsTo": [
+ "composeId"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "schedule_serverId_server_serverId_fk": {
+ "name": "schedule_serverId_server_serverId_fk",
+ "tableFrom": "schedule",
+ "tableTo": "server",
+ "columnsFrom": [
+ "serverId"
+ ],
+ "columnsTo": [
+ "serverId"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "schedule_organizationId_organization_id_fk": {
+ "name": "schedule_organizationId_organization_id_fk",
+ "tableFrom": "schedule",
+ "tableTo": "organization",
+ "columnsFrom": [
+ "organizationId"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.scim_connection_binding": {
+ "name": "scim_connection_binding",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "connection_id": {
+ "name": "connection_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "connection_key": {
+ "name": "connection_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "provisioning_domain_id": {
+ "name": "provisioning_domain_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "decommissioned_at": {
+ "name": "decommissioned_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "decommission_status": {
+ "name": "decommission_status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'active'"
+ },
+ "decommission_cursor_user_id": {
+ "name": "decommission_cursor_user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "decommission_reconciled_user_count": {
+ "name": "decommission_reconciled_user_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "decommission_batch_count": {
+ "name": "decommission_batch_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "decommission_revision": {
+ "name": "decommission_revision",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "decommission_completed_at": {
+ "name": "decommission_completed_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "decommission_lease_id": {
+ "name": "decommission_lease_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "decommission_lease_expires_at": {
+ "name": "decommission_lease_expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "scimConnectionBinding_connectionId_idx": {
+ "name": "scimConnectionBinding_connectionId_idx",
+ "columns": [
+ {
+ "expression": "connection_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "scim_connection_binding_connection_key_unique": {
+ "name": "scim_connection_binding_connection_key_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "connection_key"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.scim_group": {
+ "name": "scim_group",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "connection_id": {
+ "name": "connection_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "provisioning_domain_id": {
+ "name": "provisioning_domain_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "revision": {
+ "name": "revision",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "display_name": {
+ "name": "display_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "display_name_key": {
+ "name": "display_name_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "external_id": {
+ "name": "external_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "external_id_key": {
+ "name": "external_id_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "order_key": {
+ "name": "order_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "scimGroup_connectionId_idx": {
+ "name": "scimGroup_connectionId_idx",
+ "columns": [
+ {
+ "expression": "connection_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "scimGroup_provisioningDomainId_idx": {
+ "name": "scimGroup_provisioningDomainId_idx",
+ "columns": [
+ {
+ "expression": "provisioning_domain_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "scim_group_display_name_key_unique": {
+ "name": "scim_group_display_name_key_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "display_name_key"
+ ]
+ },
+ "scim_group_external_id_key_unique": {
+ "name": "scim_group_external_id_key_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "external_id_key"
+ ]
+ },
+ "scim_group_order_key_unique": {
+ "name": "scim_group_order_key_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "order_key"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.scim_group_member": {
+ "name": "scim_group_member",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "connection_id": {
+ "name": "connection_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "group_id": {
+ "name": "group_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "scim_user_id": {
+ "name": "scim_user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "membership_key": {
+ "name": "membership_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "scimGroupMember_connectionId_idx": {
+ "name": "scimGroupMember_connectionId_idx",
+ "columns": [
+ {
+ "expression": "connection_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "scimGroupMember_groupId_idx": {
+ "name": "scimGroupMember_groupId_idx",
+ "columns": [
+ {
+ "expression": "group_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "scimGroupMember_scimUserId_idx": {
+ "name": "scimGroupMember_scimUserId_idx",
+ "columns": [
+ {
+ "expression": "scim_user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "scim_group_member_group_id_scim_group_id_fk": {
+ "name": "scim_group_member_group_id_scim_group_id_fk",
+ "tableFrom": "scim_group_member",
+ "tableTo": "scim_group",
+ "columnsFrom": [
+ "group_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "scim_group_member_scim_user_id_scim_user_id_fk": {
+ "name": "scim_group_member_scim_user_id_scim_user_id_fk",
+ "tableFrom": "scim_group_member",
+ "tableTo": "scim_user",
+ "columnsFrom": [
+ "scim_user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "scim_group_member_membership_key_unique": {
+ "name": "scim_group_member_membership_key_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "membership_key"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.scim_identity_tombstone": {
+ "name": "scim_identity_tombstone",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "connection_id": {
+ "name": "connection_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "provisioning_domain_id": {
+ "name": "provisioning_domain_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "external_id": {
+ "name": "external_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "external_id_key": {
+ "name": "external_id_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "profile": {
+ "name": "profile",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "deleted_at": {
+ "name": "deleted_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "scimIdentityTombstone_connectionId_idx": {
+ "name": "scimIdentityTombstone_connectionId_idx",
+ "columns": [
+ {
+ "expression": "connection_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "scimIdentityTombstone_provisioningDomainId_idx": {
+ "name": "scimIdentityTombstone_provisioningDomainId_idx",
+ "columns": [
+ {
+ "expression": "provisioning_domain_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "scimIdentityTombstone_userId_idx": {
+ "name": "scimIdentityTombstone_userId_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "scim_identity_tombstone_user_id_user_id_fk": {
+ "name": "scim_identity_tombstone_user_id_user_id_fk",
+ "tableFrom": "scim_identity_tombstone",
+ "tableTo": "user",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "scim_identity_tombstone_external_id_key_unique": {
+ "name": "scim_identity_tombstone_external_id_key_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "external_id_key"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.scim_legacy_identity": {
+ "name": "scim_legacy_identity",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "legacy_provider_id": {
+ "name": "legacy_provider_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "external_key": {
+ "name": "external_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "scim_legacy_identity_organization_id_organization_id_fk": {
+ "name": "scim_legacy_identity_organization_id_organization_id_fk",
+ "tableFrom": "scim_legacy_identity",
+ "tableTo": "organization",
+ "columnsFrom": [
+ "organization_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "scim_legacy_identity_user_id_user_id_fk": {
+ "name": "scim_legacy_identity_user_id_user_id_fk",
+ "tableFrom": "scim_legacy_identity",
+ "tableTo": "user",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "scim_legacy_identity_organization_external_key_unique": {
+ "name": "scim_legacy_identity_organization_external_key_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "organization_id",
+ "external_key"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.scim_managed_connection": {
+ "name": "scim_managed_connection",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "creation_request_id": {
+ "name": "creation_request_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "connection_id": {
+ "name": "connection_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "provisioning_domain_id": {
+ "name": "provisioning_domain_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "revision": {
+ "name": "revision",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_by": {
+ "name": "created_by",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "decommission_started_at": {
+ "name": "decommission_started_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "decommission_started_by": {
+ "name": "decommission_started_by",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "decommissioned_at": {
+ "name": "decommissioned_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "decommissioned_by": {
+ "name": "decommissioned_by",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "scimManagedConnection_provisioningDomainId_idx": {
+ "name": "scimManagedConnection_provisioningDomainId_idx",
+ "columns": [
+ {
+ "expression": "provisioning_domain_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "scim_managed_connection_creation_request_id_unique": {
+ "name": "scim_managed_connection_creation_request_id_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "creation_request_id"
+ ]
+ },
+ "scim_managed_connection_connection_id_unique": {
+ "name": "scim_managed_connection_connection_id_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "connection_id"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.scim_managed_connection_event": {
+ "name": "scim_managed_connection_event",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "connection_record_id": {
+ "name": "connection_record_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "event_key": {
+ "name": "event_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "sequence": {
+ "name": "sequence",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "type": {
+ "name": "type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "actor_id": {
+ "name": "actor_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "credential_id": {
+ "name": "credential_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "scimManagedConnectionEvent_connectionRecordId_idx": {
+ "name": "scimManagedConnectionEvent_connectionRecordId_idx",
+ "columns": [
+ {
+ "expression": "connection_record_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "scim_managed_connection_event_connection_record_id_scim_managed_connection_id_fk": {
+ "name": "scim_managed_connection_event_connection_record_id_scim_managed_connection_id_fk",
+ "tableFrom": "scim_managed_connection_event",
+ "tableTo": "scim_managed_connection",
+ "columnsFrom": [
+ "connection_record_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "scim_managed_connection_event_event_key_unique": {
+ "name": "scim_managed_connection_event_event_key_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "event_key"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.scim_managed_credential": {
+ "name": "scim_managed_credential",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "connection_record_id": {
+ "name": "connection_record_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "credential_id": {
+ "name": "credential_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "token_digest": {
+ "name": "token_digest",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "hash_version": {
+ "name": "hash_version",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "active_slot_key": {
+ "name": "active_slot_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "serialized_scopes": {
+ "name": "serialized_scopes",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_by": {
+ "name": "created_by",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "last_used_at": {
+ "name": "last_used_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "revoked_at": {
+ "name": "revoked_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "revoked_by": {
+ "name": "revoked_by",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "decommissioned_at": {
+ "name": "decommissioned_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "scimManagedCredential_connectionRecordId_idx": {
+ "name": "scimManagedCredential_connectionRecordId_idx",
+ "columns": [
+ {
+ "expression": "connection_record_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "scim_managed_credential_connection_record_id_scim_managed_connection_id_fk": {
+ "name": "scim_managed_credential_connection_record_id_scim_managed_connection_id_fk",
+ "tableFrom": "scim_managed_credential",
+ "tableTo": "scim_managed_connection",
+ "columnsFrom": [
+ "connection_record_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "scim_managed_credential_credential_id_unique": {
+ "name": "scim_managed_credential_credential_id_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "credential_id"
+ ]
+ },
+ "scim_managed_credential_active_slot_key_unique": {
+ "name": "scim_managed_credential_active_slot_key_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "active_slot_key"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.scim_projection_grant": {
+ "name": "scim_projection_grant",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "connection_id": {
+ "name": "connection_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "provisioning_domain_id": {
+ "name": "provisioning_domain_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "scim_user_id": {
+ "name": "scim_user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "source_kind": {
+ "name": "source_kind",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "source_id": {
+ "name": "source_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "source_value": {
+ "name": "source_value",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "role": {
+ "name": "role",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "grant_key": {
+ "name": "grant_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "scimProjectionGrant_connectionId_idx": {
+ "name": "scimProjectionGrant_connectionId_idx",
+ "columns": [
+ {
+ "expression": "connection_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "scimProjectionGrant_provisioningDomainId_idx": {
+ "name": "scimProjectionGrant_provisioningDomainId_idx",
+ "columns": [
+ {
+ "expression": "provisioning_domain_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "scimProjectionGrant_scimUserId_idx": {
+ "name": "scimProjectionGrant_scimUserId_idx",
+ "columns": [
+ {
+ "expression": "scim_user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "scimProjectionGrant_userId_idx": {
+ "name": "scimProjectionGrant_userId_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "scim_projection_grant_scim_user_id_scim_user_id_fk": {
+ "name": "scim_projection_grant_scim_user_id_scim_user_id_fk",
+ "tableFrom": "scim_projection_grant",
+ "tableTo": "scim_user",
+ "columnsFrom": [
+ "scim_user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "scim_projection_grant_user_id_user_id_fk": {
+ "name": "scim_projection_grant_user_id_user_id_fk",
+ "tableFrom": "scim_projection_grant",
+ "tableTo": "user",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "scim_projection_grant_grant_key_unique": {
+ "name": "scim_projection_grant_grant_key_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "grant_key"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.scim_subject": {
+ "name": "scim_subject",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "profile_source_id": {
+ "name": "profile_source_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "revision": {
+ "name": "revision",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "scimSubject_profileSourceId_idx": {
+ "name": "scimSubject_profileSourceId_idx",
+ "columns": [
+ {
+ "expression": "profile_source_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "scim_subject_user_id_user_id_fk": {
+ "name": "scim_subject_user_id_user_id_fk",
+ "tableFrom": "scim_subject",
+ "tableTo": "user",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "scim_subject_user_id_unique": {
+ "name": "scim_subject_user_id_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "user_id"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.scim_user": {
+ "name": "scim_user",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "connection_id": {
+ "name": "connection_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "provisioning_domain_id": {
+ "name": "provisioning_domain_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "connection_user_key": {
+ "name": "connection_user_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_name": {
+ "name": "user_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_name_key": {
+ "name": "user_name_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "primary_email": {
+ "name": "primary_email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "work_email_value_index": {
+ "name": "work_email_value_index",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "email_value_index": {
+ "name": "email_value_index",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "display_name": {
+ "name": "display_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "formatted_name": {
+ "name": "formatted_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "given_name": {
+ "name": "given_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "family_name": {
+ "name": "family_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "serialized_emails": {
+ "name": "serialized_emails",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "serialized_attributes": {
+ "name": "serialized_attributes",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "external_id": {
+ "name": "external_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "external_id_key": {
+ "name": "external_id_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "active": {
+ "name": "active",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "order_key": {
+ "name": "order_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "scimUser_connectionId_idx": {
+ "name": "scimUser_connectionId_idx",
+ "columns": [
+ {
+ "expression": "connection_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "scimUser_provisioningDomainId_idx": {
+ "name": "scimUser_provisioningDomainId_idx",
+ "columns": [
+ {
+ "expression": "provisioning_domain_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "scimUser_userId_idx": {
+ "name": "scimUser_userId_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "scim_user_user_id_user_id_fk": {
+ "name": "scim_user_user_id_user_id_fk",
+ "tableFrom": "scim_user",
+ "tableTo": "user",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "scim_user_connection_user_key_unique": {
+ "name": "scim_user_connection_user_key_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "connection_user_key"
+ ]
+ },
+ "scim_user_user_name_key_unique": {
+ "name": "scim_user_user_name_key_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "user_name_key"
+ ]
+ },
+ "scim_user_external_id_key_unique": {
+ "name": "scim_user_external_id_key_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "external_id_key"
+ ]
+ },
+ "scim_user_order_key_unique": {
+ "name": "scim_user_order_key_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "order_key"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.security": {
+ "name": "security",
+ "schema": "",
+ "columns": {
+ "securityId": {
+ "name": "securityId",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "username": {
+ "name": "username",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "password": {
+ "name": "password",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "applicationId": {
+ "name": "applicationId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "security_applicationId_application_applicationId_fk": {
+ "name": "security_applicationId_application_applicationId_fk",
+ "tableFrom": "security",
+ "tableTo": "application",
+ "columnsFrom": [
+ "applicationId"
+ ],
+ "columnsTo": [
+ "applicationId"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "security_username_applicationId_unique": {
+ "name": "security_username_applicationId_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "username",
+ "applicationId"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.server": {
+ "name": "server",
+ "schema": "",
+ "columns": {
+ "serverId": {
+ "name": "serverId",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "ipAddress": {
+ "name": "ipAddress",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "port": {
+ "name": "port",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "username": {
+ "name": "username",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'root'"
+ },
+ "appName": {
+ "name": "appName",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "enableDockerCleanup": {
+ "name": "enableDockerCleanup",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "buildsConcurrency": {
+ "name": "buildsConcurrency",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 1
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "organizationId": {
+ "name": "organizationId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "serverStatus": {
+ "name": "serverStatus",
+ "type": "serverStatus",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'active'"
+ },
+ "serverType": {
+ "name": "serverType",
+ "type": "serverType",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'deploy'"
+ },
+ "command": {
+ "name": "command",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "''"
+ },
+ "sshKeyId": {
+ "name": "sshKeyId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "metricsConfig": {
+ "name": "metricsConfig",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{\"server\":{\"type\":\"Remote\",\"refreshRate\":60,\"port\":4500,\"token\":\"\",\"urlCallback\":\"\",\"cronJob\":\"\",\"retentionDays\":2,\"thresholds\":{\"cpu\":0,\"memory\":0}},\"containers\":{\"refreshRate\":60,\"services\":{\"include\":[],\"exclude\":[]}}}'::jsonb"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "server_organizationId_organization_id_fk": {
+ "name": "server_organizationId_organization_id_fk",
+ "tableFrom": "server",
+ "tableTo": "organization",
+ "columnsFrom": [
+ "organizationId"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "server_sshKeyId_ssh-key_sshKeyId_fk": {
+ "name": "server_sshKeyId_ssh-key_sshKeyId_fk",
+ "tableFrom": "server",
+ "tableTo": "ssh-key",
+ "columnsFrom": [
+ "sshKeyId"
+ ],
+ "columnsTo": [
+ "sshKeyId"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.session": {
+ "name": "session",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "token": {
+ "name": "token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "ip_address": {
+ "name": "ip_address",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "user_agent": {
+ "name": "user_agent",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "impersonated_by": {
+ "name": "impersonated_by",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "active_organization_id": {
+ "name": "active_organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "session_user_id_user_id_fk": {
+ "name": "session_user_id_user_id_fk",
+ "tableFrom": "session",
+ "tableTo": "user",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "session_token_unique": {
+ "name": "session_token_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "token"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.ssh-key": {
+ "name": "ssh-key",
+ "schema": "",
+ "columns": {
+ "sshKeyId": {
+ "name": "sshKeyId",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "privateKey": {
+ "name": "privateKey",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "''"
+ },
+ "publicKey": {
+ "name": "publicKey",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "lastUsedAt": {
+ "name": "lastUsedAt",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "organizationId": {
+ "name": "organizationId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "ssh-key_organizationId_organization_id_fk": {
+ "name": "ssh-key_organizationId_organization_id_fk",
+ "tableFrom": "ssh-key",
+ "tableTo": "organization",
+ "columnsFrom": [
+ "organizationId"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.sso_provider": {
+ "name": "sso_provider",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "issuer": {
+ "name": "issuer",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "oidc_config": {
+ "name": "oidc_config",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "saml_config": {
+ "name": "saml_config",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "provider_id": {
+ "name": "provider_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "domain": {
+ "name": "domain",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "domainVerified": {
+ "name": "domainVerified",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false,
+ "default": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "sso_provider_user_id_user_id_fk": {
+ "name": "sso_provider_user_id_user_id_fk",
+ "tableFrom": "sso_provider",
+ "tableTo": "user",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "sso_provider_organization_id_organization_id_fk": {
+ "name": "sso_provider_organization_id_organization_id_fk",
+ "tableFrom": "sso_provider",
+ "tableTo": "organization",
+ "columnsFrom": [
+ "organization_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "sso_provider_provider_id_unique": {
+ "name": "sso_provider_provider_id_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "provider_id"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.super_password": {
+ "name": "super_password",
+ "schema": "",
+ "columns": {
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "password_hash": {
+ "name": "password_hash",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "hint": {
+ "name": "hint",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "failed_attempts": {
+ "name": "failed_attempts",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "locked_until": {
+ "name": "locked_until",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "reset_request_count": {
+ "name": "reset_request_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "reset_window_started_at": {
+ "name": "reset_window_started_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "super_password_user_id_user_id_fk": {
+ "name": "super_password_user_id_user_id_fk",
+ "tableFrom": "super_password",
+ "tableTo": "user",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.super_password_token": {
+ "name": "super_password_token",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "type": {
+ "name": "type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "token_hash": {
+ "name": "token_hash",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "used_at": {
+ "name": "used_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "super_password_token_user_id_idx": {
+ "name": "super_password_token_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "super_password_token_user_id_user_id_fk": {
+ "name": "super_password_token_user_id_user_id_fk",
+ "tableFrom": "super_password_token",
+ "tableTo": "user",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "super_password_token_token_hash_unique": {
+ "name": "super_password_token_token_hash_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "token_hash"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.super_session": {
+ "name": "super_session",
+ "schema": "",
+ "columns": {
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "opened_at": {
+ "name": "opened_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "opened_by_session_id": {
+ "name": "opened_by_session_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_extended_at": {
+ "name": "last_extended_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "super_session_user_id_user_id_fk": {
+ "name": "super_session_user_id_user_id_fk",
+ "tableFrom": "super_session",
+ "tableTo": "user",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.project_tag": {
+ "name": "project_tag",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "projectId": {
+ "name": "projectId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "tagId": {
+ "name": "tagId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "project_tag_projectId_project_projectId_fk": {
+ "name": "project_tag_projectId_project_projectId_fk",
+ "tableFrom": "project_tag",
+ "tableTo": "project",
+ "columnsFrom": [
+ "projectId"
+ ],
+ "columnsTo": [
+ "projectId"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "project_tag_tagId_tag_tagId_fk": {
+ "name": "project_tag_tagId_tag_tagId_fk",
+ "tableFrom": "project_tag",
+ "tableTo": "tag",
+ "columnsFrom": [
+ "tagId"
+ ],
+ "columnsTo": [
+ "tagId"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "unique_project_tag": {
+ "name": "unique_project_tag",
+ "nullsNotDistinct": false,
+ "columns": [
+ "projectId",
+ "tagId"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.tag": {
+ "name": "tag",
+ "schema": "",
+ "columns": {
+ "tagId": {
+ "name": "tagId",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "color": {
+ "name": "color",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "organizationId": {
+ "name": "organizationId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "tag_organizationId_organization_id_fk": {
+ "name": "tag_organizationId_organization_id_fk",
+ "tableFrom": "tag",
+ "tableTo": "organization",
+ "columnsFrom": [
+ "organizationId"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "unique_org_tag_name": {
+ "name": "unique_org_tag_name",
+ "nullsNotDistinct": false,
+ "columns": [
+ "organizationId",
+ "name"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.user": {
+ "name": "user",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "firstName": {
+ "name": "firstName",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "''"
+ },
+ "lastName": {
+ "name": "lastName",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "''"
+ },
+ "isRegistered": {
+ "name": "isRegistered",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "expirationDate": {
+ "name": "expirationDate",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "now()"
+ },
+ "two_factor_enabled": {
+ "name": "two_factor_enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "email": {
+ "name": "email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "email_verified": {
+ "name": "email_verified",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "image": {
+ "name": "image",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "banned": {
+ "name": "banned",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "ban_reason": {
+ "name": "ban_reason",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "ban_expires": {
+ "name": "ban_expires",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "role": {
+ "name": "role",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'user'"
+ },
+ "enablePaidFeatures": {
+ "name": "enablePaidFeatures",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "allowImpersonation": {
+ "name": "allowImpersonation",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "enableEnterpriseFeatures": {
+ "name": "enableEnterpriseFeatures",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "licenseKey": {
+ "name": "licenseKey",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "isValidEnterpriseLicense": {
+ "name": "isValidEnterpriseLicense",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "stripeCustomerId": {
+ "name": "stripeCustomerId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "stripeSubscriptionId": {
+ "name": "stripeSubscriptionId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "serversQuantity": {
+ "name": "serversQuantity",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "sendInvoiceNotifications": {
+ "name": "sendInvoiceNotifications",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "isEnterpriseCloud": {
+ "name": "isEnterpriseCloud",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "trustedOrigins": {
+ "name": "trustedOrigins",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "bookmarkedTemplates": {
+ "name": "bookmarkedTemplates",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "ARRAY[]::text[]"
+ },
+ "onboardingCompletedAt": {
+ "name": "onboardingCompletedAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "user_email_unique": {
+ "name": "user_email_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "email"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.vault_provider": {
+ "name": "vault_provider",
+ "schema": "",
+ "columns": {
+ "vaultProviderId": {
+ "name": "vaultProviderId",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "providerType": {
+ "name": "providerType",
+ "type": "VaultProviderType",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "config": {
+ "name": "config",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "assignments": {
+ "name": "assignments",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'::jsonb"
+ },
+ "organizationId": {
+ "name": "organizationId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "vault_provider_org_name_idx": {
+ "name": "vault_provider_org_name_idx",
+ "columns": [
+ {
+ "expression": "organizationId",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "vault_provider_organizationId_organization_id_fk": {
+ "name": "vault_provider_organizationId_organization_id_fk",
+ "tableFrom": "vault_provider",
+ "tableTo": "organization",
+ "columnsFrom": [
+ "organizationId"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.volume_backup": {
+ "name": "volume_backup",
+ "schema": "",
+ "columns": {
+ "volumeBackupId": {
+ "name": "volumeBackupId",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "volumeName": {
+ "name": "volumeName",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "prefix": {
+ "name": "prefix",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "serviceType": {
+ "name": "serviceType",
+ "type": "serviceType",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'application'"
+ },
+ "appName": {
+ "name": "appName",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "serviceName": {
+ "name": "serviceName",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "turnOff": {
+ "name": "turnOff",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "cronExpression": {
+ "name": "cronExpression",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "keepLatestCount": {
+ "name": "keepLatestCount",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "enabled": {
+ "name": "enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "applicationId": {
+ "name": "applicationId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "postgresId": {
+ "name": "postgresId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "mariadbId": {
+ "name": "mariadbId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "mongoId": {
+ "name": "mongoId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "mysqlId": {
+ "name": "mysqlId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "redisId": {
+ "name": "redisId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "libsqlId": {
+ "name": "libsqlId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "composeId": {
+ "name": "composeId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "destinationId": {
+ "name": "destinationId",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "volume_backup_applicationId_application_applicationId_fk": {
+ "name": "volume_backup_applicationId_application_applicationId_fk",
+ "tableFrom": "volume_backup",
+ "tableTo": "application",
+ "columnsFrom": [
+ "applicationId"
+ ],
+ "columnsTo": [
+ "applicationId"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "volume_backup_postgresId_postgres_postgresId_fk": {
+ "name": "volume_backup_postgresId_postgres_postgresId_fk",
+ "tableFrom": "volume_backup",
+ "tableTo": "postgres",
+ "columnsFrom": [
+ "postgresId"
+ ],
+ "columnsTo": [
+ "postgresId"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "volume_backup_mariadbId_mariadb_mariadbId_fk": {
+ "name": "volume_backup_mariadbId_mariadb_mariadbId_fk",
+ "tableFrom": "volume_backup",
+ "tableTo": "mariadb",
+ "columnsFrom": [
+ "mariadbId"
+ ],
+ "columnsTo": [
+ "mariadbId"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "volume_backup_mongoId_mongo_mongoId_fk": {
+ "name": "volume_backup_mongoId_mongo_mongoId_fk",
+ "tableFrom": "volume_backup",
+ "tableTo": "mongo",
+ "columnsFrom": [
+ "mongoId"
+ ],
+ "columnsTo": [
+ "mongoId"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "volume_backup_mysqlId_mysql_mysqlId_fk": {
+ "name": "volume_backup_mysqlId_mysql_mysqlId_fk",
+ "tableFrom": "volume_backup",
+ "tableTo": "mysql",
+ "columnsFrom": [
+ "mysqlId"
+ ],
+ "columnsTo": [
+ "mysqlId"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "volume_backup_redisId_redis_redisId_fk": {
+ "name": "volume_backup_redisId_redis_redisId_fk",
+ "tableFrom": "volume_backup",
+ "tableTo": "redis",
+ "columnsFrom": [
+ "redisId"
+ ],
+ "columnsTo": [
+ "redisId"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "volume_backup_libsqlId_libsql_libsqlId_fk": {
+ "name": "volume_backup_libsqlId_libsql_libsqlId_fk",
+ "tableFrom": "volume_backup",
+ "tableTo": "libsql",
+ "columnsFrom": [
+ "libsqlId"
+ ],
+ "columnsTo": [
+ "libsqlId"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "volume_backup_composeId_compose_composeId_fk": {
+ "name": "volume_backup_composeId_compose_composeId_fk",
+ "tableFrom": "volume_backup",
+ "tableTo": "compose",
+ "columnsFrom": [
+ "composeId"
+ ],
+ "columnsTo": [
+ "composeId"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "volume_backup_destinationId_destination_destinationId_fk": {
+ "name": "volume_backup_destinationId_destination_destinationId_fk",
+ "tableFrom": "volume_backup",
+ "tableTo": "destination",
+ "columnsFrom": [
+ "destinationId"
+ ],
+ "columnsTo": [
+ "destinationId"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.webServerSettings": {
+ "name": "webServerSettings",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "serverIp": {
+ "name": "serverIp",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "certificateType": {
+ "name": "certificateType",
+ "type": "certificateType",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'none'"
+ },
+ "https": {
+ "name": "https",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "host": {
+ "name": "host",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "letsEncryptEmail": {
+ "name": "letsEncryptEmail",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "sshPrivateKey": {
+ "name": "sshPrivateKey",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "enableDockerCleanup": {
+ "name": "enableDockerCleanup",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "logCleanupCron": {
+ "name": "logCleanupCron",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'0 0 * * *'"
+ },
+ "metricsConfig": {
+ "name": "metricsConfig",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{\"server\":{\"type\":\"Dokploy\",\"refreshRate\":60,\"port\":4500,\"token\":\"\",\"retentionDays\":2,\"cronJob\":\"\",\"urlCallback\":\"\",\"thresholds\":{\"cpu\":0,\"memory\":0}},\"containers\":{\"refreshRate\":60,\"services\":{\"include\":[],\"exclude\":[]}}}'::jsonb"
+ },
+ "whitelabelingConfig": {
+ "name": "whitelabelingConfig",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'{\"appName\":null,\"appDescription\":null,\"logoUrl\":null,\"faviconUrl\":null,\"customCss\":null,\"loginLogoUrl\":null,\"supportUrl\":null,\"docsUrl\":null,\"errorPageTitle\":null,\"errorPageDescription\":null,\"footerText\":null,\"ogImageUrl\":null}'::jsonb"
+ },
+ "remoteServersOnly": {
+ "name": "remoteServersOnly",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "buildsConcurrency": {
+ "name": "buildsConcurrency",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 1
+ },
+ "enforceSSO": {
+ "name": "enforceSSO",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "cleanupCacheApplications": {
+ "name": "cleanupCacheApplications",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "cleanupCacheOnPreviews": {
+ "name": "cleanupCacheOnPreviews",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "cleanupCacheOnCompose": {
+ "name": "cleanupCacheOnCompose",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ }
+ },
+ "enums": {
+ "public.buildType": {
+ "name": "buildType",
+ "schema": "public",
+ "values": [
+ "dockerfile",
+ "heroku_buildpacks",
+ "paketo_buildpacks",
+ "nixpacks",
+ "static",
+ "railpack"
+ ]
+ },
+ "public.sourceType": {
+ "name": "sourceType",
+ "schema": "public",
+ "values": [
+ "docker",
+ "git",
+ "github",
+ "gitlab",
+ "bitbucket",
+ "gitea",
+ "drop"
+ ]
+ },
+ "public.backupType": {
+ "name": "backupType",
+ "schema": "public",
+ "values": [
+ "database",
+ "compose"
+ ]
+ },
+ "public.databaseType": {
+ "name": "databaseType",
+ "schema": "public",
+ "values": [
+ "postgres",
+ "mariadb",
+ "mysql",
+ "mongo",
+ "web-server",
+ "libsql"
+ ]
+ },
+ "public.composeType": {
+ "name": "composeType",
+ "schema": "public",
+ "values": [
+ "docker-compose",
+ "stack"
+ ]
+ },
+ "public.sourceTypeCompose": {
+ "name": "sourceTypeCompose",
+ "schema": "public",
+ "values": [
+ "git",
+ "github",
+ "gitlab",
+ "bitbucket",
+ "gitea",
+ "raw"
+ ]
+ },
+ "public.deploymentOperationStatus": {
+ "name": "deploymentOperationStatus",
+ "schema": "public",
+ "values": [
+ "accepted",
+ "queued",
+ "dispatch_unknown",
+ "running",
+ "succeeded",
+ "failed"
+ ]
+ },
+ "public.deploymentStatus": {
+ "name": "deploymentStatus",
+ "schema": "public",
+ "values": [
+ "running",
+ "done",
+ "error",
+ "cancelled"
+ ]
+ },
+ "public.DnsProviderType": {
+ "name": "DnsProviderType",
+ "schema": "public",
+ "values": [
+ "cloudflare",
+ "route53",
+ "porkbun",
+ "infomaniak",
+ "ovh"
+ ]
+ },
+ "public.domainType": {
+ "name": "domainType",
+ "schema": "public",
+ "values": [
+ "compose",
+ "application",
+ "preview"
+ ]
+ },
+ "public.gitProviderType": {
+ "name": "gitProviderType",
+ "schema": "public",
+ "values": [
+ "github",
+ "gitlab",
+ "bitbucket",
+ "gitea"
+ ]
+ },
+ "public.mountType": {
+ "name": "mountType",
+ "schema": "public",
+ "values": [
+ "bind",
+ "volume",
+ "file"
+ ]
+ },
+ "public.serviceType": {
+ "name": "serviceType",
+ "schema": "public",
+ "values": [
+ "application",
+ "postgres",
+ "mysql",
+ "mariadb",
+ "mongo",
+ "redis",
+ "compose",
+ "libsql"
+ ]
+ },
+ "public.networkDriver": {
+ "name": "networkDriver",
+ "schema": "public",
+ "values": [
+ "bridge",
+ "overlay"
+ ]
+ },
+ "public.notificationType": {
+ "name": "notificationType",
+ "schema": "public",
+ "values": [
+ "slack",
+ "telegram",
+ "discord",
+ "email",
+ "resend",
+ "gotify",
+ "ntfy",
+ "mattermost",
+ "pushover",
+ "custom",
+ "lark",
+ "teams"
+ ]
+ },
+ "public.patchType": {
+ "name": "patchType",
+ "schema": "public",
+ "values": [
+ "create",
+ "update",
+ "delete"
+ ]
+ },
+ "public.protocolType": {
+ "name": "protocolType",
+ "schema": "public",
+ "values": [
+ "tcp",
+ "udp"
+ ]
+ },
+ "public.publishModeType": {
+ "name": "publishModeType",
+ "schema": "public",
+ "values": [
+ "ingress",
+ "host"
+ ]
+ },
+ "public.RegistryType": {
+ "name": "RegistryType",
+ "schema": "public",
+ "values": [
+ "selfHosted",
+ "cloud"
+ ]
+ },
+ "public.scheduleType": {
+ "name": "scheduleType",
+ "schema": "public",
+ "values": [
+ "application",
+ "compose",
+ "server",
+ "dokploy-server"
+ ]
+ },
+ "public.shellType": {
+ "name": "shellType",
+ "schema": "public",
+ "values": [
+ "bash",
+ "sh"
+ ]
+ },
+ "public.serverStatus": {
+ "name": "serverStatus",
+ "schema": "public",
+ "values": [
+ "active",
+ "inactive"
+ ]
+ },
+ "public.serverType": {
+ "name": "serverType",
+ "schema": "public",
+ "values": [
+ "deploy",
+ "build"
+ ]
+ },
+ "public.applicationStatus": {
+ "name": "applicationStatus",
+ "schema": "public",
+ "values": [
+ "idle",
+ "running",
+ "done",
+ "error"
+ ]
+ },
+ "public.certificateType": {
+ "name": "certificateType",
+ "schema": "public",
+ "values": [
+ "letsencrypt",
+ "none",
+ "custom"
+ ]
+ },
+ "public.sqldNode": {
+ "name": "sqldNode",
+ "schema": "public",
+ "values": [
+ "primary",
+ "replica"
+ ]
+ },
+ "public.triggerType": {
+ "name": "triggerType",
+ "schema": "public",
+ "values": [
+ "push",
+ "tag"
+ ]
+ },
+ "public.VaultProviderType": {
+ "name": "VaultProviderType",
+ "schema": "public",
+ "values": [
+ "hashicorp",
+ "infisical",
+ "aws",
+ "aws-parameter-store",
+ "doppler",
+ "azure",
+ "scaleway",
+ "phase"
+ ]
+ }
+ },
+ "schemas": {},
+ "sequences": {},
+ "roles": {},
+ "policies": {},
+ "views": {},
+ "_meta": {
+ "columns": {},
+ "schemas": {},
+ "tables": {}
+ }
+}
\ No newline at end of file
diff --git a/apps/dokploy/drizzle/meta/_journal.json b/apps/dokploy/drizzle/meta/_journal.json
index 7cc0c143f53..b5bfb8c7200 100644
--- a/apps/dokploy/drizzle/meta/_journal.json
+++ b/apps/dokploy/drizzle/meta/_journal.json
@@ -1450,6 +1450,13 @@
"when": 1791365213520,
"tag": "0206_apikey_secret",
"breakpoints": true
+ },
+ {
+ "idx": 207,
+ "version": "7",
+ "when": 1791461560988,
+ "tag": "0207_railpack_default_0_40_1",
+ "breakpoints": true
}
]
}
\ No newline at end of file
diff --git a/docs/server-components-update.md b/docs/server-components-update.md
index 9d78ef54c7c..6e1e6c2d2b7 100644
--- a/docs/server-components-update.md
+++ b/docs/server-components-update.md
@@ -50,6 +50,6 @@ Postgres и Redis относятся к базе панели. На сервер
## Ограничения
- Railpack 0.15.4 → 0.40.1 и pack 0.39.1 → 0.40.9 — большие скачки. Перед широким выкатом проверьте сборку реального приложения.
-- Версия Railpack у каждого приложения хранится в базе (колонка `railpackVersion`, по умолчанию `0.15.4`). Новое значение по умолчанию в форме (0.40.1) применяется к приложениям, которые сохраняют настройки сборки. Изменить значение по умолчанию в базе можно только миграцией, её пока нет.
+- Версия Railpack у каждого приложения хранится в базе (колонка `railpackVersion`). Новое значение по умолчанию — 0.40.1 (миграция `0207_railpack_default_0_40_1`). Существующие приложения сохраняют свою версию, миграция её не меняет.
- Версии Dokploy (`DOKPLOY_OFFICIAL_VERSION`, сейчас v0.29.8) и npm-зависимости в этот список не входят.
- Обновление Traefik, Docker и установка в контейнере проверены только тестами и в контейнере Ubuntu. Полный прогон с приложениями на живом сервере не выполнялся.
diff --git a/packages/server/src/db/schema/application.ts b/packages/server/src/db/schema/application.ts
index 5659fa6f8ea..4708128c355 100644
--- a/packages/server/src/db/schema/application.ts
+++ b/packages/server/src/db/schema/application.ts
@@ -238,7 +238,7 @@ export const applications = pgTable("application", {
.notNull()
.default("idle"),
buildType: buildType("buildType").notNull().default("nixpacks"),
- railpackVersion: text("railpackVersion").default("0.15.4"),
+ railpackVersion: text("railpackVersion").default("0.40.1"),
herokuVersion: text("herokuVersion").default("24"),
publishDirectory: text("publishDirectory"),
isStaticSpa: boolean("isStaticSpa"),
From f274ba547ac5ada6fcbb337de9c1c4b7bb2f0da0 Mon Sep 17 00:00:00 2001
From: merge-check <140916359+agentHits@users.noreply.github.com>
Date: Thu, 8 Oct 2026 08:30:09 -0400
Subject: [PATCH 04/33] docs: update the hardening runbook to the current
Traefik pin
The runbook still named Traefik 3.7.5 and a test file that was replaced
by component-versions-sync.test.ts. Its update example also set
TRAEFIK_VERSION=3.7.5 on the panel, which would make new servers install
the old Traefik instead of the pinned 3.7.14. The example now only adds
DOKPLOY_SERVER_HARDENING; the installer sets TRAEFIK_VERSION from
TRAEFIK_IMAGE on its own.
Co-Authored-By: Claude Haiku 5.5
---
docs/server-hardening.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/docs/server-hardening.md b/docs/server-hardening.md
index 5c6b9e74cc3..372628d1883 100644
--- a/docs/server-hardening.md
+++ b/docs/server-hardening.md
@@ -12,7 +12,7 @@
| `--harden` | все три | Все три флага сразу. |
| — | `DOKPLOY_SERVER_HARDENING=ssh,ufw,fail2ban` | То же самое панель применяет к новым серверам в «Setup Server». Если задан флаг установки, переменная выставляется автоматически. |
-Версия Traefik для серверов берётся из установщика: `TRAEFIK_IMAGE`, сейчас `traefik:v3.7.5`. Тест `apps/dokploy/__test__/server/traefik-version.test.ts` падает, если версия по умолчанию в панели расходится с установщиком.
+Версия Traefik для серверов берётся из установщика: `TRAEFIK_IMAGE`, сейчас `traefik:v3.7.14`. Тест `apps/dokploy/__test__/server/component-versions-sync.test.ts` падает, если версия по умолчанию в панели расходится с установщиком.
## Как запускать
@@ -50,7 +50,7 @@ bash install-agenthits.sh harden --harden
Включить усиление для новых серверов, которые настраиваются через панель:
```bash
-orb -m dokploy -u root docker service update --env-add DOKPLOY_SERVER_HARDENING=ssh,ufw,fail2ban --env-add TRAEFIK_VERSION=3.7.5 dokploy
+orb -m dokploy -u root docker service update --env-add DOKPLOY_SERVER_HARDENING=ssh,ufw,fail2ban dokploy
```
Чтобы панель использовала новый код, в образ сервиса должна попасть сборка с этими изменениями (после пуша и сборки образа).
From 5afd651feecc34f00afd33b010c02dd09ad8fbe6 Mon Sep 17 00:00:00 2001
From: merge-check <140916359+agentHits@users.noreply.github.com>
Date: Thu, 8 Oct 2026 08:37:18 -0400
Subject: [PATCH 05/33] test: give the subprocess settings tests a 30s timeout
Two tests in the settings suite spawn bash or sh against fake docker and
curl scripts. They pass locally in about 3s, but on the CI runner, which
also runs real Docker builds in parallel, they exceeded vitest's 5s
default (pr-check run 37777323548). Raise the timeout only for the tests
that spawn processes. Most of the diff is biome reindenting the it()
calls whose timeout argument moved to its own line.
Co-Authored-By: Claude Haiku 5.5
---
.../settings/agenthits-update-script.test.ts | 255 ++++++++++--------
.../settings/dokploy-image-cleanup.test.ts | 3 +
2 files changed, 144 insertions(+), 114 deletions(-)
diff --git a/apps/dokploy/__test__/settings/agenthits-update-script.test.ts b/apps/dokploy/__test__/settings/agenthits-update-script.test.ts
index 22551cf813b..9af6e790386 100644
--- a/apps/dokploy/__test__/settings/agenthits-update-script.test.ts
+++ b/apps/dokploy/__test__/settings/agenthits-update-script.test.ts
@@ -19,6 +19,9 @@ const repoRoot = path.resolve(
const updateScript = path.join(repoRoot, "update.sh");
const installerScript = path.join(repoRoot, "install-agenthits.sh");
+// Each test spawns bash; the 5s default times out on a CI runner under load.
+const SPAWN_TEST_TIMEOUT_MS = 30_000;
+
const writeFakeDocker = (
dir: string,
options: {
@@ -156,131 +159,155 @@ const runUpdateScript = (
};
describe("AgentHits update script", () => {
- it("skips pull and service update when the installed digest and metadata already match", () => {
- const { result, calls } = runUpdateScript({
- currentDigest: "sha256:index",
- latestIndexDigest: "sha256:index",
- latestPlatformDigest: "sha256:platform",
- serviceEnv: [
- "RELEASE_TAG=agenthits-dev",
- "DOKPLOY_OFFICIAL_VERSION=v0.29.8",
- "DOKPLOY_FORK_VERSION=off_v0.29.8/Fork_160+latest",
- ],
- });
+ it(
+ "skips pull and service update when the installed digest and metadata already match",
+ () => {
+ const { result, calls } = runUpdateScript({
+ currentDigest: "sha256:index",
+ latestIndexDigest: "sha256:index",
+ latestPlatformDigest: "sha256:platform",
+ serviceEnv: [
+ "RELEASE_TAG=agenthits-dev",
+ "DOKPLOY_OFFICIAL_VERSION=v0.29.8",
+ "DOKPLOY_FORK_VERSION=off_v0.29.8/Fork_160+latest",
+ ],
+ });
- expect(result.status).toBe(0);
- expect(result.stdout).toContain("already up to date");
- expect(calls).toContain("service inspect dokploy");
- expect(calls).toContain("buildx imagetools inspect");
- expect(calls).not.toContain("pull ");
- expect(calls).not.toContain("service update");
- });
+ expect(result.status).toBe(0);
+ expect(result.stdout).toContain("already up to date");
+ expect(calls).toContain("service inspect dokploy");
+ expect(calls).toContain("buildx imagetools inspect");
+ expect(calls).not.toContain("pull ");
+ expect(calls).not.toContain("service update");
+ },
+ SPAWN_TEST_TIMEOUT_MS,
+ );
- it("skips service update when the installed service uses the current tag without a pinned digest", () => {
- const { result, calls } = runUpdateScript({
- currentImage: "ghcr.io/agenthits/dokploy:agenthits-dev",
- latestIndexDigest: "sha256:index",
- latestPlatformDigest: "sha256:platform",
- serviceEnv: [
- "RELEASE_TAG=agenthits-dev",
- "DOKPLOY_OFFICIAL_VERSION=v0.29.8",
- "DOKPLOY_FORK_VERSION=off_v0.29.8/Fork_160+latest",
- ],
- });
+ it(
+ "skips service update when the installed service uses the current tag without a pinned digest",
+ () => {
+ const { result, calls } = runUpdateScript({
+ currentImage: "ghcr.io/agenthits/dokploy:agenthits-dev",
+ latestIndexDigest: "sha256:index",
+ latestPlatformDigest: "sha256:platform",
+ serviceEnv: [
+ "RELEASE_TAG=agenthits-dev",
+ "DOKPLOY_OFFICIAL_VERSION=v0.29.8",
+ "DOKPLOY_FORK_VERSION=off_v0.29.8/Fork_160+latest",
+ ],
+ });
- expect(result.status).toBe(0);
- expect(result.stdout).toContain("metadata matches latest image");
- expect(calls).toContain("service inspect dokploy");
- expect(calls).toContain("buildx imagetools inspect");
- expect(calls).not.toContain("pull ");
- expect(calls).not.toContain("service update");
- });
+ expect(result.status).toBe(0);
+ expect(result.stdout).toContain("metadata matches latest image");
+ expect(calls).toContain("service inspect dokploy");
+ expect(calls).toContain("buildx imagetools inspect");
+ expect(calls).not.toContain("pull ");
+ expect(calls).not.toContain("service update");
+ },
+ SPAWN_TEST_TIMEOUT_MS,
+ );
- it("does not trust a mutable image tag as up to date when remote metadata cannot be loaded", () => {
- const { result, calls } = runUpdateScript({
- currentImage: "ghcr.io/agenthits/dokploy:agenthits-dev",
- latestIndexDigest: "sha256:index",
- latestPlatformDigest: "sha256:platform",
- failMetadataFetch: true,
- serviceEnv: [
- "RELEASE_TAG=agenthits-dev",
- "DOKPLOY_OFFICIAL_VERSION=v0.29.8",
- "DOKPLOY_FORK_VERSION=off_v0.29.8/Fork_160+latest",
- ],
- });
+ it(
+ "does not trust a mutable image tag as up to date when remote metadata cannot be loaded",
+ () => {
+ const { result, calls } = runUpdateScript({
+ currentImage: "ghcr.io/agenthits/dokploy:agenthits-dev",
+ latestIndexDigest: "sha256:index",
+ latestPlatformDigest: "sha256:platform",
+ failMetadataFetch: true,
+ serviceEnv: [
+ "RELEASE_TAG=agenthits-dev",
+ "DOKPLOY_OFFICIAL_VERSION=v0.29.8",
+ "DOKPLOY_FORK_VERSION=off_v0.29.8/Fork_160+latest",
+ ],
+ });
- expect(result.status).toBe(0);
- expect(result.stdout).toContain("Updating AgentHits Dokploy");
- expect(calls).toContain("service update");
- expect(calls).toContain("--update-failure-action rollback");
- expect(calls.indexOf("pull ")).toBeGreaterThanOrEqual(0);
- expect(calls.indexOf("pull ")).toBeLessThan(
- calls.indexOf("service update"),
- );
- });
+ expect(result.status).toBe(0);
+ expect(result.stdout).toContain("Updating AgentHits Dokploy");
+ expect(calls).toContain("service update");
+ expect(calls).toContain("--update-failure-action rollback");
+ expect(calls.indexOf("pull ")).toBeGreaterThanOrEqual(0);
+ expect(calls.indexOf("pull ")).toBeLessThan(
+ calls.indexOf("service update"),
+ );
+ },
+ SPAWN_TEST_TIMEOUT_MS,
+ );
- it("pulls the new image before updating the service when the digest changed", () => {
- const { result, calls } = runUpdateScript({
- currentDigest: "sha256:old",
- latestIndexDigest: "sha256:index",
- latestPlatformDigest: "sha256:platform",
- serviceEnv: [
- "RELEASE_TAG=agenthits-dev",
- "DOKPLOY_OFFICIAL_VERSION=v0.29.8",
- ],
- });
+ it(
+ "pulls the new image before updating the service when the digest changed",
+ () => {
+ const { result, calls } = runUpdateScript({
+ currentDigest: "sha256:old",
+ latestIndexDigest: "sha256:index",
+ latestPlatformDigest: "sha256:platform",
+ serviceEnv: [
+ "RELEASE_TAG=agenthits-dev",
+ "DOKPLOY_OFFICIAL_VERSION=v0.29.8",
+ ],
+ });
- expect(result.status).toBe(0);
- expect(result.stdout).toContain("Updating AgentHits Dokploy");
- expect(calls).toContain("service update");
- expect(calls).toContain("--update-failure-action rollback");
- expect(calls.indexOf("pull ")).toBeGreaterThanOrEqual(0);
- expect(calls.indexOf("pull ")).toBeLessThan(
- calls.indexOf("service update"),
- );
- });
+ expect(result.status).toBe(0);
+ expect(result.stdout).toContain("Updating AgentHits Dokploy");
+ expect(calls).toContain("service update");
+ expect(calls).toContain("--update-failure-action rollback");
+ expect(calls.indexOf("pull ")).toBeGreaterThanOrEqual(0);
+ expect(calls.indexOf("pull ")).toBeLessThan(
+ calls.indexOf("service update"),
+ );
+ },
+ SPAWN_TEST_TIMEOUT_MS,
+ );
- it("pulls before refreshing metadata when only service env is stale", () => {
- const { result, calls } = runUpdateScript({
- currentDigest: "sha256:index",
- latestIndexDigest: "sha256:index",
- latestPlatformDigest: "sha256:platform",
- serviceEnv: [
- "RELEASE_TAG=agenthits-dev",
- "DOKPLOY_OFFICIAL_VERSION=v0.29.8",
- "DOKPLOY_FORK_VERSION=off_v0.29.8/Fork_159+old",
- ],
- });
+ it(
+ "pulls before refreshing metadata when only service env is stale",
+ () => {
+ const { result, calls } = runUpdateScript({
+ currentDigest: "sha256:index",
+ latestIndexDigest: "sha256:index",
+ latestPlatformDigest: "sha256:platform",
+ serviceEnv: [
+ "RELEASE_TAG=agenthits-dev",
+ "DOKPLOY_OFFICIAL_VERSION=v0.29.8",
+ "DOKPLOY_FORK_VERSION=off_v0.29.8/Fork_159+old",
+ ],
+ });
- expect(result.status).toBe(0);
- expect(result.stdout).toContain("Updating AgentHits Dokploy");
- expect(calls).toContain("service update");
- expect(calls).toContain(
- "--env-add DOKPLOY_FORK_VERSION=off_v0.29.8/Fork_160+latest",
- );
- expect(calls.indexOf("pull ")).toBeGreaterThanOrEqual(0);
- expect(calls.indexOf("pull ")).toBeLessThan(
- calls.indexOf("service update"),
- );
- });
+ expect(result.status).toBe(0);
+ expect(result.stdout).toContain("Updating AgentHits Dokploy");
+ expect(calls).toContain("service update");
+ expect(calls).toContain(
+ "--env-add DOKPLOY_FORK_VERSION=off_v0.29.8/Fork_160+latest",
+ );
+ expect(calls.indexOf("pull ")).toBeGreaterThanOrEqual(0);
+ expect(calls.indexOf("pull ")).toBeLessThan(
+ calls.indexOf("service update"),
+ );
+ },
+ SPAWN_TEST_TIMEOUT_MS,
+ );
- it("leaves the running service untouched when the image cannot be pulled", () => {
- const { result, calls } = runUpdateScript({
- currentDigest: "sha256:old",
- latestIndexDigest: "sha256:index",
- latestPlatformDigest: "sha256:platform",
- failPull: true,
- serviceEnv: [
- "RELEASE_TAG=agenthits-dev",
- "DOKPLOY_OFFICIAL_VERSION=v0.29.8",
- ],
- });
+ it(
+ "leaves the running service untouched when the image cannot be pulled",
+ () => {
+ const { result, calls } = runUpdateScript({
+ currentDigest: "sha256:old",
+ latestIndexDigest: "sha256:index",
+ latestPlatformDigest: "sha256:platform",
+ failPull: true,
+ serviceEnv: [
+ "RELEASE_TAG=agenthits-dev",
+ "DOKPLOY_OFFICIAL_VERSION=v0.29.8",
+ ],
+ });
- expect(result.status).not.toBe(0);
- expect(result.stderr).toContain("left unchanged");
- expect(calls.match(/^pull /gm)).toHaveLength(3);
- expect(calls).not.toContain("service update");
- });
+ expect(result.status).not.toBe(0);
+ expect(result.stderr).toContain("left unchanged");
+ expect(calls.match(/^pull /gm)).toHaveLength(3);
+ expect(calls).not.toContain("service update");
+ },
+ SPAWN_TEST_TIMEOUT_MS,
+ );
it("keeps install-agenthits.sh update as a compatibility entrypoint", () => {
const installer = readFileSync(installerScript, "utf8");
diff --git a/apps/dokploy/__test__/settings/dokploy-image-cleanup.test.ts b/apps/dokploy/__test__/settings/dokploy-image-cleanup.test.ts
index 28bcc41de57..b9e1163755b 100644
--- a/apps/dokploy/__test__/settings/dokploy-image-cleanup.test.ts
+++ b/apps/dokploy/__test__/settings/dokploy-image-cleanup.test.ts
@@ -35,6 +35,8 @@ const { UPDATE_IMAGE_PULLED_MARKER } = await import(
);
const PREVIOUS = `sha256:${"a".repeat(64)}`;
+// Each case spawns sh; the 5s default times out on a CI runner under load.
+const SPAWN_TEST_TIMEOUT_MS = 30_000;
describe("Dokploy image cleanup", () => {
beforeEach(() => {
@@ -148,6 +150,7 @@ esac
rmSync(dir, { recursive: true, force: true });
}
},
+ SPAWN_TEST_TIMEOUT_MS,
);
it("does nothing at startup when the cleanup is off", async () => {
From 20216c0e1b2b426c80cb5465aa89dbc3be9acec1 Mon Sep 17 00:00:00 2001
From: merge-check <140916359+agentHits@users.noreply.github.com>
Date: Thu, 8 Oct 2026 08:54:29 -0400
Subject: [PATCH 06/33] fix(update): roll back a failed panel self-update
The panel self-update passed no failure action, so Swarm's default (pause)
applied: when the new task failed, the panel stayed down with no return to
the previous image. Both self-update commands now pass
--update-failure-action rollback, and state --update-order stop-first
explicitly.
Stop-first is deliberate. The panel runs drizzle migrations when its
container starts (apps/dokploy/migration.ts, run before server.ts), so an
overlapping old and new task would migrate and serve the same schema at
once. The image is pulled before the update, so the gap is only container
start time.
Co-Authored-By: Claude Haiku 5.5
---
.../__test__/settings/dokploy-image-cleanup.test.ts | 10 ++++++++++
packages/server/src/services/settings.ts | 9 ++++++---
2 files changed, 16 insertions(+), 3 deletions(-)
diff --git a/apps/dokploy/__test__/settings/dokploy-image-cleanup.test.ts b/apps/dokploy/__test__/settings/dokploy-image-cleanup.test.ts
index b9e1163755b..c68d12fe680 100644
--- a/apps/dokploy/__test__/settings/dokploy-image-cleanup.test.ts
+++ b/apps/dokploy/__test__/settings/dokploy-image-cleanup.test.ts
@@ -351,6 +351,16 @@ describe("Dokploy update commands", () => {
);
});
+ it("stops the old task first and rolls back a failed panel update", () => {
+ for (const command of [
+ getAgentHitsUpdateCommand("v0.30.6"),
+ getOfficialUpdateCommand("v0.30.7"),
+ ]) {
+ expect(command).toContain("--update-order stop-first");
+ expect(command).toContain("--update-failure-action rollback");
+ }
+ });
+
it("includes untagged images that swarm pulled by digest", async () => {
execAsync.mockResolvedValue({ stdout: "", stderr: "" });
diff --git a/packages/server/src/services/settings.ts b/packages/server/src/services/settings.ts
index 56463913b41..dd5d813d496 100644
--- a/packages/server/src/services/settings.ts
+++ b/packages/server/src/services/settings.ts
@@ -632,8 +632,9 @@ export const getAgentHitsUpdateCommand = (
const keepImagesArg =
keepImages === undefined ? "" : getDokployKeepImagesEnvArg(keepImages);
- // Pull while the old container still serves the panel: the service is
- // stop-first, so a pull inside the update would happen with Dokploy down.
+ // Pull while the old container still serves the panel: the update is
+ // stop-first, so a pull inside it would happen with Dokploy down. Stop-first
+ // also keeps two panel versions from running migrations at the same time.
return `
docker pull ${quoteShellArg(getAgentHitsUpdateImage())} || exit 1
echo ${quoteShellArg(UPDATE_IMAGE_PULLED_MARKER)}
@@ -649,6 +650,8 @@ docker service update --force \\
${forkVersionArg} \\
${keepImagesArg} \\
${PREVIOUS_IMAGE_ENV_ARG} \\
+ --update-order stop-first \\
+ --update-failure-action rollback \\
dokploy
`;
};
@@ -665,7 +668,7 @@ export const getOfficialUpdateCommand = (
docker pull ${image} || exit 1
echo ${quoteShellArg(UPDATE_IMAGE_PULLED_MARKER)}
${RECORD_PREVIOUS_IMAGE}
-docker service update --force --image ${image} ${keepImagesArg} ${PREVIOUS_IMAGE_ENV_ARG} dokploy
+docker service update --force --image ${image} ${keepImagesArg} ${PREVIOUS_IMAGE_ENV_ARG} --update-order stop-first --update-failure-action rollback dokploy
`;
};
From 1974a4646078f36080b0dc4865c15f0ceb81f382 Mon Sep 17 00:00:00 2001
From: merge-check <140916359+agentHits@users.noreply.github.com>
Date: Thu, 8 Oct 2026 08:57:27 -0400
Subject: [PATCH 07/33] feat(update): pre-download components before the update
changes anything
The Components update removed the running Traefik container before the new
image was local, and the Docker step restarted the daemon before anything
else was fetched. Now one gate runs before every step: it pulls the pinned
Traefik image, fetches the get.docker.com script and, on apt hosts,
downloads the pinned Docker packages. Any failure aborts with "Nothing was
changed" while every running service is still untouched.
Traefik is swapped without removing the running container first. The old
container is stopped and renamed, the new one is started and connected to
the network, and the old one is removed only after the new one has been
running for TRAEFIK_SETTLE_SECONDS (10 by default). If the new container
fails to start or is not running, the old one is renamed back, started, and
the script exits non-zero. ERR and HUP/INT/TERM traps cover a failed run or
network connect and an SSH session that drops mid-swap.
Docker keeps its daemon restart. The Docker documentation scopes live-restore
to standalone containers and states that it does not apply to Swarm services,
which are the workload on these hosts, so daemon.json is not changed.
The "recreates the Traefik container" test asserted the remove-first step and
now asserts the rename. New tests run the generated script against fake
docker, apt-get, apt-cache, curl and sudo commands to check the order and the
restore paths.
Co-Authored-By: Claude Haiku 5.5
---
.../__test__/server/server-components.test.ts | 232 +++++++++++++++++-
.../server/src/setup/server-components.ts | 82 ++++++-
2 files changed, 307 insertions(+), 7 deletions(-)
diff --git a/apps/dokploy/__test__/server/server-components.test.ts b/apps/dokploy/__test__/server/server-components.test.ts
index b2d50e9e9ec..39cfab2b1df 100644
--- a/apps/dokploy/__test__/server/server-components.test.ts
+++ b/apps/dokploy/__test__/server/server-components.test.ts
@@ -1,4 +1,13 @@
import { spawnSync } from "node:child_process";
+import {
+ chmodSync,
+ mkdtempSync,
+ readFileSync,
+ rmSync,
+ writeFileSync,
+} from "node:fs";
+import { tmpdir } from "node:os";
+import path from "node:path";
import {
BUILDPACKS_VERSION,
NIXPACKS_VERSION,
@@ -10,6 +19,7 @@ import {
buildComponentUpdateScript,
COMPONENTS_UPDATE_DONE,
parseComponentVersions,
+ type UpdatableComponent,
} from "@dokploy/server/setup/server-components";
import {
buildTraefikRunCommand,
@@ -101,11 +111,13 @@ describe("buildComponentStatuses", () => {
});
describe("buildComponentUpdateScript", () => {
- it("recreates the Traefik container from the pinned image", () => {
+ it("recreates the Traefik container from the pinned image, keeping the old one until the new one runs", () => {
const script = buildComponentUpdateScript(["traefik"]);
expect(script).toContain(`docker pull traefik:v${TRAEFIK_VERSION}`);
- expect(script).toContain("docker rm -f dokploy-traefik");
+ expect(script).toContain(
+ "docker rename dokploy-traefik dokploy-traefik-previous",
+ );
expect(script).toContain(buildTraefikRunCommand(TRAEFIK_VERSION).trim());
});
@@ -137,7 +149,9 @@ describe("buildComponentUpdateScript", () => {
it("generates valid bash for every updatable component", () => {
const script = buildComponentUpdateScript([
+ "docker",
"traefik",
+ "rclone",
"nixpacks",
"railpack",
"buildpacks",
@@ -149,6 +163,220 @@ describe("buildComponentUpdateScript", () => {
});
});
+const FAKE_TOOLS: Record = {
+ docker: `#!/bin/sh
+printf 'docker %s\\n' "$*" >> "$CALL_LOG"
+case "$1" in
+ pull) [ "$FAKE_PULL_FAILS" = 1 ] && exit 1 ;;
+ run) [ "$FAKE_RUN_FAILS" = 1 ] && exit 1 ;;
+ inspect)
+ case "$*" in
+ *State.Running*)
+ if [ "$FAKE_NOT_RUNNING" = 1 ]; then echo false; else echo true; fi
+ ;;
+ esac
+ ;;
+esac
+exit 0
+`,
+ "apt-get": `#!/bin/sh
+printf 'apt-get %s\\n' "$*" >> "$CALL_LOG"
+case "$*" in
+ *--download-only*) [ "$FAKE_DOWNLOAD_FAILS" = 1 ] && exit 1 ;;
+esac
+exit 0
+`,
+ "apt-cache": `#!/bin/sh
+printf 'apt-cache %s\\n' "$*" >> "$CALL_LOG"
+if [ "$FAKE_NO_DOCKER_PACKAGE" = 1 ]; then exit 0; fi
+echo " docker-ce | 5:29.8.2-1~ubuntu.24.04~noble | https://download.docker.com/linux/ubuntu noble/stable amd64 Packages"
+`,
+ curl: `#!/bin/sh
+printf 'curl %s\\n' "$*" >> "$CALL_LOG"
+out=""
+while [ "$#" -gt 0 ]; do
+ if [ "$1" = "-o" ]; then out="$2"; fi
+ shift
+done
+printf '%s\\n' 'echo fake get-docker "$@"' > "$out"
+`,
+ sudo: `#!/bin/sh
+exec "$@"
+`,
+ sleep: `#!/bin/sh
+exit 0
+`,
+};
+
+const runComponentScript = (
+ components: UpdatableComponent[],
+ env: Record = {},
+) => {
+ const dir = mkdtempSync(path.join(tmpdir(), "component-update-"));
+ try {
+ const callLog = path.join(dir, "calls.log");
+ writeFileSync(callLog, "");
+ for (const [name, body] of Object.entries(FAKE_TOOLS)) {
+ writeFileSync(path.join(dir, name), body);
+ chmodSync(path.join(dir, name), 0o755);
+ }
+ const result = spawnSync("bash", [], {
+ input: buildComponentUpdateScript(components),
+ encoding: "utf8",
+ env: {
+ ...process.env,
+ PATH: `${dir}:${process.env.PATH}`,
+ CALL_LOG: callLog,
+ TRAEFIK_SETTLE_SECONDS: "0",
+ ...env,
+ },
+ });
+ const calls = readFileSync(callLog, "utf8").split("\n").filter(Boolean);
+ return { result, calls };
+ } finally {
+ rmSync(dir, { force: true, recursive: true });
+ }
+};
+
+const SPAWN_TEST_TIMEOUT_MS = 30_000;
+
+describe("buildComponentUpdateScript run order", () => {
+ it(
+ "changes nothing when the Traefik image cannot be pulled",
+ () => {
+ const { result, calls } = runComponentScript(["traefik"], {
+ FAKE_PULL_FAILS: "1",
+ });
+
+ expect(result.status).not.toBe(0);
+ expect(result.stderr).toContain("Nothing was changed");
+ expect(
+ calls.some((call) => /^docker (stop|rename|run) /.test(call)),
+ ).toBe(false);
+ },
+ SPAWN_TEST_TIMEOUT_MS,
+ );
+
+ it(
+ "downloads every artifact before the first step changes anything",
+ () => {
+ const { result, calls } = runComponentScript(["traefik", "docker"]);
+
+ expect(result.status).toBe(0);
+ const download = calls.findIndex((call) =>
+ call.includes("--download-only"),
+ );
+ const pull = calls.findIndex((call) =>
+ call.startsWith("docker pull traefik:v"),
+ );
+ const stop = calls.findIndex((call) => call.startsWith("docker stop"));
+ expect(download).toBeGreaterThanOrEqual(0);
+ expect(pull).toBeGreaterThanOrEqual(0);
+ expect(pull).toBeLessThan(stop);
+ expect(download).toBeLessThan(stop);
+ },
+ SPAWN_TEST_TIMEOUT_MS,
+ );
+
+ it(
+ "replaces Traefik only after the new container has run",
+ () => {
+ const { result, calls } = runComponentScript(["traefik"]);
+
+ expect(result.status).toBe(0);
+ expect(result.stdout).toContain(
+ `Traefik version ${TRAEFIK_VERSION} installed`,
+ );
+ const at = (prefix: string) =>
+ calls.findIndex((call) => call.startsWith(prefix));
+ const order = [
+ at("docker pull traefik:v"),
+ at("docker stop dokploy-traefik"),
+ at("docker rename dokploy-traefik dokploy-traefik-previous"),
+ at("docker run -d"),
+ at("docker network connect dokploy-network dokploy-traefik"),
+ at("docker inspect -f {{.State.Running}} dokploy-traefik"),
+ ];
+ expect(order.every((position) => position >= 0)).toBe(true);
+ expect(order).toEqual([...order].sort((a, b) => a - b));
+ expect(calls.at(-1)).toBe("docker rm -f dokploy-traefik-previous");
+ },
+ SPAWN_TEST_TIMEOUT_MS,
+ );
+
+ it(
+ "restores the previous Traefik container when the new one is not running",
+ () => {
+ const { result, calls } = runComponentScript(["traefik"], {
+ FAKE_NOT_RUNNING: "1",
+ });
+
+ expect(result.status).not.toBe(0);
+ expect(result.stderr).toContain("restoring the previous container");
+ expect(calls).toContain(
+ "docker rename dokploy-traefik-previous dokploy-traefik",
+ );
+ expect(calls.at(-1)).toBe("docker start dokploy-traefik");
+ expect(result.stdout).not.toContain("installed");
+ },
+ SPAWN_TEST_TIMEOUT_MS,
+ );
+
+ it(
+ "restores the previous Traefik container when docker run fails",
+ () => {
+ const { result, calls } = runComponentScript(["traefik"], {
+ FAKE_RUN_FAILS: "1",
+ });
+
+ expect(result.status).not.toBe(0);
+ expect(calls).toContain(
+ "docker rename dokploy-traefik-previous dokploy-traefik",
+ );
+ expect(
+ calls.some((call) => call.startsWith("docker network connect")),
+ ).toBe(false);
+ expect(calls.at(-1)).toBe("docker start dokploy-traefik");
+ },
+ SPAWN_TEST_TIMEOUT_MS,
+ );
+
+ it(
+ "downloads the pinned Docker packages before the Docker step runs",
+ () => {
+ const { result, calls } = runComponentScript(["docker"]);
+
+ expect(result.status).toBe(0);
+ expect(calls).toContain(
+ "apt-get install -y -qq --download-only docker-ce=5:29.8.2-1~ubuntu.24.04~noble docker-ce-cli=5:29.8.2-1~ubuntu.24.04~noble containerd.io docker-buildx-plugin docker-compose-plugin",
+ );
+ expect(
+ result.stdout.indexOf("Downloading update artifacts"),
+ ).toBeLessThan(
+ result.stdout.indexOf(`Updating Docker to ${PINNED_VERSIONS.docker}`),
+ );
+ },
+ SPAWN_TEST_TIMEOUT_MS,
+ );
+
+ it(
+ "stops before the Docker step when apt has no package for the pinned version",
+ () => {
+ const { result, calls } = runComponentScript(["docker"], {
+ FAKE_NO_DOCKER_PACKAGE: "1",
+ });
+
+ expect(result.status).not.toBe(0);
+ expect(result.stderr).toContain("Nothing was changed");
+ expect(calls.some((call) => call.startsWith("apt-get install"))).toBe(
+ false,
+ );
+ expect(result.stdout).not.toContain("Updating Docker");
+ },
+ SPAWN_TEST_TIMEOUT_MS,
+ );
+});
+
describe("buildTraefikRunCommand", () => {
it("publishes the HTTP, HTTPS and HTTP/3 ports with the requested image", () => {
const command = buildTraefikRunCommand("3.7.5");
diff --git a/packages/server/src/setup/server-components.ts b/packages/server/src/setup/server-components.ts
index 76fff4b869c..12a8704f759 100644
--- a/packages/server/src/setup/server-components.ts
+++ b/packages/server/src/setup/server-components.ts
@@ -94,14 +94,60 @@ export const buildComponentStatuses = (
};
});
+// Runs before any step: a failed download stops the update while nothing
+// running has been touched. Only Docker and Traefik replace running services;
+// the other components are plain binaries with no running state to protect.
+const predownloadGate = (components: UpdatableComponent[]) => {
+ const downloads: string[] = [];
+ if (components.includes("traefik")) {
+ downloads.push(
+ `download_with_retry $SUDO_CMD docker pull traefik:v${TRAEFIK_VERSION} || abort_before_change "Pre-download failed: traefik:v${TRAEFIK_VERSION}."`,
+ );
+ }
+ if (components.includes("docker")) {
+ const dockerVersionPattern = PINNED_VERSIONS.docker.replace(/\./g, "\\.");
+ downloads.push(`GET_DOCKER_SCRIPT="$(mktemp)"
+download_with_retry curl -fsSL https://get.docker.com -o "$GET_DOCKER_SCRIPT" || abort_before_change "Pre-download failed: get.docker.com."
+if command -v apt-get >/dev/null 2>&1; then
+ $SUDO_CMD apt-get update -qq || abort_before_change "Pre-download failed: the apt package lists could not be refreshed."
+ DOCKER_PACKAGE_VERSION="$(apt-cache madison docker-ce | awk -F'|' '{ gsub(/ /, "", $2); print $2 }' | grep -m1 -E '^([0-9]+:)?${dockerVersionPattern}-' || true)"
+ [ -n "$DOCKER_PACKAGE_VERSION" ] || abort_before_change "Pre-download failed: Docker ${PINNED_VERSIONS.docker} is not in the apt sources."
+ download_with_retry $SUDO_CMD apt-get install -y -qq --download-only "docker-ce=$DOCKER_PACKAGE_VERSION" "docker-ce-cli=$DOCKER_PACKAGE_VERSION" containerd.io docker-buildx-plugin docker-compose-plugin || abort_before_change "Pre-download failed: the Docker packages."
+else
+ echo "No apt-get on this host: the Docker packages are not pre-downloaded; the Docker step fetches them."
+fi`);
+ }
+ if (downloads.length === 0) {
+ return "";
+ }
+ return `
+abort_before_change() {
+ echo "$1 Nothing was changed." >&2
+ exit 1
+}
+download_with_retry() {
+ local attempt=1
+ while [ "$attempt" -le 3 ]; do
+ if "$@"; then
+ return 0
+ fi
+ attempt=$((attempt + 1))
+ sleep 5
+ done
+ return 1
+}
+echo "Downloading update artifacts before anything is changed"
+${downloads.join("\n")}
+`;
+};
+
const updateStepFor = (component: UpdatableComponent) => {
switch (component) {
case "docker":
return `
echo "Updating Docker to ${PINNED_VERSIONS.docker}"
-curl -fsSL https://get.docker.com -o /tmp/get-docker.sh
-$SUDO_CMD sh /tmp/get-docker.sh --version ${PINNED_VERSIONS.docker}
-rm -f /tmp/get-docker.sh
+$SUDO_CMD sh "$GET_DOCKER_SCRIPT" --version ${PINNED_VERSIONS.docker}
+rm -f "$GET_DOCKER_SCRIPT"
echo "Docker version ${PINNED_VERSIONS.docker} installed ✅"
`;
case "rclone":
@@ -111,9 +157,34 @@ ${rcloneInstallCommand()}
case "traefik":
return `
echo "Updating Traefik to ${TRAEFIK_VERSION}"
-$SUDO_CMD docker pull traefik:v${TRAEFIK_VERSION}
-$SUDO_CMD docker rm -f dokploy-traefik >/dev/null 2>&1 || true
+traefik_previous_kept=0
+traefik_swapped=0
+restore_traefik() {
+ if [ "$traefik_swapped" = 0 ]; then
+ echo "Traefik ${TRAEFIK_VERSION} did not start; restoring the previous container." >&2
+ if [ "$traefik_previous_kept" = 1 ]; then
+ $SUDO_CMD docker rm -f dokploy-traefik >/dev/null 2>&1 || true
+ $SUDO_CMD docker rename dokploy-traefik-previous dokploy-traefik || true
+ fi
+ $SUDO_CMD docker start dokploy-traefik >/dev/null 2>&1 || true
+ fi
+ exit 1
+}
+trap restore_traefik ERR HUP INT TERM
+if $SUDO_CMD docker inspect dokploy-traefik >/dev/null 2>&1; then
+ $SUDO_CMD docker stop dokploy-traefik >/dev/null
+ $SUDO_CMD docker rm -f dokploy-traefik-previous >/dev/null 2>&1 || true
+ $SUDO_CMD docker rename dokploy-traefik dokploy-traefik-previous
+ traefik_previous_kept=1
+fi
${buildTraefikRunCommand(TRAEFIK_VERSION)}
+sleep "\${TRAEFIK_SETTLE_SECONDS:-10}"
+if [ "$($SUDO_CMD docker inspect -f '{{.State.Running}}' dokploy-traefik 2>/dev/null)" != "true" ]; then
+ restore_traefik
+fi
+traefik_swapped=1
+trap - ERR HUP INT TERM
+$SUDO_CMD docker rm -f dokploy-traefik-previous >/dev/null 2>&1 || true
echo "Traefik version ${TRAEFIK_VERSION} installed ✅"
`;
case "nixpacks":
@@ -147,6 +218,7 @@ export const buildComponentUpdateScript = (
) => `
set -e
if [ "$(id -u)" -eq 0 ]; then SUDO_CMD=""; else SUDO_CMD="sudo"; fi
+${predownloadGate(components)}
${components.map(updateStepFor).join("\n")}
echo "${COMPONENTS_UPDATE_DONE}"
`;
From 0a4a72a3d84d94c32c76789afa750a97697acffb Mon Sep 17 00:00:00 2001
From: merge-check <140916359+agentHits@users.noreply.github.com>
Date: Thu, 8 Oct 2026 09:04:08 -0400
Subject: [PATCH 08/33] feat(update): read the panel update outcome from swarm
update.sh ran an attached docker service update. Attached mode has no
timeout, and it reports a completed rollback as success, so a panel update
that rolled back looked like a success. The update is now submitted with
--detach, and the script waits until the update is registered (the service
Version.Index has moved) before it reads UpdateStatus. It fails on a paused
or rolled-back state, and when the new task is not healthy within
AGENTHITS_HEALTH_TIMEOUT (240 seconds by default).
The update-script tests fake the service index, the update state and the
container health. A new case covers a rollback.
Co-Authored-By: Claude Haiku 5.5
---
.../settings/agenthits-update-script.test.ts | 48 +++++++++++++++++++
update.sh | 45 ++++++++++++++++-
2 files changed, 92 insertions(+), 1 deletion(-)
diff --git a/apps/dokploy/__test__/settings/agenthits-update-script.test.ts b/apps/dokploy/__test__/settings/agenthits-update-script.test.ts
index 9af6e790386..ea7bfb54a2b 100644
--- a/apps/dokploy/__test__/settings/agenthits-update-script.test.ts
+++ b/apps/dokploy/__test__/settings/agenthits-update-script.test.ts
@@ -33,6 +33,7 @@ const writeFakeDocker = (
latestForkVersion?: string;
serviceEnv: string[];
failPull?: boolean;
+ updateState?: string;
},
) => {
const dockerPath = path.join(dir, "docker");
@@ -52,6 +53,16 @@ if [ "$1" = "service" ] && [ "$2" = "inspect" ]; then
${options.serviceEnv.map((entry) => ` printf '%s\\n' "${entry}"`).join("\n")}
exit 0
;;
+ *Version.Index*)
+ cat "$DOCKER_INDEX_FILE"
+ exit 0
+ ;;
+ *UpdateStatus.State*)
+ if [ "$(cat "$DOCKER_INDEX_FILE")" -gt 1 ]; then
+ printf '%s\\n' "${options.updateState ?? "completed"}"
+ fi
+ exit 0
+ ;;
esac
fi
@@ -74,6 +85,17 @@ if [ "$1" = "pull" ]; then
fi
if [ "$1" = "service" ] && [ "$2" = "update" ]; then
+ echo $(( $(cat "$DOCKER_INDEX_FILE") + 1 )) > "$DOCKER_INDEX_FILE"
+ exit 0
+fi
+
+if [ "$1" = "ps" ]; then
+ echo fake-panel-container
+ exit 0
+fi
+
+if [ "$1" = "inspect" ]; then
+ echo healthy
exit 0
fi
@@ -135,6 +157,8 @@ const runUpdateScript = (
try {
const callLog = path.join(tempDir, "docker-calls.log");
writeFileSync(callLog, "");
+ const indexFile = path.join(tempDir, "service-index");
+ writeFileSync(indexFile, "1");
writeFakeDocker(tempDir, fakeDockerOptions);
writeFakeCurl(tempDir, fakeDockerOptions);
@@ -143,7 +167,10 @@ const runUpdateScript = (
...process.env,
AGENTHITS_SKIP_HOST_CHECK: "1",
AGENTHITS_PULL_RETRY_DELAY: "0",
+ AGENTHITS_HEALTH_INTERVAL: "0",
+ AGENTHITS_HEALTH_TIMEOUT: "2",
DOCKER_CALL_LOG: callLog,
+ DOCKER_INDEX_FILE: indexFile,
PATH: `${tempDir}:${process.env.PATH}`,
},
encoding: "utf8",
@@ -309,6 +336,27 @@ describe("AgentHits update script", () => {
SPAWN_TEST_TIMEOUT_MS,
);
+ it(
+ "fails and reports the swarm state when the panel update is rolled back",
+ () => {
+ const { result, calls } = runUpdateScript({
+ currentDigest: "sha256:old",
+ latestIndexDigest: "sha256:index",
+ latestPlatformDigest: "sha256:platform",
+ updateState: "rollback_completed",
+ serviceEnv: [
+ "RELEASE_TAG=agenthits-dev",
+ "DOKPLOY_OFFICIAL_VERSION=v0.29.8",
+ ],
+ });
+
+ expect(result.status).not.toBe(0);
+ expect(result.stderr).toContain("rollback_completed");
+ expect(calls).toContain("service update --detach");
+ },
+ SPAWN_TEST_TIMEOUT_MS,
+ );
+
it("keeps install-agenthits.sh update as a compatibility entrypoint", () => {
const installer = readFileSync(installerScript, "utf8");
diff --git a/update.sh b/update.sh
index fa3e76d0d8d..c0194a4b5bd 100755
--- a/update.sh
+++ b/update.sh
@@ -7,6 +7,8 @@ DOKPLOY_OFFICIAL_VERSION_OVERRIDE="${DOKPLOY_OFFICIAL_VERSION:-}"
DOKPLOY_FORK_VERSION_OVERRIDE="${DOKPLOY_FORK_VERSION:-}"
DOKPLOY_OFFICIAL_VERSION="${DOKPLOY_OFFICIAL_VERSION_OVERRIDE:-v0.29.8}"
DOKPLOY_FORK_VERSION="${DOKPLOY_FORK_VERSION_OVERRIDE:-}"
+AGENTHITS_HEALTH_TIMEOUT="${AGENTHITS_HEALTH_TIMEOUT:-240}"
+AGENTHITS_HEALTH_INTERVAL="${AGENTHITS_HEALTH_INTERVAL:-3}"
command_exists() {
command -v "$@" >/dev/null 2>&1
@@ -206,6 +208,44 @@ metadata_matches() {
return 0
}
+panel_container_healthy() {
+ local container=""
+ container="$(docker ps -q --no-trunc --filter label=com.docker.swarm.service.name=dokploy --filter status=running | head -n1 || true)"
+ if [ -z "$container" ]; then
+ return 1
+ fi
+ [ "$(docker inspect --format '{{.State.Health.Status}}' "$container" 2>/dev/null)" = "healthy" ]
+}
+
+# Swarm rolls a failed update back, and the attached CLI still exits 0 then,
+# so the outcome is read from the service instead.
+wait_for_panel_update() {
+ local previous_index="$1"
+ local deadline=$(($(date +%s) + AGENTHITS_HEALTH_TIMEOUT))
+ local index=""
+ local state=""
+ while :; do
+ index="$(docker service inspect dokploy --format '{{.Version.Index}}' 2>/dev/null || true)"
+ state="$(docker service inspect dokploy --format '{{.UpdateStatus.State}}' 2>/dev/null || true)"
+ if [ "${index:-0}" -gt "$previous_index" ]; then
+ case "$state" in
+ paused | rollback_*)
+ echo "Error: the AgentHits Dokploy update did not succeed (swarm state: $state). See: docker service ps dokploy --no-trunc" >&2
+ exit 1
+ ;;
+ esac
+ if [ "$state" = "completed" ] && panel_container_healthy; then
+ return 0
+ fi
+ fi
+ if [ "$(date +%s)" -ge "$deadline" ]; then
+ echo "Error: AgentHits Dokploy was not healthy after ${AGENTHITS_HEALTH_TIMEOUT} seconds (swarm state: ${state:-unknown})." >&2
+ exit 1
+ fi
+ sleep "$AGENTHITS_HEALTH_INTERVAL"
+ done
+}
+
update_agenthits_dokploy() {
require_root_linux_host
install_docker_if_missing
@@ -280,7 +320,10 @@ update_agenthits_dokploy() {
elif printf '%s\n' "$service_env" | grep -q '^DOKPLOY_FORK_VERSION='; then
update_args+=(--env-rm DOKPLOY_FORK_VERSION)
fi
- docker service update "${update_args[@]}" dokploy
+ local previous_index=""
+ previous_index="$(docker service inspect dokploy --format '{{.Version.Index}}' 2>/dev/null || true)"
+ docker service update --detach "${update_args[@]}" dokploy
+ wait_for_panel_update "${previous_index:-0}"
echo "AgentHits Dokploy updated to $DOKPLOY_IMAGE"
}
From 228da6c7d20e6226b7224f313d688e619b253963 Mon Sep 17 00:00:00 2001
From: merge-check <140916359+agentHits@users.noreply.github.com>
Date: Thu, 8 Oct 2026 09:04:15 -0400
Subject: [PATCH 09/33] feat(update): update the panel host stack in dependency
order
install-agenthits.sh update replaced only the panel image. It now runs the
whole host stack, after one pre-download gate that pulls every pinned image
(Postgres, Redis, Traefik, the panel) and fetches update.sh before anything
is changed. Then, in order:
1. Postgres backup: pg_dumpall to AGENTHITS_BACKUP_DIR, kept on the host.
It runs before any swap, so a failed backup changes nothing.
2. Redis: stop-first swarm update with rollback on failure; waits for PONG.
3. Traefik: the running container is stopped and renamed, the new one is
started and joined to dokploy-network, and it must still be running after
AGENTHITS_TRAEFIK_SETTLE seconds. If it is not, the old container is put
back. The old container stays stopped as dokploy-traefik-previous with
restart=no, so a daemon restart cannot start it next to the new one.
4. Postgres: stop-first swarm update, which the shared data volume requires;
waits for pg_isready.
5. Panel: update.sh, which waits for health and rolls back on failure.
6. Docker Engine, only with DOCKER_ENGINE_UPGRADE=1: the packages are
downloaded in the gate and installed with --no-download after the rest is
healthy. The daemon restart is the one disruption that remains.
Order: Redis and Traefik fail before the database changes. Postgres follows
its backup and comes before the panel, because the new panel migrates the
database when it starts. The panel is last, so its dependencies are on their
pinned versions and healthy before its update runs.
The fresh install now calls run_dokploy_traefik for its Traefik container
with the same flags, so both paths share one definition.
install-agenthits-update.test.ts runs the installer in update mode against
fake docker, apt, curl, id and uname. It checks the order, that a failed
pre-download or backup changes nothing, that a rolled-back Redis stops the
update, and that a Traefik that does not start is restored.
Co-Authored-By: Claude Haiku 5.5
---
.../settings/install-agenthits-update.test.ts | 357 +++++++++++++++++
install-agenthits.sh | 359 ++++++++++++++++--
2 files changed, 693 insertions(+), 23 deletions(-)
create mode 100644 apps/dokploy/__test__/settings/install-agenthits-update.test.ts
diff --git a/apps/dokploy/__test__/settings/install-agenthits-update.test.ts b/apps/dokploy/__test__/settings/install-agenthits-update.test.ts
new file mode 100644
index 00000000000..42c335d9d62
--- /dev/null
+++ b/apps/dokploy/__test__/settings/install-agenthits-update.test.ts
@@ -0,0 +1,357 @@
+import { spawnSync } from "node:child_process";
+import {
+ chmodSync,
+ mkdtempSync,
+ readdirSync,
+ readFileSync,
+ rmSync,
+ writeFileSync,
+} from "node:fs";
+import { tmpdir } from "node:os";
+import path from "node:path";
+import { fileURLToPath } from "node:url";
+import { describe, expect, it } from "vitest";
+
+const repoRoot = path.resolve(
+ path.dirname(fileURLToPath(import.meta.url)),
+ "../../../..",
+);
+const installerScript = path.join(repoRoot, "install-agenthits.sh");
+
+const SPAWN_TEST_TIMEOUT_MS = 60_000;
+
+const realPath = (command: string) =>
+ spawnSync("sh", ["-c", `command -v ${command}`], {
+ encoding: "utf8",
+ }).stdout.trim();
+
+const FAKE_DOCKER = `#!/bin/sh
+printf '%s\\n' "$*" >> "$DOCKER_CALL_LOG"
+case "$1" in
+ info | version)
+ echo 29.8.2
+ exit 0
+ ;;
+ pull)
+ if [ "$2" = "$FAKE_PULL_FAILS_FOR" ]; then exit 1; fi
+ exit 0
+ ;;
+ ps)
+ echo fake-task-container
+ exit 0
+ ;;
+ exec)
+ case "$*" in
+ *pg_dumpall*)
+ if [ "$FAKE_DUMP_FAILS" = 1 ]; then exit 1; fi
+ echo "-- fake dump"
+ ;;
+ *redis-cli*)
+ echo PONG
+ ;;
+ esac
+ exit 0
+ ;;
+ inspect)
+ case "$*" in
+ *Config.Image*) echo "$FAKE_TRAEFIK_IMAGE" ;;
+ *State.Running*) echo "$FAKE_TRAEFIK_RUNNING" ;;
+ *State.Health.Status*) echo healthy ;;
+ esac
+ exit 0
+ ;;
+ service)
+ if [ "$2" = "inspect" ]; then
+ case "$5" in
+ *ContainerSpec.Image*)
+ case "$3" in
+ dokploy) echo "$FAKE_PANEL_IMAGE" ;;
+ dokploy-redis) echo "$FAKE_REDIS_IMAGE" ;;
+ dokploy-postgres) echo "$FAKE_POSTGRES_IMAGE" ;;
+ esac
+ ;;
+ *ContainerSpec.Env*)
+ printf '%s\\n' "RELEASE_TAG=agenthits-dev" "DOKPLOY_OFFICIAL_VERSION=v0.29.8"
+ ;;
+ *Version.Index*)
+ cat "$DOCKER_INDEX_FILE"
+ ;;
+ *UpdateStatus.State*)
+ if [ "$(cat "$DOCKER_INDEX_FILE")" -gt 1 ]; then echo "$FAKE_UPDATE_STATE"; fi
+ ;;
+ esac
+ exit 0
+ fi
+ if [ "$2" = "update" ]; then
+ echo $(( $(cat "$DOCKER_INDEX_FILE") + 1 )) > "$DOCKER_INDEX_FILE"
+ fi
+ exit 0
+ ;;
+ buildx)
+ cat <<'EOF'
+Name: ghcr.io/agenthits/dokploy:agenthits-dev
+MediaType: application/vnd.oci.image.index.v1+json
+Digest: sha256:index
+
+Manifests:
+ Name: ghcr.io/agenthits/dokploy:agenthits-dev@sha256:platform
+ MediaType: application/vnd.oci.image.manifest.v1+json
+ Platform: linux/amd64
+EOF
+ exit 0
+ ;;
+esac
+exit 0
+`;
+
+const FAKE_CURL = `#!/bin/sh
+echo "unexpected curl: $*" >> "$DOCKER_CALL_LOG"
+exit 1
+`;
+
+const FAKE_ID = `#!/bin/sh
+if [ "$1" = "-u" ]; then
+ echo 0
+ exit 0
+fi
+exec ${realPath("id")} "$@"
+`;
+
+const FAKE_UNAME = `#!/bin/sh
+if [ "$1" = "-s" ]; then
+ echo Linux
+ exit 0
+fi
+exec ${realPath("uname")} "$@"
+`;
+
+type OperatorScenario = {
+ redisImage?: string;
+ postgresImage?: string;
+ traefikImage?: string;
+ panelImage?: string;
+ traefikRunning?: "true" | "false";
+ updateState?: string;
+ pullFailsFor?: string;
+ dumpFails?: boolean;
+};
+
+const runInstaller = (scenario: OperatorScenario = {}) => {
+ const dir = mkdtempSync(path.join(tmpdir(), "agenthits-operator-update-"));
+ try {
+ const callLog = path.join(dir, "docker-calls.log");
+ const indexFile = path.join(dir, "service-index");
+ const backupDir = path.join(dir, "backups");
+ writeFileSync(callLog, "");
+ writeFileSync(indexFile, "1");
+
+ const fakes: Record = {
+ docker: FAKE_DOCKER,
+ curl: FAKE_CURL,
+ id: FAKE_ID,
+ uname: FAKE_UNAME,
+ };
+ for (const [name, body] of Object.entries(fakes)) {
+ writeFileSync(path.join(dir, name), body);
+ chmodSync(path.join(dir, name), 0o755);
+ }
+
+ const result = spawnSync("bash", [installerScript, "update"], {
+ encoding: "utf8",
+ env: {
+ ...process.env,
+ PATH: `${dir}:${process.env.PATH}`,
+ DOCKER_CALL_LOG: callLog,
+ DOCKER_INDEX_FILE: indexFile,
+ AGENTHITS_SKIP_HOST_CHECK: "1",
+ AGENTHITS_BACKUP_DIR: backupDir,
+ AGENTHITS_PULL_RETRY_DELAY: "0",
+ AGENTHITS_HEALTH_INTERVAL: "0",
+ AGENTHITS_HEALTH_TIMEOUT: "2",
+ AGENTHITS_TRAEFIK_SETTLE: "0",
+ REDIS_IMAGE: "redis:8.10.2",
+ POSTGRES_IMAGE: "postgres:18.6",
+ TRAEFIK_IMAGE: "traefik:v3.7.14",
+ FAKE_REDIS_IMAGE: scenario.redisImage ?? "redis:8.10.2",
+ FAKE_POSTGRES_IMAGE: scenario.postgresImage ?? "postgres:18.6",
+ FAKE_TRAEFIK_IMAGE: scenario.traefikImage ?? "traefik:v3.7.14",
+ FAKE_PANEL_IMAGE:
+ scenario.panelImage ??
+ "ghcr.io/agenthits/dokploy:agenthits-dev@sha256:index",
+ FAKE_TRAEFIK_RUNNING: scenario.traefikRunning ?? "true",
+ FAKE_UPDATE_STATE: scenario.updateState ?? "completed",
+ FAKE_PULL_FAILS_FOR: scenario.pullFailsFor ?? "",
+ FAKE_DUMP_FAILS: scenario.dumpFails ? "1" : "0",
+ },
+ });
+
+ const calls = readFileSync(callLog, "utf8").split("\n").filter(Boolean);
+ const backups = (() => {
+ try {
+ return readdirSync(backupDir);
+ } catch {
+ return [];
+ }
+ })();
+ return { result, calls, backups };
+ } finally {
+ rmSync(dir, { force: true, recursive: true });
+ }
+};
+
+const firstIndex = (calls: string[], prefix: string) =>
+ calls.findIndex((call) => call.startsWith(prefix));
+
+describe("install-agenthits.sh update", () => {
+ it(
+ "changes nothing when every pinned image is already running",
+ () => {
+ const { result, calls, backups } = runInstaller();
+
+ expect(result.status).toBe(0);
+ expect(result.stdout).toContain("Traefik already runs");
+ expect(result.stdout).toContain("already up to date");
+ expect(calls.some((call) => call.startsWith("service update"))).toBe(
+ false,
+ );
+ expect(calls.some((call) => call.startsWith("run -d"))).toBe(false);
+ expect(backups).toEqual([]);
+ },
+ SPAWN_TEST_TIMEOUT_MS,
+ );
+
+ it(
+ "pre-downloads every image before any service is touched",
+ () => {
+ const { result, calls } = runInstaller({
+ pullFailsFor: "traefik:v3.7.14",
+ redisImage: "redis:8.10.1",
+ traefikImage: "traefik:v3.6.25",
+ });
+
+ expect(result.status).not.toBe(0);
+ expect(result.stderr).toContain(
+ "Pre-download failed: traefik:v3.7.14. Nothing was changed.",
+ );
+ expect(calls.some((call) => call.startsWith("service update"))).toBe(
+ false,
+ );
+ expect(calls.some((call) => call.startsWith("stop "))).toBe(false);
+ expect(calls.some((call) => call.startsWith("run -d"))).toBe(false);
+ },
+ SPAWN_TEST_TIMEOUT_MS,
+ );
+
+ it(
+ "backs up Postgres and swaps Redis, Traefik, Postgres, then the panel, in that order",
+ () => {
+ const { result, calls, backups } = runInstaller({
+ redisImage: "redis:8.10.1",
+ postgresImage: "postgres:18.5",
+ traefikImage: "traefik:v3.6.25",
+ });
+
+ expect(result.status).toBe(0);
+ expect(backups).toHaveLength(1);
+ expect(backups[0]).toMatch(/^postgres-.*\.sql\.gz$/);
+
+ const backup = firstIndex(calls, "ps -q");
+ const redis = firstIndex(
+ calls,
+ "service update --detach --update-order stop-first --update-failure-action rollback --image redis:8.10.2 dokploy-redis",
+ );
+ const traefikStop = firstIndex(calls, "stop dokploy-traefik");
+ const traefikRun = firstIndex(calls, "run -d");
+ const postgres = firstIndex(
+ calls,
+ "service update --detach --update-order stop-first --update-failure-action rollback --image postgres:18.6 dokploy-postgres",
+ );
+ const panel = firstIndex(calls, "buildx imagetools inspect");
+ const lastPull = Math.max(
+ ...calls.map((call, index) => (call.startsWith("pull ") ? index : -1)),
+ );
+
+ for (const step of [
+ backup,
+ redis,
+ traefikStop,
+ traefikRun,
+ postgres,
+ panel,
+ ]) {
+ expect(step).toBeGreaterThanOrEqual(0);
+ }
+ expect(lastPull).toBeLessThan(backup);
+ expect(backup).toBeLessThan(redis);
+ expect(redis).toBeLessThan(traefikStop);
+ expect(traefikStop).toBeLessThan(traefikRun);
+ expect(traefikRun).toBeLessThan(postgres);
+ expect(postgres).toBeLessThan(panel);
+ },
+ SPAWN_TEST_TIMEOUT_MS,
+ );
+
+ it(
+ "stops before any swap when the Postgres backup fails",
+ () => {
+ const { result, calls } = runInstaller({
+ postgresImage: "postgres:18.5",
+ redisImage: "redis:8.10.1",
+ dumpFails: true,
+ });
+
+ expect(result.status).not.toBe(0);
+ expect(result.stderr).toContain("Nothing was changed");
+ expect(calls.some((call) => call.startsWith("service update"))).toBe(
+ false,
+ );
+ },
+ SPAWN_TEST_TIMEOUT_MS,
+ );
+
+ it(
+ "stops the update when swarm rolls Redis back",
+ () => {
+ const { result, calls } = runInstaller({
+ redisImage: "redis:8.10.1",
+ postgresImage: "postgres:18.5",
+ updateState: "rollback_completed",
+ });
+
+ expect(result.status).not.toBe(0);
+ expect(result.stderr).toContain("swarm state: rollback_completed");
+ expect(
+ calls.some((call) => call.startsWith("stop dokploy-traefik")),
+ ).toBe(false);
+ expect(
+ calls.some(
+ (call) =>
+ call.startsWith("service update") &&
+ call.includes("dokploy-postgres"),
+ ),
+ ).toBe(false);
+ },
+ SPAWN_TEST_TIMEOUT_MS,
+ );
+
+ it(
+ "restores the previous Traefik container when the new one is not running",
+ () => {
+ const { result, calls } = runInstaller({
+ traefikImage: "traefik:v3.6.25",
+ traefikRunning: "false",
+ });
+
+ expect(result.status).not.toBe(0);
+ expect(result.stderr).toContain("restoring the previous container");
+ expect(calls).toContain(
+ "rename dokploy-traefik-previous dokploy-traefik",
+ );
+ expect(calls.at(-1)).toBe("start dokploy-traefik");
+ expect(calls.some((call) => call.startsWith("service update"))).toBe(
+ false,
+ );
+ },
+ SPAWN_TEST_TIMEOUT_MS,
+ );
+});
diff --git a/install-agenthits.sh b/install-agenthits.sh
index 6a88ecc8567..50849eb860c 100755
--- a/install-agenthits.sh
+++ b/install-agenthits.sh
@@ -34,11 +34,23 @@ PASSTHROUGH_VARS=(
HARDEN_SSH
HARDEN_UFW
HARDEN_FAIL2BAN
+ DOCKER_ENGINE_UPGRADE
+ AGENTHITS_BACKUP_DIR
+ AGENTHITS_HEALTH_TIMEOUT
+ AGENTHITS_HEALTH_INTERVAL
+ AGENTHITS_TRAEFIK_SETTLE
+ AGENTHITS_PULL_RETRY_DELAY
)
ORB=""
HARDEN_UFW="${HARDEN_UFW:-0}"
HARDEN_SSH="${HARDEN_SSH:-0}"
HARDEN_FAIL2BAN="${HARDEN_FAIL2BAN:-0}"
+DOCKER_ENGINE_UPGRADE="${DOCKER_ENGINE_UPGRADE:-0}"
+AGENTHITS_BACKUP_DIR="${AGENTHITS_BACKUP_DIR:-/var/backups/agenthits}"
+AGENTHITS_HEALTH_TIMEOUT="${AGENTHITS_HEALTH_TIMEOUT:-240}"
+AGENTHITS_HEALTH_INTERVAL="${AGENTHITS_HEALTH_INTERVAL:-3}"
+AGENTHITS_TRAEFIK_SETTLE="${AGENTHITS_TRAEFIK_SETTLE:-10}"
+AGENTHITS_PULL_RETRY_DELAY="${AGENTHITS_PULL_RETRY_DELAY:-10}"
command_exists() {
command -v "$@" >/dev/null 2>&1
@@ -941,18 +953,7 @@ install_agenthits_dokploy() {
-e DEPLOYMENTS_SIGNING_KEY_FILE=/run/secrets/dokploy_deployments_signing_key \
"$DOKPLOY_IMAGE"
- docker run -d \
- --name dokploy-traefik \
- --restart always \
- -v /etc/dokploy/traefik/traefik.yml:/etc/traefik/traefik.yml \
- -v /etc/dokploy/traefik/dynamic:/etc/dokploy/traefik/dynamic \
- -v /var/run/docker.sock:/var/run/docker.sock:ro \
- -p 80:80/tcp \
- -p 443:443/tcp \
- -p 443:443/udp \
- "$TRAEFIK_IMAGE"
-
- docker network connect dokploy-network dokploy-traefik
+ run_dokploy_traefik "$TRAEFIK_IMAGE"
apply_hardening
@@ -967,26 +968,338 @@ install_agenthits_dokploy() {
echo "http://${formatted_addr}:3000"
}
-update_agenthits_dokploy() {
+download_with_retry() {
+ local attempt=1
+ while [ "$attempt" -le 3 ]; do
+ if "$@"; then
+ return 0
+ fi
+ if [ "$attempt" -lt 3 ]; then
+ sleep "$AGENTHITS_PULL_RETRY_DELAY"
+ fi
+ attempt=$((attempt + 1))
+ done
+ return 1
+}
+
+# Docker's packages are pinned by version string; the epoch and the
+# distribution suffix come from the repository.
+docker_package_version() {
+ apt-cache madison docker-ce | awk -F'|' '{ gsub(/ /, "", $2); print $2 }' | grep -m1 -E "^([0-9]+:)?${DOCKER_VERSION//./\\.}-" || true
+}
+
+docker_engine_version() {
+ docker version --format '{{.Server.Version}}' 2>/dev/null || true
+}
+
+predownload_docker_packages() {
+ if ! command_exists apt-get; then
+ echo "Pre-download failed: DOCKER_ENGINE_UPGRADE=1 needs apt-get on this host. Nothing was changed." >&2
+ exit 1
+ fi
+ if ! download_with_retry apt-get update -qq; then
+ echo "Pre-download failed: the apt package lists could not be refreshed. Nothing was changed." >&2
+ exit 1
+ fi
+ DOCKER_PACKAGE_VERSION="$(docker_package_version)"
+ if [ -z "$DOCKER_PACKAGE_VERSION" ]; then
+ echo "Pre-download failed: Docker $DOCKER_VERSION is not in the apt sources. Nothing was changed." >&2
+ exit 1
+ fi
+ if ! download_with_retry apt-get install -y -qq --download-only "docker-ce=$DOCKER_PACKAGE_VERSION" "docker-ce-cli=$DOCKER_PACKAGE_VERSION" containerd.io docker-buildx-plugin docker-compose-plugin; then
+ echo "Pre-download failed: the Docker packages. Nothing was changed." >&2
+ exit 1
+ fi
+}
+
+# update.sh runs from this checkout when it is present; otherwise it is
+# downloaded here, so the panel step cannot fail on a download mid-update.
+resolve_panel_update_script() {
local script_dir=""
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)"
-
if [ -f "$script_dir/update.sh" ]; then
- bash "$script_dir/update.sh"
- return
+ PANEL_UPDATE_SCRIPT="$script_dir/update.sh"
+ PANEL_UPDATE_IS_TEMP=0
+ return 0
+ fi
+ PANEL_UPDATE_SCRIPT="$(mktemp)"
+ PANEL_UPDATE_IS_TEMP=1
+ if ! download_with_retry curl -fsSL "$AGENTHITS_SCRIPT_BASE_URL/update.sh" -o "$PANEL_UPDATE_SCRIPT"; then
+ rm -f "$PANEL_UPDATE_SCRIPT"
+ echo "Pre-download failed: update.sh. Nothing was changed." >&2
+ exit 1
+ fi
+}
+
+# The one pre-download gate of the operator update. Every image and package
+# the update needs is local before any service, container or daemon is touched.
+predownload_update_artifacts() {
+ resolve_panel_update_script
+ local image=""
+ for image in "$POSTGRES_IMAGE" "$REDIS_IMAGE" "$TRAEFIK_IMAGE" "$DOKPLOY_IMAGE"; do
+ if ! download_with_retry docker pull "$image"; then
+ echo "Pre-download failed: $image. Nothing was changed." >&2
+ exit 1
+ fi
+ done
+ DOCKER_ENGINE_UPGRADE_NEEDED=0
+ if [ "$DOCKER_ENGINE_UPGRADE" = "1" ] && [ "$(docker_engine_version)" != "$DOCKER_VERSION" ]; then
+ DOCKER_ENGINE_UPGRADE_NEEDED=1
+ predownload_docker_packages
+ fi
+}
+
+image_without_digest() {
+ echo "${1%%@*}"
+}
+
+service_image() {
+ docker service inspect "$1" --format '{{.Spec.TaskTemplate.ContainerSpec.Image}}' 2>/dev/null || true
+}
+
+# The running task container of a swarm service; with an image, only one that runs it.
+task_container() {
+ local service="$1"
+ local image="${2:-}"
+ if [ -n "$image" ]; then
+ docker ps -q --no-trunc --filter "label=com.docker.swarm.service.name=$service" --filter status=running --filter "ancestor=$(image_without_digest "$image")" | head -n1 || true
+ else
+ docker ps -q --no-trunc --filter "label=com.docker.swarm.service.name=$service" --filter status=running | head -n1 || true
fi
+}
+
+redis_ready() {
+ local container=""
+ container="$(task_container dokploy-redis "$REDIS_IMAGE")"
+ [ -n "$container" ] && [ "$(docker exec "$container" redis-cli ping 2>/dev/null)" = "PONG" ]
+}
+
+postgres_ready() {
+ local container=""
+ container="$(task_container dokploy-postgres "$POSTGRES_IMAGE")"
+ [ -n "$container" ] && docker exec "$container" pg_isready -U dokploy -d dokploy >/dev/null 2>&1
+}
- # Not a RETURN trap: it stays set after this function and fires again
- # when main returns, where update_script is unbound under set -u.
- local update_script=""
- update_script="$(mktemp)"
- curl -fsSL "$AGENTHITS_SCRIPT_BASE_URL/update.sh" -o "$update_script"
+panel_ready() {
+ local container=""
+ container="$(task_container dokploy)"
+ [ -n "$container" ] && [ "$(docker inspect --format '{{.State.Health.Status}}' "$container" 2>/dev/null)" = "healthy" ]
+}
+
+wait_until_ready() {
+ local check="$1"
+ local deadline=$(($(date +%s) + AGENTHITS_HEALTH_TIMEOUT))
+ until "$check"; do
+ if [ "$(date +%s)" -ge "$deadline" ]; then
+ return 1
+ fi
+ sleep "$AGENTHITS_HEALTH_INTERVAL"
+ done
+}
+
+# Swarm reports a failed update as paused or rolled back. Those states are read
+# only once the update is registered (Version.Index has moved), so a state left
+# over from an earlier update is not taken for this one.
+swap_swarm_service() {
+ local service="$1"
+ local image="$2"
+ local ready="$3"
+ local previous_image=""
+ local previous_index=""
+ local index=""
+ local state=""
+ previous_image="$(service_image "$service")"
+ previous_index="$(docker service inspect "$service" --format '{{.Version.Index}}' 2>/dev/null || true)"
+
+ if ! docker service update --detach --update-order stop-first --update-failure-action rollback --image "$image" "$service" >/dev/null; then
+ echo "Error: docker service update was refused for $service; it was not changed." >&2
+ exit 1
+ fi
+
+ local deadline=$(($(date +%s) + AGENTHITS_HEALTH_TIMEOUT))
+ while :; do
+ index="$(docker service inspect "$service" --format '{{.Version.Index}}' 2>/dev/null || true)"
+ state="$(docker service inspect "$service" --format '{{.UpdateStatus.State}}' 2>/dev/null || true)"
+ if [ "${index:-0}" -gt "${previous_index:-0}" ]; then
+ case "$state" in
+ rollback_*)
+ echo "Error: $service did not start on $image (swarm state: $state). Previous image: $previous_image. Check with: docker service ps $service --no-trunc" >&2
+ exit 1
+ ;;
+ paused)
+ echo "Error: $service is paused after its update to $image (swarm state: paused). Previous image: $previous_image. Roll back with: docker service update --image $previous_image $service" >&2
+ exit 1
+ ;;
+ esac
+ if [ "$state" = "completed" ] && "$ready"; then
+ echo "$service runs $image (previous image kept: $previous_image)"
+ return 0
+ fi
+ fi
+ if [ "$(date +%s)" -ge "$deadline" ]; then
+ echo "Error: $service was not ready on $image within $AGENTHITS_HEALTH_TIMEOUT seconds. Roll back with: docker service update --image $previous_image $service" >&2
+ exit 1
+ fi
+ sleep "$AGENTHITS_HEALTH_INTERVAL"
+ done
+}
+
+backup_postgres() {
+ local container=""
+ local file=""
+ container="$(task_container dokploy-postgres)"
+ if [ -z "$container" ]; then
+ echo "Error: no running dokploy-postgres task to back up. Nothing was changed." >&2
+ exit 1
+ fi
+ mkdir -p "$AGENTHITS_BACKUP_DIR"
+ file="$AGENTHITS_BACKUP_DIR/postgres-$(date -u +%Y%m%dT%H%M%SZ).sql.gz"
+ if ! docker exec "$container" pg_dumpall -U dokploy | gzip >"$file" || [ ! -s "$file" ]; then
+ rm -f "$file"
+ echo "Error: the Postgres backup failed. Nothing was changed." >&2
+ exit 1
+ fi
+ echo "Postgres backup written to $file"
+}
+
+run_dokploy_traefik() {
+ docker run -d \
+ --name dokploy-traefik \
+ --restart always \
+ -v /etc/dokploy/traefik/traefik.yml:/etc/traefik/traefik.yml \
+ -v /etc/dokploy/traefik/dynamic:/etc/dokploy/traefik/dynamic \
+ -v /var/run/docker.sock:/var/run/docker.sock:ro \
+ -p 80:80/tcp \
+ -p 443:443/tcp \
+ -p 443:443/udp \
+ "$1"
+
+ docker network connect dokploy-network dokploy-traefik
+}
+
+TRAEFIK_TARGET_IMAGE=""
+TRAEFIK_PREVIOUS_KEPT=0
+TRAEFIK_SWAPPED=0
+
+# Ports 80 and 443 are free only after the old container stops, so the old one
+# is put back when the new one does not come up.
+restore_dokploy_traefik() {
+ trap - ERR HUP INT TERM
+ if [ "$TRAEFIK_SWAPPED" = "0" ]; then
+ echo "Error: Traefik did not start on $TRAEFIK_TARGET_IMAGE; restoring the previous container." >&2
+ if [ "$TRAEFIK_PREVIOUS_KEPT" = "1" ]; then
+ docker rm -f dokploy-traefik >/dev/null 2>&1 || true
+ docker rename dokploy-traefik-previous dokploy-traefik || true
+ docker update --restart always dokploy-traefik >/dev/null 2>&1 || true
+ fi
+ docker start dokploy-traefik >/dev/null 2>&1 || true
+ fi
+ exit 1
+}
+
+swap_dokploy_traefik() {
+ local current=""
+ current="$(docker inspect --format '{{.Config.Image}}' dokploy-traefik 2>/dev/null || true)"
+ if [ "$(image_without_digest "$current")" = "$(image_without_digest "$TRAEFIK_IMAGE")" ]; then
+ echo "Traefik already runs $TRAEFIK_IMAGE"
+ return 0
+ fi
+
+ TRAEFIK_TARGET_IMAGE="$TRAEFIK_IMAGE"
+ TRAEFIK_PREVIOUS_KEPT=0
+ TRAEFIK_SWAPPED=0
+ # errtrace makes the ERR trap fire for a failed docker run inside run_dokploy_traefik.
+ set -o errtrace
+ trap restore_dokploy_traefik ERR HUP INT TERM
+
+ if docker inspect dokploy-traefik >/dev/null 2>&1; then
+ docker stop dokploy-traefik >/dev/null
+ docker rm -f dokploy-traefik-previous >/dev/null 2>&1 || true
+ docker rename dokploy-traefik dokploy-traefik-previous
+ TRAEFIK_PREVIOUS_KEPT=1
+ # A stopped container with restart=always is started again when the daemon restarts.
+ docker update --restart no dokploy-traefik-previous >/dev/null
+ fi
+
+ run_dokploy_traefik "$TRAEFIK_IMAGE"
+ sleep "$AGENTHITS_TRAEFIK_SETTLE"
+ if [ "$(docker inspect --format '{{.State.Running}}' dokploy-traefik 2>/dev/null)" != "true" ]; then
+ restore_dokploy_traefik
+ fi
+
+ TRAEFIK_SWAPPED=1
+ trap - ERR HUP INT TERM
+ echo "Traefik runs $TRAEFIK_IMAGE (previous container kept stopped as dokploy-traefik-previous)"
+}
+
+require_update_services() {
+ local service=""
+ for service in dokploy dokploy-postgres dokploy-redis; do
+ if ! docker service inspect "$service" >/dev/null 2>&1; then
+ echo "Error: Docker service '$service' was not found. Run install first." >&2
+ exit 1
+ fi
+ done
+ if ! docker inspect dokploy-traefik >/dev/null 2>&1; then
+ echo "Error: container 'dokploy-traefik' was not found. Run install first." >&2
+ exit 1
+ fi
+}
+
+run_panel_update() {
local status=0
- bash "$update_script" || status=$?
- rm -f "$update_script"
+ bash "$PANEL_UPDATE_SCRIPT" || status=$?
+ if [ "$PANEL_UPDATE_IS_TEMP" = "1" ]; then
+ rm -f "$PANEL_UPDATE_SCRIPT"
+ fi
return "$status"
}
+# Docker Engine restarts dockerd, and live-restore does not cover swarm services,
+# so every service on the host restarts here. It runs last, on pre-downloaded packages.
+upgrade_docker_engine() {
+ if [ "$DOCKER_ENGINE_UPGRADE_NEEDED" != "1" ]; then
+ return 0
+ fi
+ echo "Upgrading Docker Engine to $DOCKER_VERSION; the daemon restarts and the swarm services on this host restart with it."
+ if ! apt-get install -y -qq --no-download "docker-ce=$DOCKER_PACKAGE_VERSION" "docker-ce-cli=$DOCKER_PACKAGE_VERSION" containerd.io docker-buildx-plugin docker-compose-plugin; then
+ echo "Error: installing the pre-downloaded Docker packages failed. Check with: apt-get install -f" >&2
+ exit 1
+ fi
+ ensure_docker_running
+ if ! wait_until_ready panel_ready; then
+ echo "Error: the panel was not healthy after the Docker restart. See: docker service ps dokploy --no-trunc" >&2
+ exit 1
+ fi
+ echo "Docker Engine is $DOCKER_VERSION"
+}
+
+# Order: Redis, Traefik, Postgres, then the panel. A failed Redis or Traefik swap
+# stops before the database changes. Postgres follows its backup and comes
+# before the panel, because the new panel migrates the database when it starts.
+# The panel is last, so its dependencies are already on their pinned versions
+# and healthy when its rollback-protected update runs.
+update_agenthits_dokploy() {
+ require_update_services
+ predownload_update_artifacts
+
+ local postgres_swap=0
+ if [ "$(image_without_digest "$(service_image dokploy-postgres)")" != "$(image_without_digest "$POSTGRES_IMAGE")" ]; then
+ postgres_swap=1
+ backup_postgres
+ fi
+ if [ "$(image_without_digest "$(service_image dokploy-redis)")" != "$(image_without_digest "$REDIS_IMAGE")" ]; then
+ swap_swarm_service dokploy-redis "$REDIS_IMAGE" redis_ready
+ fi
+ swap_dokploy_traefik
+ if [ "$postgres_swap" = "1" ]; then
+ swap_swarm_service dokploy-postgres "$POSTGRES_IMAGE" postgres_ready
+ fi
+ run_panel_update
+ upgrade_docker_engine
+ echo "AgentHits operator update finished."
+}
+
parse_hardening_flags() {
local arg=""
for arg in "$@"; do
From a364baa7c02e6680a74ecec765f5a445f2aae79e Mon Sep 17 00:00:00 2001
From: merge-check <140916359+agentHits@users.noreply.github.com>
Date: Thu, 8 Oct 2026 09:05:23 -0400
Subject: [PATCH 10/33] docs: describe the update order, rollback and backups
agenthits-install.md now lists the operator update steps in order, the
pre-download gate, the Postgres backup location, the Traefik keep-until-
verified swap, the optional Docker Engine upgrade and the manual rollback
commands. server-components-update.md states the pre-download gate, the
Traefik restore behaviour, why live-restore is not used for swarm services,
and that pre-downloading Docker packages needs apt. server-hardening.md
notes that the update path leaves SSH, UFW and Fail2Ban unchanged.
Co-Authored-By: Claude Haiku 5.5
---
docs/agenthits-install.md | 35 ++++++++++++++++++++++++++++++++
docs/server-components-update.md | 9 ++++----
docs/server-hardening.md | 2 ++
3 files changed, 42 insertions(+), 4 deletions(-)
diff --git a/docs/agenthits-install.md b/docs/agenthits-install.md
index d1765733d0a..7b36580e169 100644
--- a/docs/agenthits-install.md
+++ b/docs/agenthits-install.md
@@ -228,6 +228,41 @@ Windows (PowerShell от администратора):
`update.sh`. Если установлен тот же image digest, команда завершается без
повторного `docker pull` и без перезапуска `dokploy`.
+`update` обновляет весь стек панели, а не только её образ. Сначала скачивается
+всё, что понадобится: образы Postgres, Redis, Traefik и панели (закреплённые
+теги) и `update.sh`. Если что-то не скачалось, команда завершается строкой
+`Pre-download failed: ... Nothing was changed.`, и ни один сервис не трогается.
+Затем шаги идут в таком порядке:
+
+1. Резервная копия Postgres: `pg_dumpall` в `AGENTHITS_BACKUP_DIR` (по умолчанию
+ `/var/backups/agenthits`). Файл остаётся на хосте и не удаляется
+ автоматически. Делается, только если Postgres будет заменён, до любой замены.
+2. Redis: `docker service update` с остановкой старой задачи до запуска новой и
+ откатом при сбое. Ждёт ответа `PONG`.
+3. Traefik: старый контейнер останавливается и переименовывается в
+ `dokploy-traefik-previous`, новый запускается и подключается к
+ `dokploy-network`. Через `AGENTHITS_TRAEFIK_SETTLE` секунд (10 по умолчанию)
+ проверяется, что он работает; если нет, старый контейнер возвращается.
+ Старый контейнер остаётся остановленным, с `--restart no`, чтобы перезапуск
+ демона не запустил его рядом с новым. При следующем обновлении Traefik он
+ заменяется.
+4. Postgres: `docker service update` с остановкой старой задачи до запуска новой,
+ потому что две задачи не могут работать с одним volume. Ждёт `pg_isready`.
+5. Панель: `update.sh`. Ждёт `healthy`-статуса контейнера; при откате swarm или
+ нездоровом статусе команда завершается с ошибкой.
+6. Docker Engine: только при `DOCKER_ENGINE_UPGRADE=1` и если установленная
+ версия отличается от `DOCKER_VERSION`. Пакеты скачиваются заранее, установка
+ идёт в самом конце. Перезапуск демона перезапускает все сервисы swarm на
+ хосте: live-restore к сервисам swarm не применяется.
+
+Простой на каждом шаге равен времени остановки старой задачи (или контейнера) и
+запуска новой: образы уже локальные, поэтому скачивание в простое не участвует.
+Единственный шаг, который перезапускает демон Docker, — шаг 6.
+
+Ручной откат. Для сервисов swarm: `docker service update --image <предыдущий образ> <сервис>`,
+предыдущий образ печатается в логе шага. Для Traefik:
+`docker rm -f dokploy-traefik && docker rename dokploy-traefik-previous dokploy-traefik && docker update --restart always dokploy-traefik && docker start dokploy-traefik`.
+
В dashboard кнопка `Check for updates` для AgentHits fork сравнивает текущий
Docker service image digest с `ghcr.io/agenthits/dokploy:agenthits-dev`.
Если digest отличается, обновление через UI запускает `docker service update`
diff --git a/docs/server-components-update.md b/docs/server-components-update.md
index 6e1e6c2d2b7..993a606b279 100644
--- a/docs/server-components-update.md
+++ b/docs/server-components-update.md
@@ -28,10 +28,10 @@ Postgres и Redis относятся к базе панели. На сервер
## Что делает кнопка обновления
-Кнопка **Components** на карточке сервера сравнивает установленные версии с закреплёнными и обновляет отличающиеся:
+Кнопка **Components** на карточке сервера сравнивает установленные версии с закреплёнными и обновляет отличающиеся. Сначала скачивается всё, что нужно выбранным компонентам: образ Traefik, скрипт get.docker.com и (на хостах с apt) пакеты Docker. Если какая-либо загрузка не удалась, команда завершается с сообщением `Nothing was changed`, и ни один сервис не трогается:
-- Docker: переустанавливается закреплённая версия через get.docker.com. Демон Docker перезапускается, контейнеры с политикой перезапуска перезапускаются, сервисы swarm недоступны несколько секунд.
-- Traefik: пересоздаётся контейнер `dokploy-traefik`. Сервисы на сервере недоступны несколько секунд.
+- Docker: пакеты закреплённой версии уже скачаны, затем она устанавливается через get.docker.com. Демон Docker перезапускается, и сервисы swarm на сервере перезапускаются вместе с ним. Live-restore это не предотвращает: по документации Docker он относится только к обычным контейнерам, а не к сервисам swarm, поэтому `daemon.json` не меняется. На хостах без apt пакеты загружает менеджер пакетов во время шага Docker.
+- Traefik: образ скачивается до первого шага. Старый контейнер `dokploy-traefik` останавливается и переименовывается в `dokploy-traefik-previous`, новый запускается и подключается к сети `dokploy-network`. Если он не работает через 10 секунд (`TRAEFIK_SETTLE_SECONDS`), старый контейнер возвращается и команда завершается с ошибкой. Порты 80 и 443 освобождаются только после остановки старого контейнера, поэтому простой примерно равен времени запуска нового. После успеха старый контейнер удаляется.
- RClone, Nixpacks, Railpack, Buildpacks: переустанавливаются закреплённые версии, работающие сервисы не трогаются.
Установка сервера (Setup Server) уже установленное не обновляет. Поэтому после смены версии нужна именно эта кнопка.
@@ -52,4 +52,5 @@ Postgres и Redis относятся к базе панели. На сервер
- Railpack 0.15.4 → 0.40.1 и pack 0.39.1 → 0.40.9 — большие скачки. Перед широким выкатом проверьте сборку реального приложения.
- Версия Railpack у каждого приложения хранится в базе (колонка `railpackVersion`). Новое значение по умолчанию — 0.40.1 (миграция `0207_railpack_default_0_40_1`). Существующие приложения сохраняют свою версию, миграция её не меняет.
- Версии Dokploy (`DOKPLOY_OFFICIAL_VERSION`, сейчас v0.29.8) и npm-зависимости в этот список не входят.
-- Обновление Traefik, Docker и установка в контейнере проверены только тестами и в контейнере Ubuntu. Полный прогон с приложениями на живом сервере не выполнялся.
+- Обновление Traefik и Docker проверено тестами с подменёнными командами docker, apt-get, apt-cache и curl. Полный прогон с приложениями на живом сервере не выполнялся.
+- Предварительная загрузка пакетов Docker работает только на хостах с apt. Если в apt-репозиториях нет закреплённой версии Docker, обновление останавливается до любых изменений.
diff --git a/docs/server-hardening.md b/docs/server-hardening.md
index 372628d1883..cce63f414de 100644
--- a/docs/server-hardening.md
+++ b/docs/server-hardening.md
@@ -14,6 +14,8 @@
Версия Traefik для серверов берётся из установщика: `TRAEFIK_IMAGE`, сейчас `traefik:v3.7.14`. Тест `apps/dokploy/__test__/server/component-versions-sync.test.ts` падает, если версия по умолчанию в панели расходится с установщиком.
+Флаги усиления принимают только `install` и `harden`. `install-agenthits.sh update` обновляет панель и её стек, но не меняет SSH, UFW и Fail2Ban.
+
## Как запускать
### macOS (панель установлена в OrbStack)
From ca366f367972774f8a4154fa9abdd2b0219a3fde Mon Sep 17 00:00:00 2001
From: merge-check <140916359+agentHits@users.noreply.github.com>
Date: Thu, 8 Oct 2026 09:06:53 -0400
Subject: [PATCH 11/33] refactor(update): name the new operator settings after
the panel
The settings added for the operator update (backup directory, health timeout
and interval, Traefik settle time) now use the DOKPLOY_ prefix the installer
already uses for its own settings. Their messages no longer carry the fork
name, and the default backup directory is /var/backups/dokploy.
Co-Authored-By: Claude Haiku 5.5
---
.../settings/agenthits-update-script.test.ts | 4 +--
.../settings/install-agenthits-update.test.ts | 10 +++---
docs/agenthits-install.md | 6 ++--
install-agenthits.sh | 34 +++++++++----------
update.sh | 12 +++----
5 files changed, 33 insertions(+), 33 deletions(-)
diff --git a/apps/dokploy/__test__/settings/agenthits-update-script.test.ts b/apps/dokploy/__test__/settings/agenthits-update-script.test.ts
index ea7bfb54a2b..e11c33e0d9d 100644
--- a/apps/dokploy/__test__/settings/agenthits-update-script.test.ts
+++ b/apps/dokploy/__test__/settings/agenthits-update-script.test.ts
@@ -167,8 +167,8 @@ const runUpdateScript = (
...process.env,
AGENTHITS_SKIP_HOST_CHECK: "1",
AGENTHITS_PULL_RETRY_DELAY: "0",
- AGENTHITS_HEALTH_INTERVAL: "0",
- AGENTHITS_HEALTH_TIMEOUT: "2",
+ DOKPLOY_HEALTH_INTERVAL: "0",
+ DOKPLOY_HEALTH_TIMEOUT: "2",
DOCKER_CALL_LOG: callLog,
DOCKER_INDEX_FILE: indexFile,
PATH: `${tempDir}:${process.env.PATH}`,
diff --git a/apps/dokploy/__test__/settings/install-agenthits-update.test.ts b/apps/dokploy/__test__/settings/install-agenthits-update.test.ts
index 42c335d9d62..e1bea5ef832 100644
--- a/apps/dokploy/__test__/settings/install-agenthits-update.test.ts
+++ b/apps/dokploy/__test__/settings/install-agenthits-update.test.ts
@@ -137,7 +137,7 @@ type OperatorScenario = {
};
const runInstaller = (scenario: OperatorScenario = {}) => {
- const dir = mkdtempSync(path.join(tmpdir(), "agenthits-operator-update-"));
+ const dir = mkdtempSync(path.join(tmpdir(), "dokploy-operator-update-"));
try {
const callLog = path.join(dir, "docker-calls.log");
const indexFile = path.join(dir, "service-index");
@@ -164,11 +164,11 @@ const runInstaller = (scenario: OperatorScenario = {}) => {
DOCKER_CALL_LOG: callLog,
DOCKER_INDEX_FILE: indexFile,
AGENTHITS_SKIP_HOST_CHECK: "1",
- AGENTHITS_BACKUP_DIR: backupDir,
+ DOKPLOY_BACKUP_DIR: backupDir,
AGENTHITS_PULL_RETRY_DELAY: "0",
- AGENTHITS_HEALTH_INTERVAL: "0",
- AGENTHITS_HEALTH_TIMEOUT: "2",
- AGENTHITS_TRAEFIK_SETTLE: "0",
+ DOKPLOY_HEALTH_INTERVAL: "0",
+ DOKPLOY_HEALTH_TIMEOUT: "2",
+ DOKPLOY_TRAEFIK_SETTLE: "0",
REDIS_IMAGE: "redis:8.10.2",
POSTGRES_IMAGE: "postgres:18.6",
TRAEFIK_IMAGE: "traefik:v3.7.14",
diff --git a/docs/agenthits-install.md b/docs/agenthits-install.md
index 7b36580e169..9fb57fbbe3b 100644
--- a/docs/agenthits-install.md
+++ b/docs/agenthits-install.md
@@ -234,14 +234,14 @@ Windows (PowerShell от администратора):
`Pre-download failed: ... Nothing was changed.`, и ни один сервис не трогается.
Затем шаги идут в таком порядке:
-1. Резервная копия Postgres: `pg_dumpall` в `AGENTHITS_BACKUP_DIR` (по умолчанию
- `/var/backups/agenthits`). Файл остаётся на хосте и не удаляется
+1. Резервная копия Postgres: `pg_dumpall` в `DOKPLOY_BACKUP_DIR` (по умолчанию
+ `/var/backups/dokploy`). Файл остаётся на хосте и не удаляется
автоматически. Делается, только если Postgres будет заменён, до любой замены.
2. Redis: `docker service update` с остановкой старой задачи до запуска новой и
откатом при сбое. Ждёт ответа `PONG`.
3. Traefik: старый контейнер останавливается и переименовывается в
`dokploy-traefik-previous`, новый запускается и подключается к
- `dokploy-network`. Через `AGENTHITS_TRAEFIK_SETTLE` секунд (10 по умолчанию)
+ `dokploy-network`. Через `DOKPLOY_TRAEFIK_SETTLE` секунд (10 по умолчанию)
проверяется, что он работает; если нет, старый контейнер возвращается.
Старый контейнер остаётся остановленным, с `--restart no`, чтобы перезапуск
демона не запустил его рядом с новым. При следующем обновлении Traefik он
diff --git a/install-agenthits.sh b/install-agenthits.sh
index 50849eb860c..7552bd55616 100755
--- a/install-agenthits.sh
+++ b/install-agenthits.sh
@@ -35,10 +35,10 @@ PASSTHROUGH_VARS=(
HARDEN_UFW
HARDEN_FAIL2BAN
DOCKER_ENGINE_UPGRADE
- AGENTHITS_BACKUP_DIR
- AGENTHITS_HEALTH_TIMEOUT
- AGENTHITS_HEALTH_INTERVAL
- AGENTHITS_TRAEFIK_SETTLE
+ DOKPLOY_BACKUP_DIR
+ DOKPLOY_HEALTH_TIMEOUT
+ DOKPLOY_HEALTH_INTERVAL
+ DOKPLOY_TRAEFIK_SETTLE
AGENTHITS_PULL_RETRY_DELAY
)
ORB=""
@@ -46,10 +46,10 @@ HARDEN_UFW="${HARDEN_UFW:-0}"
HARDEN_SSH="${HARDEN_SSH:-0}"
HARDEN_FAIL2BAN="${HARDEN_FAIL2BAN:-0}"
DOCKER_ENGINE_UPGRADE="${DOCKER_ENGINE_UPGRADE:-0}"
-AGENTHITS_BACKUP_DIR="${AGENTHITS_BACKUP_DIR:-/var/backups/agenthits}"
-AGENTHITS_HEALTH_TIMEOUT="${AGENTHITS_HEALTH_TIMEOUT:-240}"
-AGENTHITS_HEALTH_INTERVAL="${AGENTHITS_HEALTH_INTERVAL:-3}"
-AGENTHITS_TRAEFIK_SETTLE="${AGENTHITS_TRAEFIK_SETTLE:-10}"
+DOKPLOY_BACKUP_DIR="${DOKPLOY_BACKUP_DIR:-/var/backups/dokploy}"
+DOKPLOY_HEALTH_TIMEOUT="${DOKPLOY_HEALTH_TIMEOUT:-240}"
+DOKPLOY_HEALTH_INTERVAL="${DOKPLOY_HEALTH_INTERVAL:-3}"
+DOKPLOY_TRAEFIK_SETTLE="${DOKPLOY_TRAEFIK_SETTLE:-10}"
AGENTHITS_PULL_RETRY_DELAY="${AGENTHITS_PULL_RETRY_DELAY:-10}"
command_exists() {
@@ -1088,12 +1088,12 @@ panel_ready() {
wait_until_ready() {
local check="$1"
- local deadline=$(($(date +%s) + AGENTHITS_HEALTH_TIMEOUT))
+ local deadline=$(($(date +%s) + DOKPLOY_HEALTH_TIMEOUT))
until "$check"; do
if [ "$(date +%s)" -ge "$deadline" ]; then
return 1
fi
- sleep "$AGENTHITS_HEALTH_INTERVAL"
+ sleep "$DOKPLOY_HEALTH_INTERVAL"
done
}
@@ -1116,7 +1116,7 @@ swap_swarm_service() {
exit 1
fi
- local deadline=$(($(date +%s) + AGENTHITS_HEALTH_TIMEOUT))
+ local deadline=$(($(date +%s) + DOKPLOY_HEALTH_TIMEOUT))
while :; do
index="$(docker service inspect "$service" --format '{{.Version.Index}}' 2>/dev/null || true)"
state="$(docker service inspect "$service" --format '{{.UpdateStatus.State}}' 2>/dev/null || true)"
@@ -1137,10 +1137,10 @@ swap_swarm_service() {
fi
fi
if [ "$(date +%s)" -ge "$deadline" ]; then
- echo "Error: $service was not ready on $image within $AGENTHITS_HEALTH_TIMEOUT seconds. Roll back with: docker service update --image $previous_image $service" >&2
+ echo "Error: $service was not ready on $image within $DOKPLOY_HEALTH_TIMEOUT seconds. Roll back with: docker service update --image $previous_image $service" >&2
exit 1
fi
- sleep "$AGENTHITS_HEALTH_INTERVAL"
+ sleep "$DOKPLOY_HEALTH_INTERVAL"
done
}
@@ -1152,8 +1152,8 @@ backup_postgres() {
echo "Error: no running dokploy-postgres task to back up. Nothing was changed." >&2
exit 1
fi
- mkdir -p "$AGENTHITS_BACKUP_DIR"
- file="$AGENTHITS_BACKUP_DIR/postgres-$(date -u +%Y%m%dT%H%M%SZ).sql.gz"
+ mkdir -p "$DOKPLOY_BACKUP_DIR"
+ file="$DOKPLOY_BACKUP_DIR/postgres-$(date -u +%Y%m%dT%H%M%SZ).sql.gz"
if ! docker exec "$container" pg_dumpall -U dokploy | gzip >"$file" || [ ! -s "$file" ]; then
rm -f "$file"
echo "Error: the Postgres backup failed. Nothing was changed." >&2
@@ -1222,7 +1222,7 @@ swap_dokploy_traefik() {
fi
run_dokploy_traefik "$TRAEFIK_IMAGE"
- sleep "$AGENTHITS_TRAEFIK_SETTLE"
+ sleep "$DOKPLOY_TRAEFIK_SETTLE"
if [ "$(docker inspect --format '{{.State.Running}}' dokploy-traefik 2>/dev/null)" != "true" ]; then
restore_dokploy_traefik
fi
@@ -1297,7 +1297,7 @@ update_agenthits_dokploy() {
fi
run_panel_update
upgrade_docker_engine
- echo "AgentHits operator update finished."
+ echo "Panel host update finished."
}
parse_hardening_flags() {
diff --git a/update.sh b/update.sh
index c0194a4b5bd..85682638e1b 100755
--- a/update.sh
+++ b/update.sh
@@ -7,8 +7,8 @@ DOKPLOY_OFFICIAL_VERSION_OVERRIDE="${DOKPLOY_OFFICIAL_VERSION:-}"
DOKPLOY_FORK_VERSION_OVERRIDE="${DOKPLOY_FORK_VERSION:-}"
DOKPLOY_OFFICIAL_VERSION="${DOKPLOY_OFFICIAL_VERSION_OVERRIDE:-v0.29.8}"
DOKPLOY_FORK_VERSION="${DOKPLOY_FORK_VERSION_OVERRIDE:-}"
-AGENTHITS_HEALTH_TIMEOUT="${AGENTHITS_HEALTH_TIMEOUT:-240}"
-AGENTHITS_HEALTH_INTERVAL="${AGENTHITS_HEALTH_INTERVAL:-3}"
+DOKPLOY_HEALTH_TIMEOUT="${DOKPLOY_HEALTH_TIMEOUT:-240}"
+DOKPLOY_HEALTH_INTERVAL="${DOKPLOY_HEALTH_INTERVAL:-3}"
command_exists() {
command -v "$@" >/dev/null 2>&1
@@ -221,7 +221,7 @@ panel_container_healthy() {
# so the outcome is read from the service instead.
wait_for_panel_update() {
local previous_index="$1"
- local deadline=$(($(date +%s) + AGENTHITS_HEALTH_TIMEOUT))
+ local deadline=$(($(date +%s) + DOKPLOY_HEALTH_TIMEOUT))
local index=""
local state=""
while :; do
@@ -230,7 +230,7 @@ wait_for_panel_update() {
if [ "${index:-0}" -gt "$previous_index" ]; then
case "$state" in
paused | rollback_*)
- echo "Error: the AgentHits Dokploy update did not succeed (swarm state: $state). See: docker service ps dokploy --no-trunc" >&2
+ echo "Error: the Dokploy panel update did not succeed (swarm state: $state). See: docker service ps dokploy --no-trunc" >&2
exit 1
;;
esac
@@ -239,10 +239,10 @@ wait_for_panel_update() {
fi
fi
if [ "$(date +%s)" -ge "$deadline" ]; then
- echo "Error: AgentHits Dokploy was not healthy after ${AGENTHITS_HEALTH_TIMEOUT} seconds (swarm state: ${state:-unknown})." >&2
+ echo "Error: the Dokploy panel was not healthy after ${DOKPLOY_HEALTH_TIMEOUT} seconds (swarm state: ${state:-unknown})." >&2
exit 1
fi
- sleep "$AGENTHITS_HEALTH_INTERVAL"
+ sleep "$DOKPLOY_HEALTH_INTERVAL"
done
}
From 1c67146ffb85324d2ccf962856a101745a5d0653 Mon Sep 17 00:00:00 2001
From: merge-check <140916359+agentHits@users.noreply.github.com>
Date: Thu, 8 Oct 2026 09:09:20 -0400
Subject: [PATCH 12/33] fix(update): keep the Postgres backup readable by root
only
pg_dumpall writes the data and the role password hashes. The backup was
created with the default umask, so the file could be world-readable. The
backup directory is now created 0700 and the dump runs under umask 077, so
the file is 0600. The installer test checks the mode of the file it writes.
Co-Authored-By: Claude Haiku 5.5
---
.../__test__/settings/install-agenthits-update.test.ts | 9 +++++++--
docs/agenthits-install.md | 4 ++--
install-agenthits.sh | 5 +++--
3 files changed, 12 insertions(+), 6 deletions(-)
diff --git a/apps/dokploy/__test__/settings/install-agenthits-update.test.ts b/apps/dokploy/__test__/settings/install-agenthits-update.test.ts
index e1bea5ef832..ec20abbcd92 100644
--- a/apps/dokploy/__test__/settings/install-agenthits-update.test.ts
+++ b/apps/dokploy/__test__/settings/install-agenthits-update.test.ts
@@ -5,6 +5,7 @@ import {
readdirSync,
readFileSync,
rmSync,
+ statSync,
writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
@@ -193,7 +194,10 @@ const runInstaller = (scenario: OperatorScenario = {}) => {
return [];
}
})();
- return { result, calls, backups };
+ const backupModes = backups.map(
+ (name) => statSync(path.join(backupDir, name)).mode & 0o777,
+ );
+ return { result, calls, backups, backupModes };
} finally {
rmSync(dir, { force: true, recursive: true });
}
@@ -245,7 +249,7 @@ describe("install-agenthits.sh update", () => {
it(
"backs up Postgres and swaps Redis, Traefik, Postgres, then the panel, in that order",
() => {
- const { result, calls, backups } = runInstaller({
+ const { result, calls, backups, backupModes } = runInstaller({
redisImage: "redis:8.10.1",
postgresImage: "postgres:18.5",
traefikImage: "traefik:v3.6.25",
@@ -253,6 +257,7 @@ describe("install-agenthits.sh update", () => {
expect(result.status).toBe(0);
expect(backups).toHaveLength(1);
+ expect(backupModes).toEqual([0o600]);
expect(backups[0]).toMatch(/^postgres-.*\.sql\.gz$/);
const backup = firstIndex(calls, "ps -q");
diff --git a/docs/agenthits-install.md b/docs/agenthits-install.md
index 9fb57fbbe3b..b4c935bf174 100644
--- a/docs/agenthits-install.md
+++ b/docs/agenthits-install.md
@@ -235,8 +235,8 @@ Windows (PowerShell от администратора):
Затем шаги идут в таком порядке:
1. Резервная копия Postgres: `pg_dumpall` в `DOKPLOY_BACKUP_DIR` (по умолчанию
- `/var/backups/dokploy`). Файл остаётся на хосте и не удаляется
- автоматически. Делается, только если Postgres будет заменён, до любой замены.
+ `/var/backups/dokploy`). Файл остаётся на хосте, доступен только root (каталог 0700, файл
+ 0600) и не удаляется автоматически. Делается, только если Postgres будет заменён, до любой замены.
2. Redis: `docker service update` с остановкой старой задачи до запуска новой и
откатом при сбое. Ждёт ответа `PONG`.
3. Traefik: старый контейнер останавливается и переименовывается в
diff --git a/install-agenthits.sh b/install-agenthits.sh
index 7552bd55616..7ac8aaf1151 100755
--- a/install-agenthits.sh
+++ b/install-agenthits.sh
@@ -1152,9 +1152,10 @@ backup_postgres() {
echo "Error: no running dokploy-postgres task to back up. Nothing was changed." >&2
exit 1
fi
- mkdir -p "$DOKPLOY_BACKUP_DIR"
+ # The dump holds the data and the role password hashes, so it stays private.
+ mkdir -p -m 700 "$DOKPLOY_BACKUP_DIR"
file="$DOKPLOY_BACKUP_DIR/postgres-$(date -u +%Y%m%dT%H%M%SZ).sql.gz"
- if ! docker exec "$container" pg_dumpall -U dokploy | gzip >"$file" || [ ! -s "$file" ]; then
+ if ! (umask 077 && docker exec "$container" pg_dumpall -U dokploy | gzip >"$file") || [ ! -s "$file" ]; then
rm -f "$file"
echo "Error: the Postgres backup failed. Nothing was changed." >&2
exit 1
From 5f99a67753e345413aff9df49bae6a350f51dafd Mon Sep 17 00:00:00 2001
From: merge-check <140916359+agentHits@users.noreply.github.com>
Date: Thu, 8 Oct 2026 09:10:22 -0400
Subject: [PATCH 13/33] fix(update): refuse a Postgres major version change
before anything runs
The Postgres swap keeps the existing volume mount, so a new major version
does not read the old data directory. The PostgreSQL 18 image would start a
new, empty cluster while the panel reconnects to it. The update now compares
the running and pinned major versions before the pre-download gate and stops
with "Nothing was changed" when they differ. The major version check reuses
get_postgres_major_version, which now takes an optional image argument.
Co-Authored-By: Claude Haiku 5.5
---
.../settings/install-agenthits-update.test.ts | 17 +++++++++++++++++
docs/agenthits-install.md | 4 ++++
install-agenthits.sh | 11 ++++++++++-
3 files changed, 31 insertions(+), 1 deletion(-)
diff --git a/apps/dokploy/__test__/settings/install-agenthits-update.test.ts b/apps/dokploy/__test__/settings/install-agenthits-update.test.ts
index ec20abbcd92..68335c15b97 100644
--- a/apps/dokploy/__test__/settings/install-agenthits-update.test.ts
+++ b/apps/dokploy/__test__/settings/install-agenthits-update.test.ts
@@ -359,4 +359,21 @@ describe("install-agenthits.sh update", () => {
},
SPAWN_TEST_TIMEOUT_MS,
);
+ it(
+ "refuses a Postgres major version change before anything runs",
+ () => {
+ const { result, calls } = runInstaller({
+ postgresImage: "postgres:17.5",
+ });
+
+ expect(result.status).not.toBe(0);
+ expect(result.stderr).toContain("changes the major version");
+ expect(result.stderr).toContain("Nothing was changed");
+ expect(calls.some((call) => call.startsWith("pull "))).toBe(false);
+ expect(calls.some((call) => call.startsWith("service update"))).toBe(
+ false,
+ );
+ },
+ SPAWN_TEST_TIMEOUT_MS,
+ );
});
diff --git a/docs/agenthits-install.md b/docs/agenthits-install.md
index b4c935bf174..a2a62bba027 100644
--- a/docs/agenthits-install.md
+++ b/docs/agenthits-install.md
@@ -255,6 +255,10 @@ Windows (PowerShell от администратора):
идёт в самом конце. Перезапуск демона перезапускает все сервисы swarm на
хосте: live-restore к сервисам swarm не применяется.
+Смена major-версии Postgres (например, 17 → 18) этим обновлением не выполняется:
+команда останавливается до любых изменений. Перенос данных между major-версиями
+делается отдельно (dump и restore или `pg_upgrade`).
+
Простой на каждом шаге равен времени остановки старой задачи (или контейнера) и
запуска новой: образы уже локальные, поэтому скачивание в простое не участвует.
Единственный шаг, который перезапускает демон Docker, — шаг 6.
diff --git a/install-agenthits.sh b/install-agenthits.sh
index 7ac8aaf1151..62de1329b3c 100755
--- a/install-agenthits.sh
+++ b/install-agenthits.sh
@@ -91,7 +91,8 @@ create_secret_if_missing() {
}
get_postgres_major_version() {
- local tag="${POSTGRES_IMAGE##*:}"
+ local image="${1:-$POSTGRES_IMAGE}"
+ local tag="${image##*:}"
tag="${tag%%-*}"
tag="${tag%%.*}"
@@ -1282,6 +1283,14 @@ upgrade_docker_engine() {
# and healthy when its rollback-protected update runs.
update_agenthits_dokploy() {
require_update_services
+
+ local current_postgres=""
+ current_postgres="$(image_without_digest "$(service_image dokploy-postgres)")"
+ if [ "$(get_postgres_major_version "$current_postgres" || true)" != "$(get_postgres_major_version || true)" ]; then
+ echo "Error: Postgres $current_postgres -> $POSTGRES_IMAGE changes the major version, which this update does not migrate. Nothing was changed." >&2
+ exit 1
+ fi
+
predownload_update_artifacts
local postgres_swap=0
From f0084e8154041fa36b6cd7f6ec8ed4f77bc59418 Mon Sep 17 00:00:00 2001
From: merge-check <140916359+agentHits@users.noreply.github.com>
Date: Thu, 8 Oct 2026 09:10:59 -0400
Subject: [PATCH 14/33] fix(update): set the backup directory mode with an
explicit chmod
mkdir -p -m applies the mode only to the deepest directory, which shellcheck
flags as SC2174. The directory is created with mkdir -p and then chmod 700,
which is the same result without the ambiguity.
Co-Authored-By: Claude Haiku 5.5
---
install-agenthits.sh | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/install-agenthits.sh b/install-agenthits.sh
index 62de1329b3c..4b1660894c3 100755
--- a/install-agenthits.sh
+++ b/install-agenthits.sh
@@ -1154,7 +1154,8 @@ backup_postgres() {
exit 1
fi
# The dump holds the data and the role password hashes, so it stays private.
- mkdir -p -m 700 "$DOKPLOY_BACKUP_DIR"
+ mkdir -p "$DOKPLOY_BACKUP_DIR"
+ chmod 700 "$DOKPLOY_BACKUP_DIR"
file="$DOKPLOY_BACKUP_DIR/postgres-$(date -u +%Y%m%dT%H%M%SZ).sql.gz"
if ! (umask 077 && docker exec "$container" pg_dumpall -U dokploy | gzip >"$file") || [ ! -s "$file" ]; then
rm -f "$file"
From e442554822c517a63a6d9ed9c1beb5e3722983b3 Mon Sep 17 00:00:00 2001
From: merge-check <140916359+agentHits@users.noreply.github.com>
Date: Thu, 8 Oct 2026 09:16:08 -0400
Subject: [PATCH 15/33] fix(update): pin Docker in update.sh to the shared
version
update.sh installed Docker 28.5.0 on a host without Docker, while the
installer and the component pins use 29.8.2. Read the version from
DOCKER_VERSION with the same default as the installer, and extend the
version sync test to cover update.sh.
Co-Authored-By: Claude Haiku 5.5
---
.../__test__/server/component-versions-sync.test.ts | 10 ++++++++++
update.sh | 3 ++-
2 files changed, 12 insertions(+), 1 deletion(-)
diff --git a/apps/dokploy/__test__/server/component-versions-sync.test.ts b/apps/dokploy/__test__/server/component-versions-sync.test.ts
index db6351a3738..537d97959e5 100644
--- a/apps/dokploy/__test__/server/component-versions-sync.test.ts
+++ b/apps/dokploy/__test__/server/component-versions-sync.test.ts
@@ -66,6 +66,16 @@ describe("install-agenthits.sh defaults", () => {
});
});
+describe("update.sh defaults", () => {
+ const updateScript = read("update.sh");
+
+ it("pins Docker", () => {
+ expect(
+ pinIn(updateScript, /DOCKER_VERSION="\$\{DOCKER_VERSION:-([^}]+)\}"/),
+ ).toBe(PINNED_VERSIONS.docker);
+ });
+});
+
describe("Dockerfile", () => {
const dockerfile = read("Dockerfile");
diff --git a/update.sh b/update.sh
index 85682638e1b..610f7d98da1 100755
--- a/update.sh
+++ b/update.sh
@@ -9,6 +9,7 @@ DOKPLOY_OFFICIAL_VERSION="${DOKPLOY_OFFICIAL_VERSION_OVERRIDE:-v0.29.8}"
DOKPLOY_FORK_VERSION="${DOKPLOY_FORK_VERSION_OVERRIDE:-}"
DOKPLOY_HEALTH_TIMEOUT="${DOKPLOY_HEALTH_TIMEOUT:-240}"
DOKPLOY_HEALTH_INTERVAL="${DOKPLOY_HEALTH_INTERVAL:-3}"
+DOCKER_VERSION="${DOCKER_VERSION:-29.8.2}"
command_exists() {
command -v "$@" >/dev/null 2>&1
@@ -34,7 +35,7 @@ install_docker_if_missing() {
if command_exists docker; then
echo "Docker already installed"
else
- curl -sSL https://get.docker.com | sh -s -- --version 28.5.0
+ curl -sSL https://get.docker.com | sh -s -- --version "$DOCKER_VERSION"
fi
}
From b73097e0e3a9ed2373dcc9c89b017eb41be5851b Mon Sep 17 00:00:00 2001
From: merge-check <140916359+agentHits@users.noreply.github.com>
Date: Thu, 8 Oct 2026 09:56:42 -0400
Subject: [PATCH 16/33] feat(update): roll back the panel host Docker engine
upgrade
The Docker Engine upgrade stays off unless DOCKER_ENGINE_UPGRADE=1 is set in
the environment of the update command; the macOS and WSL launchers now pass
it on. Before the Engine is touched, the installed packages the upgrade
replaces are saved under DOKPLOY_BACKUP_DIR/docker-rollback-/ (directory
0700, files 0600). After the upgrade the stack must report the pinned version
with an active swarm and healthy panel, Postgres, Redis and Traefik; if not,
the saved packages are installed back and the stack is checked again. When
the rollback fails too, the commands to finish it by hand are printed.
The Docker apt source is compared with VERSION_CODENAME before anything
changes, and a mismatch stops the update without rewriting the source.
Traefik is recreated with docker create, connected to dokploy-network and to
every other network of the old container (bridge excluded), and only then
started. /etc/dokploy is 755 instead of 777.
Co-Authored-By: Claude Haiku 5.5
---
.../settings/install-agenthits-update.test.ts | 507 +++++++++++++++++-
install-agenthits.ps1 | 2 +-
install-agenthits.sh | 226 +++++++-
3 files changed, 699 insertions(+), 36 deletions(-)
diff --git a/apps/dokploy/__test__/settings/install-agenthits-update.test.ts b/apps/dokploy/__test__/settings/install-agenthits-update.test.ts
index 68335c15b97..9b8a3cd44d4 100644
--- a/apps/dokploy/__test__/settings/install-agenthits-update.test.ts
+++ b/apps/dokploy/__test__/settings/install-agenthits-update.test.ts
@@ -1,6 +1,7 @@
import { spawnSync } from "node:child_process";
import {
chmodSync,
+ mkdirSync,
mkdtempSync,
readdirSync,
readFileSync,
@@ -18,6 +19,7 @@ const repoRoot = path.resolve(
"../../../..",
);
const installerScript = path.join(repoRoot, "install-agenthits.sh");
+const powershellScript = path.join(repoRoot, "install-agenthits.ps1");
const SPAWN_TEST_TIMEOUT_MS = 60_000;
@@ -29,8 +31,15 @@ const realPath = (command: string) =>
const FAKE_DOCKER = `#!/bin/sh
printf '%s\\n' "$*" >> "$DOCKER_CALL_LOG"
case "$1" in
- info | version)
- echo 29.8.2
+ info)
+ case "$*" in
+ *Swarm*) echo active ;;
+ *) echo 29.8.2 ;;
+ esac
+ exit 0
+ ;;
+ version)
+ cat "$DOCKER_ENGINE_FILE"
exit 0
;;
pull)
@@ -56,11 +65,23 @@ case "$1" in
inspect)
case "$*" in
*Config.Image*) echo "$FAKE_TRAEFIK_IMAGE" ;;
+ *NetworkSettings*) echo "$FAKE_TRAEFIK_NETWORKS" ;;
*State.Running*) echo "$FAKE_TRAEFIK_RUNNING" ;;
*State.Health.Status*) echo healthy ;;
esac
exit 0
;;
+ network)
+ case "$2" in
+ inspect)
+ case " $FAKE_MISSING_NETWORKS " in *" $3 "*) exit 1 ;; esac
+ ;;
+ connect)
+ case " $FAKE_CONNECT_FAILS " in *" $3 "*) exit 1 ;; esac
+ ;;
+ esac
+ exit 0
+ ;;
service)
if [ "$2" = "inspect" ]; then
case "$5" in
@@ -105,6 +126,79 @@ esac
exit 0
`;
+const FAKE_APT_GET = `#!/bin/sh
+printf 'apt-get %s\\n' "$*" >> "$DOCKER_CALL_LOG"
+case "$1" in
+ download)
+ [ "$FAKE_DOWNLOAD_FAILS" = 1 ] && exit 1
+ printf 'fake package\\n' > "\${2%%=*}.deb"
+ ;;
+ install)
+ case "$*" in
+ *--allow-downgrades*)
+ [ "$FAKE_ROLLBACK_FAILS" = 1 ] && exit 1
+ printf '%s\\n' "$FAKE_PREVIOUS_ENGINE" > "$DOCKER_ENGINE_FILE"
+ ;;
+ *--no-download*)
+ [ "$FAKE_UPGRADE_FAILS" = 1 ] && exit 1
+ if [ "$FAKE_UPGRADE_KEEPS_OLD" != 1 ]; then
+ printf '%s\\n' "$FAKE_TARGET_ENGINE" > "$DOCKER_ENGINE_FILE"
+ fi
+ ;;
+ esac
+ ;;
+esac
+exit 0
+`;
+
+const FAKE_APT_CACHE = `#!/bin/sh
+printf 'apt-cache %s\\n' "$*" >> "$DOCKER_CALL_LOG"
+case "$1" in
+ madison)
+ echo " docker-ce | 5:29.8.2-1~ubuntu.24.04~noble | https://download.docker.com/linux/ubuntu noble/stable amd64 Packages"
+ ;;
+ policy)
+ case "$2" in
+ containerd.io)
+ printf 'containerd.io:\\n Installed: 1.7.27-1\\n Candidate: 1.7.27-1\\n'
+ ;;
+ docker-buildx-plugin)
+ printf 'docker-buildx-plugin:\\n Installed: 0.25.0-1~ubuntu.24.04~noble\\n Candidate: 0.25.0-1~ubuntu.24.04~noble\\n'
+ ;;
+ docker-compose-plugin)
+ printf 'docker-compose-plugin:\\n Installed: 2.40.0-1~ubuntu.24.04~noble\\n Candidate: 2.40.0-1~ubuntu.24.04~noble\\n'
+ ;;
+ esac
+ ;;
+esac
+exit 0
+`;
+
+const FAKE_DPKG_QUERY = `#!/bin/sh
+printf 'dpkg-query %s\\n' "$*" >> "$DOCKER_CALL_LOG"
+case "$2" in
+ *Status-Status*)
+ case "$3" in
+ docker-ce | docker-ce-cli | containerd.io | docker-buildx-plugin | docker-compose-plugin) echo installed ;;
+ esac
+ ;;
+ *Version*)
+ case "$3" in
+ docker-ce | docker-ce-cli | docker-ce-rootless-extras) echo "5:28.3.0-1~ubuntu.24.04~noble" ;;
+ containerd.io) echo "1.7.27-1" ;;
+ docker-buildx-plugin) echo "0.25.0-1~ubuntu.24.04~noble" ;;
+ docker-compose-plugin) echo "2.40.0-1~ubuntu.24.04~noble" ;;
+ esac
+ ;;
+esac
+exit 0
+`;
+
+const FAKE_SYSTEMCTL = `#!/bin/sh
+printf 'systemctl %s\\n' "$*" >> "$DOCKER_CALL_LOG"
+exit 0
+`;
+
const FAKE_CURL = `#!/bin/sh
echo "unexpected curl: $*" >> "$DOCKER_CALL_LOG"
exit 1
@@ -132,22 +226,58 @@ type OperatorScenario = {
traefikImage?: string;
panelImage?: string;
traefikRunning?: "true" | "false";
+ traefikNetworks?: string;
+ missingNetworks?: string;
+ connectFails?: string;
updateState?: string;
pullFailsFor?: string;
dumpFails?: boolean;
+ upgradeEngine?: boolean;
+ engineVersion?: string;
+ previousEngine?: string;
+ upgradeKeepsOld?: boolean;
+ upgradeFails?: boolean;
+ rollbackFails?: boolean;
+ downloadFails?: boolean;
+ hostCodename?: string;
+ repoCodename?: string;
};
-const runInstaller = (scenario: OperatorScenario = {}) => {
+const runInstaller = (
+ scenario: OperatorScenario = {},
+ args: string[] = ["update"],
+) => {
const dir = mkdtempSync(path.join(tmpdir(), "dokploy-operator-update-"));
try {
const callLog = path.join(dir, "docker-calls.log");
const indexFile = path.join(dir, "service-index");
+ const engineFile = path.join(dir, "engine");
const backupDir = path.join(dir, "backups");
+ const sourcesDir = path.join(dir, "sources");
+ const osRelease = path.join(dir, "os-release");
writeFileSync(callLog, "");
writeFileSync(indexFile, "1");
+ writeFileSync(engineFile, `${scenario.engineVersion ?? "29.8.2"}\n`);
+ writeFileSync(
+ osRelease,
+ `ID=ubuntu\nVERSION_CODENAME=${scenario.hostCodename ?? "noble"}\n`,
+ );
+ const sourceFiles = scenario.repoCodename
+ ? {
+ "docker.list": `deb [arch=amd64] https://download.docker.com/linux/ubuntu ${scenario.repoCodename} stable\n`,
+ }
+ : {};
+ mkdirSync(sourcesDir);
+ for (const [name, body] of Object.entries(sourceFiles)) {
+ writeFileSync(path.join(sourcesDir, name), body);
+ }
const fakes: Record = {
docker: FAKE_DOCKER,
+ "apt-get": FAKE_APT_GET,
+ "apt-cache": FAKE_APT_CACHE,
+ "dpkg-query": FAKE_DPKG_QUERY,
+ systemctl: FAKE_SYSTEMCTL,
curl: FAKE_CURL,
id: FAKE_ID,
uname: FAKE_UNAME,
@@ -157,15 +287,19 @@ const runInstaller = (scenario: OperatorScenario = {}) => {
chmodSync(path.join(dir, name), 0o755);
}
- const result = spawnSync("bash", [installerScript, "update"], {
+ const result = spawnSync("bash", [installerScript, ...args], {
encoding: "utf8",
env: {
...process.env,
PATH: `${dir}:${process.env.PATH}`,
DOCKER_CALL_LOG: callLog,
DOCKER_INDEX_FILE: indexFile,
+ DOCKER_ENGINE_FILE: engineFile,
AGENTHITS_SKIP_HOST_CHECK: "1",
DOKPLOY_BACKUP_DIR: backupDir,
+ DOKPLOY_APT_SOURCES_DIR: sourcesDir,
+ DOKPLOY_OS_RELEASE: osRelease,
+ DOCKER_ENGINE_UPGRADE: scenario.upgradeEngine ? "1" : "0",
AGENTHITS_PULL_RETRY_DELAY: "0",
DOKPLOY_HEALTH_INTERVAL: "0",
DOKPLOY_HEALTH_TIMEOUT: "2",
@@ -173,6 +307,7 @@ const runInstaller = (scenario: OperatorScenario = {}) => {
REDIS_IMAGE: "redis:8.10.2",
POSTGRES_IMAGE: "postgres:18.6",
TRAEFIK_IMAGE: "traefik:v3.7.14",
+ DOCKER_VERSION: "29.8.2",
FAKE_REDIS_IMAGE: scenario.redisImage ?? "redis:8.10.2",
FAKE_POSTGRES_IMAGE: scenario.postgresImage ?? "postgres:18.6",
FAKE_TRAEFIK_IMAGE: scenario.traefikImage ?? "traefik:v3.7.14",
@@ -180,9 +315,19 @@ const runInstaller = (scenario: OperatorScenario = {}) => {
scenario.panelImage ??
"ghcr.io/agenthits/dokploy:agenthits-dev@sha256:index",
FAKE_TRAEFIK_RUNNING: scenario.traefikRunning ?? "true",
+ FAKE_TRAEFIK_NETWORKS:
+ scenario.traefikNetworks ?? "bridge dokploy-network",
+ FAKE_MISSING_NETWORKS: scenario.missingNetworks ?? "",
+ FAKE_CONNECT_FAILS: scenario.connectFails ?? "",
FAKE_UPDATE_STATE: scenario.updateState ?? "completed",
FAKE_PULL_FAILS_FOR: scenario.pullFailsFor ?? "",
FAKE_DUMP_FAILS: scenario.dumpFails ? "1" : "0",
+ FAKE_PREVIOUS_ENGINE: scenario.previousEngine ?? "28.3.0",
+ FAKE_TARGET_ENGINE: "29.8.2",
+ FAKE_UPGRADE_KEEPS_OLD: scenario.upgradeKeepsOld ? "1" : "0",
+ FAKE_UPGRADE_FAILS: scenario.upgradeFails ? "1" : "0",
+ FAKE_ROLLBACK_FAILS: scenario.rollbackFails ? "1" : "0",
+ FAKE_DOWNLOAD_FAILS: scenario.downloadFails ? "1" : "0",
},
});
@@ -194,10 +339,24 @@ const runInstaller = (scenario: OperatorScenario = {}) => {
return [];
}
})();
+ const backupEntries = backups.map((name) => {
+ const entry = path.join(backupDir, name);
+ const stat = statSync(entry);
+ return {
+ name,
+ mode: stat.mode & 0o777,
+ files: stat.isDirectory()
+ ? readdirSync(entry).map((file) => ({
+ name: file,
+ mode: statSync(path.join(entry, file)).mode & 0o777,
+ }))
+ : [],
+ };
+ });
const backupModes = backups.map(
(name) => statSync(path.join(backupDir, name)).mode & 0o777,
);
- return { result, calls, backups, backupModes };
+ return { result, calls, backups, backupModes, backupEntries };
} finally {
rmSync(dir, { force: true, recursive: true });
}
@@ -206,6 +365,9 @@ const runInstaller = (scenario: OperatorScenario = {}) => {
const firstIndex = (calls: string[], prefix: string) =>
calls.findIndex((call) => call.startsWith(prefix));
+const DOCKER_NEW = "5:29.8.2-1~ubuntu.24.04~noble";
+const DOCKER_OLD = "5:28.3.0-1~ubuntu.24.04~noble";
+
describe("install-agenthits.sh update", () => {
it(
"changes nothing when every pinned image is already running",
@@ -218,7 +380,9 @@ describe("install-agenthits.sh update", () => {
expect(calls.some((call) => call.startsWith("service update"))).toBe(
false,
);
- expect(calls.some((call) => call.startsWith("run -d"))).toBe(false);
+ expect(calls.some((call) => call.startsWith("create --name"))).toBe(
+ false,
+ );
expect(backups).toEqual([]);
},
SPAWN_TEST_TIMEOUT_MS,
@@ -241,7 +405,9 @@ describe("install-agenthits.sh update", () => {
false,
);
expect(calls.some((call) => call.startsWith("stop "))).toBe(false);
- expect(calls.some((call) => call.startsWith("run -d"))).toBe(false);
+ expect(calls.some((call) => call.startsWith("create --name"))).toBe(
+ false,
+ );
},
SPAWN_TEST_TIMEOUT_MS,
);
@@ -265,8 +431,13 @@ describe("install-agenthits.sh update", () => {
calls,
"service update --detach --update-order stop-first --update-failure-action rollback --image redis:8.10.2 dokploy-redis",
);
- const traefikStop = firstIndex(calls, "stop dokploy-traefik");
- const traefikRun = firstIndex(calls, "run -d");
+ const traefikRename = firstIndex(
+ calls,
+ "rename dokploy-traefik dokploy-traefik-previous",
+ );
+ const traefikCreate = firstIndex(calls, "create --name dokploy-traefik");
+ const traefikStop = firstIndex(calls, "stop dokploy-traefik-previous");
+ const traefikStart = firstIndex(calls, "start dokploy-traefik");
const postgres = firstIndex(
calls,
"service update --detach --update-order stop-first --update-failure-action rollback --image postgres:18.6 dokploy-postgres",
@@ -279,8 +450,10 @@ describe("install-agenthits.sh update", () => {
for (const step of [
backup,
redis,
+ traefikRename,
+ traefikCreate,
traefikStop,
- traefikRun,
+ traefikStart,
postgres,
panel,
]) {
@@ -288,9 +461,11 @@ describe("install-agenthits.sh update", () => {
}
expect(lastPull).toBeLessThan(backup);
expect(backup).toBeLessThan(redis);
- expect(redis).toBeLessThan(traefikStop);
- expect(traefikStop).toBeLessThan(traefikRun);
- expect(traefikRun).toBeLessThan(postgres);
+ expect(redis).toBeLessThan(traefikRename);
+ expect(traefikRename).toBeLessThan(traefikCreate);
+ expect(traefikCreate).toBeLessThan(traefikStop);
+ expect(traefikStop).toBeLessThan(traefikStart);
+ expect(traefikStart).toBeLessThan(postgres);
expect(postgres).toBeLessThan(panel);
},
SPAWN_TEST_TIMEOUT_MS,
@@ -359,6 +534,73 @@ describe("install-agenthits.sh update", () => {
},
SPAWN_TEST_TIMEOUT_MS,
);
+
+ it(
+ "reconnects every network of the old Traefik container before the new one starts",
+ () => {
+ const { result, calls } = runInstaller({
+ traefikImage: "traefik:v3.6.25",
+ traefikNetworks: "bridge dokploy-network app-a app-b",
+ });
+
+ expect(result.status).toBe(0);
+ const connected = (network: string) =>
+ calls.indexOf(`network connect ${network} dokploy-traefik`);
+ const start = calls.indexOf("start dokploy-traefik");
+ expect(connected("app-a")).toBeGreaterThanOrEqual(0);
+ expect(connected("app-b")).toBeGreaterThanOrEqual(0);
+ expect(connected("app-a")).toBeLessThan(start);
+ expect(connected("app-b")).toBeLessThan(start);
+ expect(
+ calls.some((call) => call.startsWith("network connect bridge")),
+ ).toBe(false);
+ expect(
+ calls.filter(
+ (call) => call === "network connect dokploy-network dokploy-traefik",
+ ),
+ ).toHaveLength(1);
+ },
+ SPAWN_TEST_TIMEOUT_MS,
+ );
+
+ it(
+ "skips a Traefik network that no longer exists",
+ () => {
+ const { result, calls } = runInstaller({
+ traefikImage: "traefik:v3.6.25",
+ traefikNetworks: "app-a app-b",
+ missingNetworks: "app-b",
+ });
+
+ expect(result.status).toBe(0);
+ expect(calls).toContain("network connect app-a dokploy-traefik");
+ expect(calls).not.toContain("network connect app-b dokploy-traefik");
+ },
+ SPAWN_TEST_TIMEOUT_MS,
+ );
+
+ it(
+ "restores the previous Traefik container when a network cannot be connected",
+ () => {
+ const { result, calls } = runInstaller({
+ traefikImage: "traefik:v3.6.25",
+ traefikNetworks: "app-a",
+ connectFails: "app-a",
+ });
+
+ expect(result.status).not.toBe(0);
+ expect(result.stderr).toContain("restoring the previous container");
+ expect(calls).toContain(
+ "rename dokploy-traefik-previous dokploy-traefik",
+ );
+ expect(calls.at(-1)).toBe("start dokploy-traefik");
+ expect(calls.some((call) => call.startsWith("service update"))).toBe(
+ false,
+ );
+ },
+ SPAWN_TEST_TIMEOUT_MS,
+ );
+
it(
"refuses a Postgres major version change before anything runs",
() => {
@@ -377,3 +619,242 @@ describe("install-agenthits.sh update", () => {
SPAWN_TEST_TIMEOUT_MS,
);
});
+
+describe("install-agenthits.sh Docker Engine upgrade", () => {
+ it(
+ "leaves the Docker Engine alone unless DOCKER_ENGINE_UPGRADE=1",
+ () => {
+ const { result, calls } = runInstaller({ engineVersion: "28.3.0" });
+
+ expect(result.status).toBe(0);
+ expect(calls.some((call) => call.startsWith("apt-get"))).toBe(false);
+ expect(calls.some((call) => call.startsWith("systemctl"))).toBe(false);
+ },
+ SPAWN_TEST_TIMEOUT_MS,
+ );
+
+ it(
+ "does nothing when the Engine already runs the pinned version",
+ () => {
+ const { result, calls } = runInstaller({ upgradeEngine: true });
+
+ expect(result.status).toBe(0);
+ expect(calls.some((call) => call.startsWith("apt-get"))).toBe(false);
+ },
+ SPAWN_TEST_TIMEOUT_MS,
+ );
+
+ it(
+ "saves the current Docker packages before the Engine is changed",
+ () => {
+ const { result, calls, backupEntries } = runInstaller({
+ upgradeEngine: true,
+ engineVersion: "28.3.0",
+ });
+
+ expect(result.status).toBe(0);
+ expect(result.stdout).toContain("Docker Engine is 29.8.2");
+ const save = calls.indexOf(`apt-get download docker-ce=${DOCKER_OLD}`);
+ const predownload = calls.indexOf(
+ `apt-get install -y -qq --download-only docker-ce=${DOCKER_NEW} docker-ce-cli=${DOCKER_NEW} containerd.io docker-buildx-plugin docker-compose-plugin`,
+ );
+ const install = calls.indexOf(
+ `apt-get install -y -qq --no-download docker-ce=${DOCKER_NEW} docker-ce-cli=${DOCKER_NEW} containerd.io docker-buildx-plugin docker-compose-plugin`,
+ );
+ expect(save).toBeGreaterThanOrEqual(0);
+ expect(save).toBeLessThan(predownload);
+ expect(predownload).toBeLessThan(install);
+ expect(calls).toContain(`apt-get download docker-ce-cli=${DOCKER_OLD}`);
+ expect(
+ calls.some((call) => call.startsWith("apt-get download containerd.io")),
+ ).toBe(false);
+ expect(backupEntries).toHaveLength(1);
+ expect(backupEntries[0]?.name).toMatch(/^docker-rollback-\d{8}T\d{6}Z$/);
+ expect(backupEntries[0]?.mode).toBe(0o700);
+ expect(backupEntries[0]?.files.map((file) => file.name).sort()).toEqual([
+ "docker-ce-cli.deb",
+ "docker-ce.deb",
+ ]);
+ expect(backupEntries[0]?.files.every((file) => file.mode === 0o600)).toBe(
+ true,
+ );
+ },
+ SPAWN_TEST_TIMEOUT_MS,
+ );
+
+ it(
+ "stops before any change when a current Docker package cannot be saved",
+ () => {
+ const { result, calls } = runInstaller({
+ upgradeEngine: true,
+ engineVersion: "28.3.0",
+ downloadFails: true,
+ });
+
+ expect(result.status).not.toBe(0);
+ expect(result.stderr).toContain(
+ "Pre-download failed: current Docker packages for rollback. Nothing was changed.",
+ );
+ expect(calls.some((call) => call.startsWith("apt-get install"))).toBe(
+ false,
+ );
+ expect(calls.some((call) => call.startsWith("service update"))).toBe(
+ false,
+ );
+ },
+ SPAWN_TEST_TIMEOUT_MS,
+ );
+
+ it(
+ "refuses to upgrade when the Docker apt repository is for another release",
+ () => {
+ const { result, calls, backups } = runInstaller({
+ upgradeEngine: true,
+ engineVersion: "28.3.0",
+ hostCodename: "noble",
+ repoCodename: "jammy",
+ });
+
+ expect(result.status).not.toBe(0);
+ expect(result.stderr).toContain("is for jammy, but this host runs noble");
+ expect(result.stderr).toContain("Nothing was changed");
+ expect(calls.some((call) => call.startsWith("apt-get"))).toBe(false);
+ expect(backups).toEqual([]);
+ },
+ SPAWN_TEST_TIMEOUT_MS,
+ );
+
+ it(
+ "rolls the Engine back when the upgraded host does not report the pinned version",
+ () => {
+ const { result, calls, backupEntries } = runInstaller({
+ upgradeEngine: true,
+ engineVersion: "28.3.0",
+ upgradeKeepsOld: true,
+ });
+
+ expect(result.status).not.toBe(0);
+ expect(result.stderr).toContain(
+ "Docker Engine was rolled back to 28.3.0",
+ );
+ const rollbackDir = backupEntries[0]?.name ?? "";
+ expect(result.stderr).toContain(rollbackDir);
+ expect(
+ calls.some(
+ (call) =>
+ call.startsWith(
+ "apt-get install -y --allow-downgrades --no-download ",
+ ) && call.includes(rollbackDir),
+ ),
+ ).toBe(true);
+ },
+ SPAWN_TEST_TIMEOUT_MS,
+ );
+
+ it(
+ "rolls the Engine back when the upgrade install itself fails",
+ () => {
+ const { result, calls } = runInstaller({
+ upgradeEngine: true,
+ engineVersion: "28.3.0",
+ upgradeFails: true,
+ });
+
+ expect(result.status).not.toBe(0);
+ expect(result.stderr).toContain(
+ "installing the pre-downloaded Docker packages failed",
+ );
+ expect(result.stderr).toContain(
+ "Docker Engine was rolled back to 28.3.0",
+ );
+ expect(
+ calls.some((call) =>
+ call.startsWith("apt-get install -y --allow-downgrades"),
+ ),
+ ).toBe(true);
+ },
+ SPAWN_TEST_TIMEOUT_MS,
+ );
+
+ it(
+ "prints the commands for a manual rollback when the rollback itself fails",
+ () => {
+ const { result } = runInstaller({
+ upgradeEngine: true,
+ engineVersion: "28.3.0",
+ upgradeKeepsOld: true,
+ rollbackFails: true,
+ });
+
+ expect(result.status).not.toBe(0);
+ expect(result.stderr).toContain(
+ "the Docker Engine rollback did not finish",
+ );
+ expect(result.stderr).toContain(
+ "apt-get install -y --allow-downgrades --no-download",
+ );
+ expect(result.stderr).toContain("docker-rollback-");
+ },
+ SPAWN_TEST_TIMEOUT_MS,
+ );
+});
+
+describe("install-agenthits.sh Traefik and host settings", () => {
+ it("keeps /etc/dokploy readable by others but not writable", () => {
+ const installer = readFileSync(installerScript, "utf8");
+
+ expect(installer).toContain("chmod 755 /etc/dokploy\n");
+ expect(installer).not.toContain("chmod 777");
+ });
+
+ it("defaults DOCKER_ENGINE_UPGRADE to off", () => {
+ const installer = readFileSync(installerScript, "utf8");
+
+ expect(installer).toContain(
+ 'DOCKER_ENGINE_UPGRADE="${DOCKER_ENGINE_UPGRADE:-0}"',
+ );
+ });
+
+ it("passes DOCKER_ENGINE_UPGRADE through the macOS and WSL launchers", () => {
+ const installer = readFileSync(installerScript, "utf8");
+ const powershell = readFileSync(powershellScript, "utf8");
+ const passthrough = installer.slice(
+ installer.indexOf("PASSTHROUGH_VARS=("),
+ installer.indexOf(")", installer.indexOf("PASSTHROUGH_VARS=(")),
+ );
+ const wslPassthrough = powershell.slice(
+ powershell.indexOf("$PassthroughVars = @("),
+ powershell.indexOf(")", powershell.indexOf("$PassthroughVars = @(")),
+ );
+
+ expect(passthrough).toContain("DOCKER_ENGINE_UPGRADE");
+ expect(wslPassthrough).toContain("'DOCKER_ENGINE_UPGRADE'");
+ });
+
+ it("keeps the Docker rollback path in the installer", () => {
+ const installer = readFileSync(installerScript, "utf8");
+
+ expect(installer).toContain(
+ "apt-get install -y --allow-downgrades --no-download",
+ );
+ expect(installer).toContain("docker-rollback-");
+ });
+
+ it("reattaches the Traefik networks and starts the new container last in the installer", () => {
+ const installer = readFileSync(installerScript, "utf8");
+ const swap = installer.slice(installer.indexOf("swap_dokploy_traefik() {"));
+ const order = [
+ swap.indexOf('TRAEFIK_EXTRA_NETWORKS="$(traefik_extra_networks)"'),
+ swap.indexOf('create_dokploy_traefik "$TRAEFIK_IMAGE"'),
+ swap.indexOf("docker stop dokploy-traefik-previous"),
+ swap.indexOf("docker start dokploy-traefik >/dev/null"),
+ ];
+
+ expect(order.every((position) => position >= 0)).toBe(true);
+ expect(order).toEqual([...order].sort((a, b) => a - b));
+ expect(installer).toContain(
+ 'docker network connect "$network" dokploy-traefik',
+ );
+ expect(installer).not.toContain("docker run -d");
+ });
+});
diff --git a/install-agenthits.ps1 b/install-agenthits.ps1
index 9e949985f44..41a8ac351a4 100644
--- a/install-agenthits.ps1
+++ b/install-agenthits.ps1
@@ -28,7 +28,7 @@ $PassthroughVars = @(
'DOKPLOY_IMAGE', 'DOKPLOY_RELEASE_TAG', 'DOKPLOY_OFFICIAL_VERSION', 'DOKPLOY_FORK_VERSION',
'TRAEFIK_IMAGE', 'POSTGRES_IMAGE', 'REDIS_IMAGE', 'POSTGRES_DATA_TARGET',
'AGENTHITS_SCRIPT_BASE_URL', 'DOCKER_VERSION', 'DOCKER_SWARM_INIT_ARGS', 'ADVERTISE_ADDR', 'PUBLIC_IP',
- 'DOKPLOY_SERVER_HARDENING', 'HARDEN_SSH', 'HARDEN_UFW', 'HARDEN_FAIL2BAN'
+ 'DOKPLOY_SERVER_HARDENING', 'HARDEN_SSH', 'HARDEN_UFW', 'HARDEN_FAIL2BAN', 'DOCKER_ENGINE_UPGRADE'
)
# VM creator id of WSL in the Hyper-V firewall (documented by Microsoft).
$WslVmCreatorId = '{40E0AC32-46A5-438A-A0B2-2B479E8F2E90}'
diff --git a/install-agenthits.sh b/install-agenthits.sh
index 4b1660894c3..c465b56d207 100755
--- a/install-agenthits.sh
+++ b/install-agenthits.sh
@@ -51,6 +51,12 @@ DOKPLOY_HEALTH_TIMEOUT="${DOKPLOY_HEALTH_TIMEOUT:-240}"
DOKPLOY_HEALTH_INTERVAL="${DOKPLOY_HEALTH_INTERVAL:-3}"
DOKPLOY_TRAEFIK_SETTLE="${DOKPLOY_TRAEFIK_SETTLE:-10}"
AGENTHITS_PULL_RETRY_DELAY="${AGENTHITS_PULL_RETRY_DELAY:-10}"
+DOKPLOY_APT_SOURCES_DIR="${DOKPLOY_APT_SOURCES_DIR:-/etc/apt/sources.list.d}"
+DOKPLOY_OS_RELEASE="${DOKPLOY_OS_RELEASE:-/etc/os-release}"
+DOCKER_PACKAGE_VERSION=""
+DOCKER_ENGINE_UPGRADE_NEEDED=0
+DOCKER_ENGINE_PREVIOUS_VERSION=""
+DOCKER_ROLLBACK_DIR=""
command_exists() {
command -v "$@" >/dev/null 2>&1
@@ -265,11 +271,7 @@ os_release_value() {
)
}
-ensure_docker_running() {
- if docker info >/dev/null 2>&1; then
- return 0
- fi
-
+start_docker_service() {
if command_exists systemctl && [ -d /run/systemd/system ]; then
systemctl enable --now docker
elif command_exists rc-service; then
@@ -278,6 +280,14 @@ ensure_docker_running() {
elif command_exists service; then
service docker start
fi
+}
+
+ensure_docker_running() {
+ if docker info >/dev/null 2>&1; then
+ return 0
+ fi
+
+ start_docker_service
local i=0
while [ "$i" -lt 30 ]; do
@@ -886,7 +896,7 @@ install_agenthits_dokploy() {
docker network create --driver overlay --attachable dokploy-network
mkdir -p /etc/dokploy
- chmod 777 /etc/dokploy
+ chmod 755 /etc/dokploy
create_default_traefik_files
create_secret_if_missing dokploy_postgres_password "$(generate_random_secret)"
@@ -993,11 +1003,94 @@ docker_engine_version() {
docker version --format '{{.Server.Version}}' 2>/dev/null || true
}
+docker_package_installed_version() {
+ if [ "$(dpkg-query -W -f='${db:Status-Status}' "$1" 2>/dev/null)" = "installed" ]; then
+ dpkg-query -W -f='${Version}' "$1"
+ fi
+}
+
+docker_package_candidate_version() {
+ apt-cache policy "$1" 2>/dev/null | awk '/^ Candidate:/ { print $2; exit }'
+}
+
+# Packages from another release would replace the Engine with ones built for it.
+docker_source_codenames() {
+ local file=""
+ for file in "$DOKPLOY_APT_SOURCES_DIR"/*.list; do
+ [ -f "$file" ] || continue
+ awk '/download\.docker\.com/ && !/^[[:space:]]*#/ { for (i = 2; i < NF; i++) if ($i ~ /download\.docker\.com/) { print $(i + 1); break } }' "$file"
+ done
+ for file in "$DOKPLOY_APT_SOURCES_DIR"/*.sources; do
+ [ -f "$file" ] || continue
+ awk 'BEGIN { RS = ""; FS = "\n" } /download\.docker\.com/ { for (i = 1; i <= NF; i++) if ($i ~ /^Suites:/) { split($i, suite, /[ \t]+/); print suite[2] } }' "$file"
+ done
+}
+
+check_docker_repo_codename() {
+ local repo_codenames=""
+ local repo_codename=""
+ local host_codename=""
+ repo_codenames="$(docker_source_codenames)"
+ if [ -z "$repo_codenames" ]; then
+ return 0
+ fi
+ # shellcheck disable=SC1090
+ host_codename="$( (. "$DOKPLOY_OS_RELEASE" && printf '%s' "${VERSION_CODENAME:-}") 2>/dev/null || true)"
+ if [ -z "$host_codename" ]; then
+ echo "Pre-download failed: VERSION_CODENAME is not set in $DOKPLOY_OS_RELEASE, so the Docker apt repository cannot be checked. Nothing was changed." >&2
+ exit 1
+ fi
+ for repo_codename in $repo_codenames; do
+ if [ "$repo_codename" != "$host_codename" ]; then
+ echo "Pre-download failed: the Docker apt repository in $DOKPLOY_APT_SOURCES_DIR is for $repo_codename, but this host runs $host_codename. Point it at $host_codename yourself and run the update again. Nothing was changed." >&2
+ exit 1
+ fi
+ done
+}
+
+# Saved before the Engine is touched, so a failed upgrade can install them back.
+save_docker_rollback_packages() {
+ local package=""
+ local installed=""
+ local candidate=""
+ if [ -z "$(docker_package_installed_version docker-ce)" ]; then
+ echo "Pre-download failed: current Docker packages for rollback (docker-ce is not installed). Nothing was changed." >&2
+ exit 1
+ fi
+ DOCKER_ROLLBACK_DIR="$DOKPLOY_BACKUP_DIR/docker-rollback-$(date -u +%Y%m%dT%H%M%SZ)"
+ mkdir -p "$DOKPLOY_BACKUP_DIR"
+ chmod 700 "$DOKPLOY_BACKUP_DIR"
+ if ! (umask 077 && mkdir -p "$DOCKER_ROLLBACK_DIR"); then
+ echo "Pre-download failed: current Docker packages for rollback. Nothing was changed." >&2
+ exit 1
+ fi
+ for package in docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin docker-ce-rootless-extras; do
+ installed="$(docker_package_installed_version "$package")"
+ if [ -z "$installed" ]; then
+ continue
+ fi
+ case "$package" in
+ docker-ce | docker-ce-cli) ;;
+ *)
+ candidate="$(docker_package_candidate_version "$package")"
+ if [ "$candidate" = "$installed" ]; then
+ continue
+ fi
+ ;;
+ esac
+ if ! (umask 077 && cd "$DOCKER_ROLLBACK_DIR" && apt-get download "$package=$installed"); then
+ echo "Pre-download failed: current Docker packages for rollback. Nothing was changed." >&2
+ exit 1
+ fi
+ done
+}
+
predownload_docker_packages() {
if ! command_exists apt-get; then
echo "Pre-download failed: DOCKER_ENGINE_UPGRADE=1 needs apt-get on this host. Nothing was changed." >&2
exit 1
fi
+ check_docker_repo_codename
if ! download_with_retry apt-get update -qq; then
echo "Pre-download failed: the apt package lists could not be refreshed. Nothing was changed." >&2
exit 1
@@ -1007,6 +1100,7 @@ predownload_docker_packages() {
echo "Pre-download failed: Docker $DOCKER_VERSION is not in the apt sources. Nothing was changed." >&2
exit 1
fi
+ save_docker_rollback_packages
if ! download_with_retry apt-get install -y -qq --download-only "docker-ce=$DOCKER_PACKAGE_VERSION" "docker-ce-cli=$DOCKER_PACKAGE_VERSION" containerd.io docker-buildx-plugin docker-compose-plugin; then
echo "Pre-download failed: the Docker packages. Nothing was changed." >&2
exit 1
@@ -1044,7 +1138,8 @@ predownload_update_artifacts() {
fi
done
DOCKER_ENGINE_UPGRADE_NEEDED=0
- if [ "$DOCKER_ENGINE_UPGRADE" = "1" ] && [ "$(docker_engine_version)" != "$DOCKER_VERSION" ]; then
+ DOCKER_ENGINE_PREVIOUS_VERSION="$(docker_engine_version)"
+ if [ "$DOCKER_ENGINE_UPGRADE" = "1" ] && [ "$DOCKER_ENGINE_PREVIOUS_VERSION" != "$DOCKER_VERSION" ]; then
DOCKER_ENGINE_UPGRADE_NEEDED=1
predownload_docker_packages
fi
@@ -1087,10 +1182,27 @@ panel_ready() {
[ -n "$container" ] && [ "$(docker inspect --format '{{.State.Health.Status}}' "$container" 2>/dev/null)" = "healthy" ]
}
+docker_daemon_running() {
+ docker info >/dev/null 2>&1
+}
+
+swarm_node_active() {
+ [ "$(docker info --format '{{.Swarm.LocalNodeState}}' 2>/dev/null)" = "active" ]
+}
+
+traefik_running() {
+ [ "$(docker inspect --format '{{.State.Running}}' dokploy-traefik 2>/dev/null)" = "true" ]
+}
+
+docker_stack_ready() {
+ [ "$(docker_engine_version)" = "$1" ] && swarm_node_active && panel_ready && postgres_ready && redis_ready && traefik_running
+}
+
wait_until_ready() {
local check="$1"
+ shift
local deadline=$(($(date +%s) + DOKPLOY_HEALTH_TIMEOUT))
- until "$check"; do
+ until "$check" "$@"; do
if [ "$(date +%s)" -ge "$deadline" ]; then
return 1
fi
@@ -1165,8 +1277,24 @@ backup_postgres() {
echo "Postgres backup written to $file"
}
-run_dokploy_traefik() {
- docker run -d \
+# The networks the running Traefik is attached to, minus the two that are handled
+# separately: bridge is the default and dokploy-network is always connected.
+traefik_extra_networks() {
+ local networks=""
+ local network=""
+ networks="$(docker inspect -f '{{range $k,$v := .NetworkSettings.Networks}}{{$k}} {{end}}' dokploy-traefik 2>/dev/null || true)"
+ for network in $networks; do
+ case "$network" in
+ bridge | dokploy-network) ;;
+ *) echo "$network" ;;
+ esac
+ done
+}
+
+# The container is created and connected to all its networks before it starts,
+# so the time without a running Traefik stays short.
+create_dokploy_traefik() {
+ docker create \
--name dokploy-traefik \
--restart always \
-v /etc/dokploy/traefik/traefik.yml:/etc/traefik/traefik.yml \
@@ -1175,12 +1303,24 @@ run_dokploy_traefik() {
-p 80:80/tcp \
-p 443:443/tcp \
-p 443:443/udp \
- "$1"
+ "$1" >/dev/null
docker network connect dokploy-network dokploy-traefik
+ local network=""
+ for network in $TRAEFIK_EXTRA_NETWORKS; do
+ if docker network inspect "$network" >/dev/null 2>&1; then
+ docker network connect "$network" dokploy-traefik
+ fi
+ done
+}
+
+run_dokploy_traefik() {
+ create_dokploy_traefik "$1"
+ docker start dokploy-traefik >/dev/null
}
TRAEFIK_TARGET_IMAGE=""
+TRAEFIK_EXTRA_NETWORKS=""
TRAEFIK_PREVIOUS_KEPT=0
TRAEFIK_SWAPPED=0
@@ -1189,7 +1329,7 @@ TRAEFIK_SWAPPED=0
restore_dokploy_traefik() {
trap - ERR HUP INT TERM
if [ "$TRAEFIK_SWAPPED" = "0" ]; then
- echo "Error: Traefik did not start on $TRAEFIK_TARGET_IMAGE; restoring the previous container." >&2
+ echo "Error: Traefik on $TRAEFIK_TARGET_IMAGE did not come up with all its networks; restoring the previous container." >&2
if [ "$TRAEFIK_PREVIOUS_KEPT" = "1" ]; then
docker rm -f dokploy-traefik >/dev/null 2>&1 || true
docker rename dokploy-traefik-previous dokploy-traefik || true
@@ -1211,12 +1351,12 @@ swap_dokploy_traefik() {
TRAEFIK_TARGET_IMAGE="$TRAEFIK_IMAGE"
TRAEFIK_PREVIOUS_KEPT=0
TRAEFIK_SWAPPED=0
- # errtrace makes the ERR trap fire for a failed docker run inside run_dokploy_traefik.
+ # errtrace makes the ERR trap fire for a failed docker command inside create_dokploy_traefik.
set -o errtrace
trap restore_dokploy_traefik ERR HUP INT TERM
+ TRAEFIK_EXTRA_NETWORKS="$(traefik_extra_networks)"
if docker inspect dokploy-traefik >/dev/null 2>&1; then
- docker stop dokploy-traefik >/dev/null
docker rm -f dokploy-traefik-previous >/dev/null 2>&1 || true
docker rename dokploy-traefik dokploy-traefik-previous
TRAEFIK_PREVIOUS_KEPT=1
@@ -1224,7 +1364,11 @@ swap_dokploy_traefik() {
docker update --restart no dokploy-traefik-previous >/dev/null
fi
- run_dokploy_traefik "$TRAEFIK_IMAGE"
+ create_dokploy_traefik "$TRAEFIK_IMAGE"
+ if [ "$TRAEFIK_PREVIOUS_KEPT" = "1" ]; then
+ docker stop dokploy-traefik-previous >/dev/null
+ fi
+ docker start dokploy-traefik >/dev/null
sleep "$DOKPLOY_TRAEFIK_SETTLE"
if [ "$(docker inspect --format '{{.State.Running}}' dokploy-traefik 2>/dev/null)" != "true" ]; then
restore_dokploy_traefik
@@ -1258,21 +1402,59 @@ run_panel_update() {
return "$status"
}
+print_docker_manual_rollback() {
+ cat >&2 <&2
+ if ! apt-get install -y --allow-downgrades --no-download "$DOCKER_ROLLBACK_DIR"/*.deb; then
+ return 1
+ fi
+ if ! docker_daemon_running; then
+ start_docker_service || true
+ fi
+ if ! wait_until_ready docker_daemon_running; then
+ return 1
+ fi
+ if docker inspect dokploy-traefik >/dev/null 2>&1 && ! traefik_running; then
+ docker start dokploy-traefik >/dev/null || true
+ fi
+ wait_until_ready docker_stack_ready "$DOCKER_ENGINE_PREVIOUS_VERSION"
+}
+
+fail_docker_engine_upgrade() {
+ echo "$1" >&2
+ if rollback_docker_engine; then
+ echo "Error: Docker Engine was rolled back to $DOCKER_ENGINE_PREVIOUS_VERSION. The packages it used are in $DOCKER_ROLLBACK_DIR." >&2
+ exit 1
+ fi
+ echo "Error: the Docker Engine rollback did not finish. The saved packages are in $DOCKER_ROLLBACK_DIR." >&2
+ print_docker_manual_rollback
+ exit 1
+}
+
# Docker Engine restarts dockerd, and live-restore does not cover swarm services,
# so every service on the host restarts here. It runs last, on pre-downloaded packages.
upgrade_docker_engine() {
if [ "$DOCKER_ENGINE_UPGRADE_NEEDED" != "1" ]; then
return 0
fi
- echo "Upgrading Docker Engine to $DOCKER_VERSION; the daemon restarts and the swarm services on this host restart with it."
+ echo "Upgrading Docker Engine from $DOCKER_ENGINE_PREVIOUS_VERSION to $DOCKER_VERSION; the daemon restarts and the swarm services on this host restart with it."
if ! apt-get install -y -qq --no-download "docker-ce=$DOCKER_PACKAGE_VERSION" "docker-ce-cli=$DOCKER_PACKAGE_VERSION" containerd.io docker-buildx-plugin docker-compose-plugin; then
- echo "Error: installing the pre-downloaded Docker packages failed. Check with: apt-get install -f" >&2
- exit 1
+ fail_docker_engine_upgrade "Error: installing the pre-downloaded Docker packages failed."
fi
- ensure_docker_running
- if ! wait_until_ready panel_ready; then
- echo "Error: the panel was not healthy after the Docker restart. See: docker service ps dokploy --no-trunc" >&2
- exit 1
+ if ! docker_daemon_running; then
+ start_docker_service || true
+ fi
+ if ! wait_until_ready docker_stack_ready "$DOCKER_VERSION"; then
+ fail_docker_engine_upgrade "Error: the host was not healthy on Docker Engine $DOCKER_VERSION within $DOKPLOY_HEALTH_TIMEOUT seconds."
fi
echo "Docker Engine is $DOCKER_VERSION"
}
From 97320dfbe11b1af638b5d0fdf032ad4218b96bbb Mon Sep 17 00:00:00 2001
From: merge-check <140916359+agentHits@users.noreply.github.com>
Date: Thu, 8 Oct 2026 09:56:47 -0400
Subject: [PATCH 17/33] feat(servers): install the pinned Docker from apt with
rollback
The component update installs the pinned Docker from the packages downloaded
before any step runs, with apt-get install --no-download, instead of running
get.docker.com a second time. get.docker.com is still fetched only on hosts
without apt-get, where nothing can be pre-downloaded or rolled back.
Before anything changes, the Docker apt source is compared with
VERSION_CODENAME. When it names another release only the Docker step is
skipped, with a message, and the other components still run. When no Docker
source exists and apt does not offer the pinned version, Docker's repository
is added for ubuntu and debian; an existing source is never rewritten.
The packages that the upgrade replaces are saved first, as in the panel host
installer. If the daemon does not report the pinned version, or Traefik is not
running afterwards, the saved packages are installed back, Traefik is started
and the step fails.
Traefik is created with docker create, connected to dokploy-network and to
every other network of the old container (read on the host; bridge excluded),
and only then started. The old container is stopped just before the new one
starts, so the time without a running Traefik stays short.
The Docker step is opt-in: filterDockerUpgrade removes docker from the list
unless upgradeDocker is set, and logs one line saying so.
Co-Authored-By: Claude Haiku 5.5
---
.../__test__/server/server-components.test.ts | 660 ++++++++++++++++--
.../server/src/setup/server-components.ts | 266 ++++++-
packages/server/src/setup/traefik-setup.ts | 26 +-
3 files changed, 866 insertions(+), 86 deletions(-)
diff --git a/apps/dokploy/__test__/server/server-components.test.ts b/apps/dokploy/__test__/server/server-components.test.ts
index 39cfab2b1df..ce0dbb00051 100644
--- a/apps/dokploy/__test__/server/server-components.test.ts
+++ b/apps/dokploy/__test__/server/server-components.test.ts
@@ -1,9 +1,13 @@
import { spawnSync } from "node:child_process";
import {
chmodSync,
+ existsSync,
+ mkdirSync,
mkdtempSync,
+ readdirSync,
readFileSync,
rmSync,
+ statSync,
writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
@@ -18,10 +22,13 @@ import {
buildComponentStatuses,
buildComponentUpdateScript,
COMPONENTS_UPDATE_DONE,
+ DOCKER_UPGRADE_SKIPPED_MESSAGE,
+ filterDockerUpgrade,
parseComponentVersions,
type UpdatableComponent,
} from "@dokploy/server/setup/server-components";
import {
+ buildTraefikCreateCommand,
buildTraefikRunCommand,
TRAEFIK_VERSION,
} from "@dokploy/server/setup/traefik-setup";
@@ -118,7 +125,38 @@ describe("buildComponentUpdateScript", () => {
expect(script).toContain(
"docker rename dokploy-traefik dokploy-traefik-previous",
);
- expect(script).toContain(buildTraefikRunCommand(TRAEFIK_VERSION).trim());
+ expect(script).toContain(buildTraefikCreateCommand(TRAEFIK_VERSION).trim());
+ });
+
+ it("creates the replacement Traefik, attaches its networks, and starts it last", () => {
+ const script = buildComponentUpdateScript(["traefik"]);
+ const order = [
+ script.indexOf("docker rename dokploy-traefik dokploy-traefik-previous"),
+ script.indexOf("docker create --name dokploy-traefik"),
+ script.indexOf("docker network connect dokploy-network dokploy-traefik"),
+ script.indexOf(
+ 'docker network connect "$traefik_network" dokploy-traefik',
+ ),
+ script.indexOf("docker stop dokploy-traefik-previous"),
+ script.indexOf("\n$SUDO_CMD docker start dokploy-traefik >/dev/null\n"),
+ ];
+
+ expect(order.every((position) => position >= 0)).toBe(true);
+ expect(order).toEqual([...order].sort((a, b) => a - b));
+ expect(script).not.toContain("docker run");
+ });
+
+ it("reads the networks of the running Traefik on the host, before it is replaced", () => {
+ const script = buildComponentUpdateScript(["traefik"]);
+ const read = script.indexOf(
+ "{{range $k,$v := .NetworkSettings.Networks}}{{$k}} {{end}}",
+ );
+
+ expect(read).toBeGreaterThanOrEqual(0);
+ expect(read).toBeLessThan(
+ script.indexOf("docker rename dokploy-traefik dokploy-traefik-previous"),
+ );
+ expect(script).toContain("bridge | dokploy-network) continue ;;");
});
it("passes the pinned versions into the installers through env", () => {
@@ -141,6 +179,34 @@ describe("buildComponentUpdateScript", () => {
);
});
+ it("installs Docker from the pre-downloaded apt packages, and runs get.docker.com only without apt", () => {
+ const script = buildComponentUpdateScript(["docker"]);
+
+ expect(script).toContain("apt-get install -y --no-download");
+ expect(script.indexOf("if command -v apt-get")).toBeLessThan(
+ script.indexOf("get.docker.com -o"),
+ );
+ });
+
+ it("keeps a rollback for a failed Docker update in the generated script", () => {
+ const script = buildComponentUpdateScript(["docker"]);
+
+ expect(script).toContain(
+ "apt-get install -y --allow-downgrades --no-download",
+ );
+ expect(script).toContain("docker-rollback-");
+ });
+
+ it("skips only the Docker step when the apt repository is for another release", () => {
+ const script = buildComponentUpdateScript(["docker", "traefik"]);
+
+ expect(script).toContain("DOCKER_SKIPPED=1");
+ expect(script).toContain(
+ "Docker not updated: the Docker apt repository in",
+ );
+ expect(script).toContain("Docker update skipped");
+ });
+
it("ends with the marker the UI waits for", () => {
expect(buildComponentUpdateScript(["buildpacks"])).toContain(
COMPONENTS_UPDATE_DONE,
@@ -163,14 +229,57 @@ describe("buildComponentUpdateScript", () => {
});
});
+describe("filterDockerUpgrade", () => {
+ it("keeps Docker when the upgrade is enabled", () => {
+ expect(filterDockerUpgrade(["docker", "rclone"], true)).toEqual({
+ components: ["docker", "rclone"],
+ skippedDocker: false,
+ });
+ });
+
+ it("removes Docker when the upgrade is not enabled", () => {
+ expect(filterDockerUpgrade(["rclone", "docker", "traefik"], false)).toEqual(
+ { components: ["rclone", "traefik"], skippedDocker: true },
+ );
+ });
+
+ it("reports nothing when Docker is not selected", () => {
+ expect(filterDockerUpgrade(["traefik"], false)).toEqual({
+ components: ["traefik"],
+ skippedDocker: false,
+ });
+ });
+
+ it("leaves no components when Docker was the only one selected", () => {
+ expect(filterDockerUpgrade(["docker"], false)).toEqual({
+ components: [],
+ skippedDocker: true,
+ });
+ });
+
+ it("names the reason in the line the dialog shows", () => {
+ expect(DOCKER_UPGRADE_SKIPPED_MESSAGE).toBe(
+ "Docker Engine not upgraded: it restarts every container on this server. Enable it explicitly to upgrade.",
+ );
+ });
+});
+
const FAKE_TOOLS: Record = {
docker: `#!/bin/sh
printf 'docker %s\\n' "$*" >> "$CALL_LOG"
case "$1" in
pull) [ "$FAKE_PULL_FAILS" = 1 ] && exit 1 ;;
- run) [ "$FAKE_RUN_FAILS" = 1 ] && exit 1 ;;
+ create) [ "$FAKE_CREATE_FAILS" = 1 ] && exit 1 ;;
+ version) cat "$FAKE_STATE/engine" ;;
+ network)
+ case "$2" in
+ inspect) case " $FAKE_MISSING_NETWORKS " in *" $3 "*) exit 1 ;; esac ;;
+ connect) case " $FAKE_CONNECT_FAILS " in *" $3 "*) exit 1 ;; esac ;;
+ esac
+ ;;
inspect)
case "$*" in
+ *NetworkSettings*) echo "$FAKE_TRAEFIK_NETWORKS" ;;
*State.Running*)
if [ "$FAKE_NOT_RUNNING" = 1 ]; then echo false; else echo true; fi
;;
@@ -181,15 +290,80 @@ exit 0
`,
"apt-get": `#!/bin/sh
printf 'apt-get %s\\n' "$*" >> "$CALL_LOG"
-case "$*" in
- *--download-only*) [ "$FAKE_DOWNLOAD_FAILS" = 1 ] && exit 1 ;;
+case "$1" in
+ update) touch "$FAKE_STATE/updated" ;;
+ download)
+ [ "$FAKE_DOWNLOAD_FAILS" = 1 ] && exit 1
+ printf 'fake package\\n' > "\${2%%=*}.deb"
+ ;;
+ install)
+ case "$*" in
+ *--allow-downgrades*)
+ [ "$FAKE_ROLLBACK_FAILS" = 1 ] && exit 1
+ printf '%s\\n' "$FAKE_PREVIOUS_ENGINE" > "$FAKE_STATE/engine"
+ ;;
+ *--no-download*)
+ if [ "$FAKE_UPGRADE_KEEPS_OLD" != 1 ]; then
+ printf '%s\\n' "$FAKE_TARGET_ENGINE" > "$FAKE_STATE/engine"
+ fi
+ ;;
+ esac
+ ;;
esac
exit 0
`,
"apt-cache": `#!/bin/sh
printf 'apt-cache %s\\n' "$*" >> "$CALL_LOG"
-if [ "$FAKE_NO_DOCKER_PACKAGE" = 1 ]; then exit 0; fi
-echo " docker-ce | 5:29.8.2-1~ubuntu.24.04~noble | https://download.docker.com/linux/ubuntu noble/stable amd64 Packages"
+case "$1" in
+ madison)
+ [ "$FAKE_NO_CANDIDATE" = 1 ] && exit 0
+ if [ "$FAKE_CANDIDATE_AFTER_REPO" = 1 ] && [ ! -f "$DOKPLOY_APT_SOURCES_DIR/docker.list" ]; then
+ exit 0
+ fi
+ echo " docker-ce | 5:29.8.2-1~ubuntu.24.04~noble | https://download.docker.com/linux/ubuntu noble/stable amd64 Packages"
+ ;;
+ policy)
+ case "$2" in
+ containerd.io)
+ candidate=1.7.27-1
+ if [ "$FAKE_CONTAINERD_CHANGES" = 1 ]; then candidate=2.0.0-1; fi
+ printf 'containerd.io:\\n Installed: 1.7.27-1\\n Candidate: %s\\n' "$candidate"
+ ;;
+ docker-buildx-plugin)
+ printf 'docker-buildx-plugin:\\n Installed: 0.25.0-1~ubuntu.24.04~noble\\n Candidate: 0.25.0-1~ubuntu.24.04~noble\\n'
+ ;;
+ docker-compose-plugin)
+ printf 'docker-compose-plugin:\\n Installed: 2.40.0-1~ubuntu.24.04~noble\\n Candidate: 2.40.0-1~ubuntu.24.04~noble\\n'
+ ;;
+ esac
+ ;;
+esac
+exit 0
+`,
+ "dpkg-query": `#!/bin/sh
+printf 'dpkg-query %s\\n' "$*" >> "$CALL_LOG"
+case "$2" in
+ *Status-Status*)
+ case "$3" in
+ docker-ce | docker-ce-cli | containerd.io | docker-buildx-plugin | docker-compose-plugin) echo installed ;;
+ esac
+ ;;
+ *Version*)
+ case "$3" in
+ docker-ce | docker-ce-cli | docker-ce-rootless-extras) echo "5:28.3.0-1~ubuntu.24.04~noble" ;;
+ containerd.io) echo "1.7.27-1" ;;
+ docker-buildx-plugin) echo "0.25.0-1~ubuntu.24.04~noble" ;;
+ docker-compose-plugin) echo "2.40.0-1~ubuntu.24.04~noble" ;;
+ esac
+ ;;
+esac
+exit 0
+`,
+ dpkg: `#!/bin/sh
+echo amd64
+`,
+ systemctl: `#!/bin/sh
+printf 'systemctl %s\\n' "$*" >> "$CALL_LOG"
`,
curl: `#!/bin/sh
printf 'curl %s\\n' "$*" >> "$CALL_LOG"
@@ -208,14 +382,66 @@ exit 0
`,
};
+const DOCKER_LIST_JAMMY =
+ "deb [arch=amd64 signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu jammy stable\n";
+const DOCKER_LIST_NOBLE =
+ "deb [arch=amd64 signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu noble stable\n";
+const DOCKER_SOURCES_JAMMY =
+ "Types: deb\nURIs: https://download.docker.com/linux/ubuntu\nSuites: jammy\nComponents: stable\nSigned-By: /etc/apt/keyrings/docker.asc\n";
+
+type ScriptOptions = {
+ env?: Record;
+ osRelease?: string;
+ sourceFiles?: Record;
+};
+
+const listBackups = (backups: string) => {
+ try {
+ return readdirSync(backups).map((name) => {
+ const dirPath = path.join(backups, name);
+ return {
+ name,
+ mode: statSync(dirPath).mode & 0o777,
+ files: readdirSync(dirPath).map((file) => ({
+ name: file,
+ mode: statSync(path.join(dirPath, file)).mode & 0o777,
+ })),
+ };
+ });
+ } catch {
+ return [];
+ }
+};
+
+const readSources = (sources: string) =>
+ Object.fromEntries(
+ readdirSync(sources).map((name) => [
+ name,
+ readFileSync(path.join(sources, name), "utf8"),
+ ]),
+ );
+
const runComponentScript = (
components: UpdatableComponent[],
- env: Record = {},
+ options: ScriptOptions = {},
) => {
const dir = mkdtempSync(path.join(tmpdir(), "component-update-"));
try {
const callLog = path.join(dir, "calls.log");
+ const osRelease = path.join(dir, "os-release");
+ const sources = path.join(dir, "sources");
+ const keyring = path.join(dir, "keyrings", "docker.asc");
+ const backups = path.join(dir, "backups");
+ mkdirSync(sources);
writeFileSync(callLog, "");
+ writeFileSync(path.join(dir, "engine"), "28.3.0\n");
+ writeFileSync(
+ osRelease,
+ options.osRelease ?? "ID=ubuntu\nVERSION_CODENAME=noble\n",
+ );
+ for (const [name, body] of Object.entries(options.sourceFiles ?? {})) {
+ writeFileSync(path.join(sources, name), body);
+ }
for (const [name, body] of Object.entries(FAKE_TOOLS)) {
writeFileSync(path.join(dir, name), body);
chmodSync(path.join(dir, name), 0o755);
@@ -227,31 +453,49 @@ const runComponentScript = (
...process.env,
PATH: `${dir}:${process.env.PATH}`,
CALL_LOG: callLog,
+ FAKE_STATE: dir,
+ FAKE_PREVIOUS_ENGINE: "28.3.0",
+ FAKE_TARGET_ENGINE: "29.8.2",
TRAEFIK_SETTLE_SECONDS: "0",
- ...env,
+ DOKPLOY_BACKUP_DIR: backups,
+ DOKPLOY_OS_RELEASE: osRelease,
+ DOKPLOY_APT_SOURCES_DIR: sources,
+ DOKPLOY_DOCKER_KEYRING: keyring,
+ ...options.env,
},
});
- const calls = readFileSync(callLog, "utf8").split("\n").filter(Boolean);
- return { result, calls };
+ return {
+ status: result.status,
+ stdout: result.stdout,
+ stderr: result.stderr,
+ calls: readFileSync(callLog, "utf8").split("\n").filter(Boolean),
+ backups: listBackups(backups),
+ keyringExists: existsSync(keyring),
+ sources: readSources(sources),
+ };
} finally {
rmSync(dir, { force: true, recursive: true });
}
};
const SPAWN_TEST_TIMEOUT_MS = 30_000;
+const DOCKER_OLD = "5:28.3.0-1~ubuntu.24.04~noble";
+const DOCKER_NEW = "5:29.8.2-1~ubuntu.24.04~noble";
describe("buildComponentUpdateScript run order", () => {
it(
"changes nothing when the Traefik image cannot be pulled",
() => {
- const { result, calls } = runComponentScript(["traefik"], {
- FAKE_PULL_FAILS: "1",
+ const run = runComponentScript(["traefik"], {
+ env: { FAKE_PULL_FAILS: "1" },
});
- expect(result.status).not.toBe(0);
- expect(result.stderr).toContain("Nothing was changed");
+ expect(run.status).not.toBe(0);
+ expect(run.stderr).toContain("Nothing was changed");
expect(
- calls.some((call) => /^docker (stop|rename|run) /.test(call)),
+ run.calls.some((call) =>
+ /^docker (stop|rename|create|start) /.test(call),
+ ),
).toBe(false);
},
SPAWN_TEST_TIMEOUT_MS,
@@ -260,20 +504,22 @@ describe("buildComponentUpdateScript run order", () => {
it(
"downloads every artifact before the first step changes anything",
() => {
- const { result, calls } = runComponentScript(["traefik", "docker"]);
+ const run = runComponentScript(["traefik", "docker"]);
- expect(result.status).toBe(0);
- const download = calls.findIndex((call) =>
+ expect(run.status).toBe(0);
+ const download = run.calls.findIndex((call) =>
call.includes("--download-only"),
);
- const pull = calls.findIndex((call) =>
+ const pull = run.calls.findIndex((call) =>
call.startsWith("docker pull traefik:v"),
);
- const stop = calls.findIndex((call) => call.startsWith("docker stop"));
+ const firstChange = run.calls.findIndex((call) =>
+ call.startsWith("docker rename dokploy-traefik"),
+ );
expect(download).toBeGreaterThanOrEqual(0);
expect(pull).toBeGreaterThanOrEqual(0);
- expect(pull).toBeLessThan(stop);
- expect(download).toBeLessThan(stop);
+ expect(pull).toBeLessThan(firstChange);
+ expect(download).toBeLessThan(firstChange);
},
SPAWN_TEST_TIMEOUT_MS,
);
@@ -281,25 +527,26 @@ describe("buildComponentUpdateScript run order", () => {
it(
"replaces Traefik only after the new container has run",
() => {
- const { result, calls } = runComponentScript(["traefik"]);
+ const run = runComponentScript(["traefik"]);
- expect(result.status).toBe(0);
- expect(result.stdout).toContain(
+ expect(run.status).toBe(0);
+ expect(run.stdout).toContain(
`Traefik version ${TRAEFIK_VERSION} installed`,
);
const at = (prefix: string) =>
- calls.findIndex((call) => call.startsWith(prefix));
+ run.calls.findIndex((call) => call.startsWith(prefix));
const order = [
at("docker pull traefik:v"),
- at("docker stop dokploy-traefik"),
at("docker rename dokploy-traefik dokploy-traefik-previous"),
- at("docker run -d"),
+ at("docker create --name dokploy-traefik"),
at("docker network connect dokploy-network dokploy-traefik"),
+ at("docker stop dokploy-traefik-previous"),
+ at("docker start dokploy-traefik"),
at("docker inspect -f {{.State.Running}} dokploy-traefik"),
];
expect(order.every((position) => position >= 0)).toBe(true);
expect(order).toEqual([...order].sort((a, b) => a - b));
- expect(calls.at(-1)).toBe("docker rm -f dokploy-traefik-previous");
+ expect(run.calls.at(-1)).toBe("docker rm -f dokploy-traefik-previous");
},
SPAWN_TEST_TIMEOUT_MS,
);
@@ -307,36 +554,104 @@ describe("buildComponentUpdateScript run order", () => {
it(
"restores the previous Traefik container when the new one is not running",
() => {
- const { result, calls } = runComponentScript(["traefik"], {
- FAKE_NOT_RUNNING: "1",
+ const run = runComponentScript(["traefik"], {
+ env: { FAKE_NOT_RUNNING: "1" },
});
- expect(result.status).not.toBe(0);
- expect(result.stderr).toContain("restoring the previous container");
- expect(calls).toContain(
+ expect(run.status).not.toBe(0);
+ expect(run.stderr).toContain("restoring the previous container");
+ expect(run.calls).toContain(
"docker rename dokploy-traefik-previous dokploy-traefik",
);
- expect(calls.at(-1)).toBe("docker start dokploy-traefik");
- expect(result.stdout).not.toContain("installed");
+ expect(run.calls.at(-1)).toBe("docker start dokploy-traefik");
+ expect(run.stdout).not.toContain("installed");
},
SPAWN_TEST_TIMEOUT_MS,
);
it(
- "restores the previous Traefik container when docker run fails",
+ "restores the previous Traefik container when docker create fails",
() => {
- const { result, calls } = runComponentScript(["traefik"], {
- FAKE_RUN_FAILS: "1",
+ const run = runComponentScript(["traefik"], {
+ env: { FAKE_CREATE_FAILS: "1" },
});
- expect(result.status).not.toBe(0);
- expect(calls).toContain(
+ expect(run.status).not.toBe(0);
+ expect(run.calls).toContain(
"docker rename dokploy-traefik-previous dokploy-traefik",
);
expect(
- calls.some((call) => call.startsWith("docker network connect")),
+ run.calls.some((call) => call.startsWith("docker network connect")),
).toBe(false);
- expect(calls.at(-1)).toBe("docker start dokploy-traefik");
+ expect(run.calls.at(-1)).toBe("docker start dokploy-traefik");
+ },
+ SPAWN_TEST_TIMEOUT_MS,
+ );
+
+ it(
+ "connects every network of the old Traefik container before the new one starts",
+ () => {
+ const run = runComponentScript(["traefik"], {
+ env: { FAKE_TRAEFIK_NETWORKS: "bridge dokploy-network app-a app-b" },
+ });
+
+ expect(run.status).toBe(0);
+ const connected = (network: string) =>
+ run.calls.indexOf(`docker network connect ${network} dokploy-traefik`);
+ const start = run.calls.indexOf("docker start dokploy-traefik");
+ expect(connected("app-a")).toBeGreaterThanOrEqual(0);
+ expect(connected("app-b")).toBeGreaterThanOrEqual(0);
+ expect(connected("app-a")).toBeLessThan(start);
+ expect(connected("app-b")).toBeLessThan(start);
+ expect(
+ run.calls.some((call) =>
+ call.startsWith("docker network connect bridge"),
+ ),
+ ).toBe(false);
+ expect(
+ run.calls.filter(
+ (call) =>
+ call === "docker network connect dokploy-network dokploy-traefik",
+ ),
+ ).toHaveLength(1);
+ },
+ SPAWN_TEST_TIMEOUT_MS,
+ );
+
+ it(
+ "skips a network that no longer exists",
+ () => {
+ const run = runComponentScript(["traefik"], {
+ env: {
+ FAKE_TRAEFIK_NETWORKS: "app-a app-b",
+ FAKE_MISSING_NETWORKS: "app-b",
+ },
+ });
+
+ expect(run.status).toBe(0);
+ expect(run.calls).toContain(
+ "docker network connect app-a dokploy-traefik",
+ );
+ expect(run.calls).not.toContain(
+ "docker network connect app-b dokploy-traefik",
+ );
+ },
+ SPAWN_TEST_TIMEOUT_MS,
+ );
+
+ it(
+ "restores the previous Traefik container when a network cannot be connected",
+ () => {
+ const run = runComponentScript(["traefik"], {
+ env: { FAKE_TRAEFIK_NETWORKS: "app-a", FAKE_CONNECT_FAILS: "app-a" },
+ });
+
+ expect(run.status).not.toBe(0);
+ expect(run.stderr).toContain("restoring the previous container");
+ expect(run.calls).toContain(
+ "docker rename dokploy-traefik-previous dokploy-traefik",
+ );
+ expect(run.calls.at(-1)).toBe("docker start dokploy-traefik");
},
SPAWN_TEST_TIMEOUT_MS,
);
@@ -344,37 +659,268 @@ describe("buildComponentUpdateScript run order", () => {
it(
"downloads the pinned Docker packages before the Docker step runs",
() => {
- const { result, calls } = runComponentScript(["docker"]);
+ const run = runComponentScript(["docker"]);
- expect(result.status).toBe(0);
- expect(calls).toContain(
- "apt-get install -y -qq --download-only docker-ce=5:29.8.2-1~ubuntu.24.04~noble docker-ce-cli=5:29.8.2-1~ubuntu.24.04~noble containerd.io docker-buildx-plugin docker-compose-plugin",
+ expect(run.status).toBe(0);
+ expect(run.calls).toContain(
+ `apt-get install -y -qq --download-only docker-ce=${DOCKER_NEW} docker-ce-cli=${DOCKER_NEW} containerd.io docker-buildx-plugin docker-compose-plugin`,
+ );
+ expect(run.stdout.indexOf("Downloading update artifacts")).toBeLessThan(
+ run.stdout.indexOf(`Updating Docker to ${PINNED_VERSIONS.docker}`),
+ );
+ },
+ SPAWN_TEST_TIMEOUT_MS,
+ );
+
+ it(
+ "saves the current Docker packages before the Engine is changed",
+ () => {
+ const run = runComponentScript(["docker"]);
+
+ expect(run.status).toBe(0);
+ const save = run.calls.indexOf(
+ `apt-get download docker-ce=${DOCKER_OLD}`,
+ );
+ const predownload = run.calls.indexOf(
+ `apt-get install -y -qq --download-only docker-ce=${DOCKER_NEW} docker-ce-cli=${DOCKER_NEW} containerd.io docker-buildx-plugin docker-compose-plugin`,
+ );
+ const install = run.calls.indexOf(
+ `apt-get install -y --no-download docker-ce=${DOCKER_NEW} docker-ce-cli=${DOCKER_NEW} containerd.io docker-buildx-plugin docker-compose-plugin`,
+ );
+ expect(save).toBeGreaterThanOrEqual(0);
+ expect(save).toBeLessThan(predownload);
+ expect(predownload).toBeLessThan(install);
+ expect(run.calls).toContain(
+ `apt-get download docker-ce-cli=${DOCKER_OLD}`,
);
expect(
- result.stdout.indexOf("Downloading update artifacts"),
- ).toBeLessThan(
- result.stdout.indexOf(`Updating Docker to ${PINNED_VERSIONS.docker}`),
+ run.calls.some((call) =>
+ call.startsWith("apt-get download containerd.io"),
+ ),
+ ).toBe(false);
+ expect(run.backups).toHaveLength(1);
+ expect(run.backups[0]?.name).toMatch(/^docker-rollback-\d{8}T\d{6}Z$/);
+ expect(run.backups[0]?.mode).toBe(0o700);
+ expect(run.backups[0]?.files.map((file) => file.name).sort()).toEqual([
+ "docker-ce-cli.deb",
+ "docker-ce.deb",
+ ]);
+ expect(run.backups[0]?.files.every((file) => file.mode === 0o600)).toBe(
+ true,
+ );
+ },
+ SPAWN_TEST_TIMEOUT_MS,
+ );
+
+ it(
+ "installs the pinned Docker from apt without running get.docker.com",
+ () => {
+ const run = runComponentScript(["docker"]);
+
+ expect(run.status).toBe(0);
+ expect(run.calls.some((call) => call.includes("get.docker.com"))).toBe(
+ false,
+ );
+ expect(run.stdout).toContain(
+ `Docker version ${PINNED_VERSIONS.docker} installed`,
);
},
SPAWN_TEST_TIMEOUT_MS,
);
it(
- "stops before the Docker step when apt has no package for the pinned version",
+ "rolls Docker back when the daemon does not come back on the pinned version",
() => {
- const { result, calls } = runComponentScript(["docker"], {
- FAKE_NO_DOCKER_PACKAGE: "1",
+ const run = runComponentScript(["docker"], {
+ env: { FAKE_UPGRADE_KEEPS_OLD: "1" },
});
- expect(result.status).not.toBe(0);
- expect(result.stderr).toContain("Nothing was changed");
- expect(calls.some((call) => call.startsWith("apt-get install"))).toBe(
+ expect(run.status).not.toBe(0);
+ expect(run.stderr).toContain("Docker was rolled back to 28.3.0");
+ expect(run.stderr).toContain("docker-rollback-");
+ expect(
+ run.calls.some(
+ (call) =>
+ call.startsWith(
+ "apt-get install -y --allow-downgrades --no-download ",
+ ) && call.includes("docker-rollback-"),
+ ),
+ ).toBe(true);
+ },
+ SPAWN_TEST_TIMEOUT_MS,
+ );
+
+ it(
+ "prints the commands to finish the rollback by hand when the rollback fails",
+ () => {
+ const run = runComponentScript(["docker"], {
+ env: { FAKE_UPGRADE_KEEPS_OLD: "1", FAKE_ROLLBACK_FAILS: "1" },
+ });
+
+ expect(run.status).not.toBe(0);
+ expect(run.stderr).toContain("did not finish");
+ expect(run.stderr).toContain(
+ "apt-get install -y --allow-downgrades --no-download",
+ );
+ expect(run.stderr).toContain("docker-rollback-");
+ },
+ SPAWN_TEST_TIMEOUT_MS,
+ );
+
+ it(
+ "stops before any change when a current Docker package cannot be saved",
+ () => {
+ const run = runComponentScript(["docker"], {
+ env: { FAKE_DOWNLOAD_FAILS: "1" },
+ });
+
+ expect(run.status).not.toBe(0);
+ expect(run.stderr).toContain(
+ "Pre-download failed: current Docker packages for rollback. Nothing was changed.",
+ );
+ expect(run.calls.some((call) => call.startsWith("apt-get install"))).toBe(
false,
);
- expect(result.stdout).not.toContain("Updating Docker");
+ expect(run.stdout).not.toContain("Updating Docker");
},
SPAWN_TEST_TIMEOUT_MS,
);
+
+ it(
+ "skips only the Docker step when the apt repository is for another release",
+ () => {
+ const run = runComponentScript(["docker", "traefik"], {
+ sourceFiles: { "docker.list": DOCKER_LIST_JAMMY },
+ });
+
+ expect(run.status).toBe(0);
+ expect(run.stdout).toContain(
+ "Docker not updated: the Docker apt repository in",
+ );
+ expect(run.stdout).toContain("is for jammy, but this host runs noble");
+ expect(run.stdout).toContain("Docker update skipped");
+ expect(run.stdout).toContain(
+ `Traefik version ${TRAEFIK_VERSION} installed`,
+ );
+ expect(run.stdout).toContain(COMPONENTS_UPDATE_DONE);
+ expect(run.calls.some((call) => call.startsWith("apt-get "))).toBe(false);
+ expect(run.calls.some((call) => call.includes("get.docker.com"))).toBe(
+ false,
+ );
+ },
+ SPAWN_TEST_TIMEOUT_MS,
+ );
+
+ it(
+ "reads the Docker repository codename from a deb822 sources file too",
+ () => {
+ const run = runComponentScript(["docker"], {
+ sourceFiles: { "docker.sources": DOCKER_SOURCES_JAMMY },
+ });
+
+ expect(run.status).toBe(0);
+ expect(run.stdout).toContain("is for jammy, but this host runs noble");
+ expect(run.calls.some((call) => call.startsWith("apt-get "))).toBe(false);
+ },
+ SPAWN_TEST_TIMEOUT_MS,
+ );
+
+ it(
+ "accepts a Docker repository for the same release",
+ () => {
+ const run = runComponentScript(["docker"], {
+ sourceFiles: { "docker.list": DOCKER_LIST_NOBLE },
+ });
+
+ expect(run.status).toBe(0);
+ expect(run.stdout).not.toContain("Docker not updated");
+ expect(run.stdout).toContain(
+ `Docker version ${PINNED_VERSIONS.docker} installed`,
+ );
+ },
+ SPAWN_TEST_TIMEOUT_MS,
+ );
+
+ it(
+ "adds Docker's repository when none is configured and apt does not offer the pinned version",
+ () => {
+ const run = runComponentScript(["docker"], {
+ env: { FAKE_CANDIDATE_AFTER_REPO: "1" },
+ });
+
+ expect(run.status).toBe(0);
+ expect(
+ run.calls.some((call) =>
+ call.startsWith(
+ "curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o ",
+ ),
+ ),
+ ).toBe(true);
+ expect(run.keyringExists).toBe(true);
+ expect(run.sources["docker.list"]).toMatch(
+ /^deb \[arch=amd64 signed-by=.+docker\.asc\] https:\/\/download\.docker\.com\/linux\/ubuntu noble stable\n$/,
+ );
+ expect(
+ run.calls.filter((call) => call === "apt-get update -qq"),
+ ).toHaveLength(2);
+ expect(run.stdout).toContain(
+ `Docker version ${PINNED_VERSIONS.docker} installed`,
+ );
+ },
+ SPAWN_TEST_TIMEOUT_MS,
+ );
+
+ it(
+ "does not rewrite an existing Docker repository that does not offer the pinned version",
+ () => {
+ const run = runComponentScript(["docker"], {
+ env: { FAKE_NO_CANDIDATE: "1" },
+ sourceFiles: { "docker.list": DOCKER_LIST_NOBLE },
+ });
+
+ expect(run.status).not.toBe(0);
+ expect(run.stderr).toContain("Nothing was changed");
+ expect(run.sources["docker.list"]).toBe(DOCKER_LIST_NOBLE);
+ expect(run.keyringExists).toBe(false);
+ },
+ SPAWN_TEST_TIMEOUT_MS,
+ );
+
+ it(
+ "stops before any change on an unsupported distribution without a Docker repository",
+ () => {
+ const run = runComponentScript(["docker"], {
+ env: { FAKE_CANDIDATE_AFTER_REPO: "1" },
+ osRelease: "ID=fedora\nVERSION_CODENAME=40\n",
+ });
+
+ expect(run.status).not.toBe(0);
+ expect(run.stderr).toContain(
+ "only added automatically on ubuntu and debian",
+ );
+ expect(run.stderr).toContain("Nothing was changed");
+ expect(run.calls.some((call) => call.startsWith("curl"))).toBe(false);
+ expect(run.calls.some((call) => call.startsWith("apt-get install"))).toBe(
+ false,
+ );
+ expect(run.keyringExists).toBe(false);
+ expect(run.sources).toEqual({});
+ },
+ SPAWN_TEST_TIMEOUT_MS,
+ );
+});
+
+describe("buildTraefikCreateCommand", () => {
+ it("creates the container with the same ports and image as run, without starting it", () => {
+ const command = buildTraefikCreateCommand("3.7.5");
+
+ expect(command).toContain("docker create");
+ expect(command).not.toContain("docker run");
+ expect(command).not.toContain("network connect");
+ expect(command).toMatch(/-p 443:443\s/);
+ expect(command).toContain("-p 443:443/udp");
+ expect(command).toContain("traefik:v3.7.5");
+ });
});
describe("buildTraefikRunCommand", () => {
diff --git a/packages/server/src/setup/server-components.ts b/packages/server/src/setup/server-components.ts
index 12a8704f759..97526ca5885 100644
--- a/packages/server/src/setup/server-components.ts
+++ b/packages/server/src/setup/server-components.ts
@@ -6,7 +6,7 @@ import {
} from "../utils/servers/destination";
import { PINNED_VERSIONS } from "./component-versions";
import { rcloneInstallCommand } from "./server-setup";
-import { buildTraefikRunCommand, TRAEFIK_VERSION } from "./traefik-setup";
+import { buildTraefikCreateCommand, TRAEFIK_VERSION } from "./traefik-setup";
export const COMPONENT_NAMES = [
"docker",
@@ -94,6 +94,217 @@ export const buildComponentStatuses = (
};
});
+export const DOCKER_UPGRADE_SKIPPED_MESSAGE =
+ "Docker Engine not upgraded: it restarts every container on this server. Enable it explicitly to upgrade.";
+
+export const filterDockerUpgrade = (
+ components: UpdatableComponent[],
+ upgradeDocker: boolean,
+) => {
+ if (upgradeDocker || !components.includes("docker")) {
+ return { components, skippedDocker: false };
+ }
+ return {
+ components: components.filter((component) => component !== "docker"),
+ skippedDocker: true,
+ };
+};
+
+const dockerVersionPattern = PINNED_VERSIONS.docker.replace(/\./g, "\\.");
+
+const dockerHelpers = () => `
+DOKPLOY_BACKUP_DIR="\${DOKPLOY_BACKUP_DIR:-/var/backups/dokploy}"
+DOKPLOY_OS_RELEASE="\${DOKPLOY_OS_RELEASE:-/etc/os-release}"
+DOKPLOY_APT_SOURCES_DIR="\${DOKPLOY_APT_SOURCES_DIR:-/etc/apt/sources.list.d}"
+DOKPLOY_DOCKER_KEYRING="\${DOKPLOY_DOCKER_KEYRING:-/etc/apt/keyrings/docker.asc}"
+DOCKER_FROM_APT=0
+DOCKER_SKIPPED=0
+DOCKER_PACKAGE_VERSION=""
+DOCKER_ROLLBACK_DIR=""
+
+docker_installed_version() {
+ if [ "$(dpkg-query -W -f='\${db:Status-Status}' "$1" 2>/dev/null)" = "installed" ]; then
+ dpkg-query -W -f='\${Version}' "$1"
+ fi
+}
+
+docker_candidate_version() {
+ apt-cache policy "$1" 2>/dev/null | awk '/^ Candidate:/ { print $2; exit }'
+}
+
+docker_repo_codenames() {
+ local file=""
+ for file in "$DOKPLOY_APT_SOURCES_DIR"/*.list; do
+ [ -f "$file" ] || continue
+ awk '/download\\.docker\\.com/ && !/^[[:space:]]*#/ { for (i = 2; i < NF; i++) if ($i ~ /download\\.docker\\.com/) { print $(i + 1); break } }' "$file"
+ done
+ for file in "$DOKPLOY_APT_SOURCES_DIR"/*.sources; do
+ [ -f "$file" ] || continue
+ awk 'BEGIN { RS = ""; FS = "\\n" } /download\\.docker\\.com/ { for (i = 1; i <= NF; i++) if ($i ~ /^Suites:/) { split($i, suite, /[ \\t]+/); print suite[2] } }' "$file"
+ done
+}
+
+docker_check_repo_codename() {
+ local repo_codenames="" repo_codename="" host_codename=""
+ repo_codenames="$(docker_repo_codenames)"
+ if [ -z "$repo_codenames" ]; then
+ return 0
+ fi
+ host_codename="$( (. "$DOKPLOY_OS_RELEASE" && printf '%s' "\${VERSION_CODENAME:-}") 2>/dev/null || true)"
+ for repo_codename in $repo_codenames; do
+ if [ -z "$host_codename" ] || [ "$repo_codename" != "$host_codename" ]; then
+ echo "Docker not updated: the Docker apt repository in $DOKPLOY_APT_SOURCES_DIR is for $repo_codename, but this host runs \${host_codename:-an unknown release}. Fix that source file yourself and run the update again. The other components continue."
+ return 1
+ fi
+ done
+ return 0
+}
+
+docker_pinned_candidate() {
+ apt-cache madison docker-ce | awk -F'|' '{ gsub(/ /, "", $2); print $2 }' | grep -m1 -E '^([0-9]+:)?${dockerVersionPattern}-' || true
+}
+
+docker_add_apt_repository() {
+ local id="" codename="" arch="" key=""
+ if [ -n "$(docker_repo_codenames)" ] || [ -e "$DOKPLOY_APT_SOURCES_DIR/docker.list" ]; then
+ abort_before_change "Pre-download failed: Docker ${PINNED_VERSIONS.docker} is not offered by the Docker apt repository configured on this host."
+ fi
+ id="$( (. "$DOKPLOY_OS_RELEASE" && printf '%s' "\${ID:-}") 2>/dev/null || true)"
+ codename="$( (. "$DOKPLOY_OS_RELEASE" && printf '%s' "\${VERSION_CODENAME:-}") 2>/dev/null || true)"
+ case "$id" in
+ ubuntu | debian) ;;
+ *)
+ abort_before_change "Pre-download failed: Docker ${PINNED_VERSIONS.docker} is not in the apt sources, and Docker's repository is only added automatically on ubuntu and debian (this host is '$id')."
+ ;;
+ esac
+ if [ -z "$codename" ]; then
+ abort_before_change "Pre-download failed: VERSION_CODENAME is not set in $DOKPLOY_OS_RELEASE."
+ fi
+ key="$(mktemp)"
+ download_with_retry curl -fsSL "https://download.docker.com/linux/$id/gpg" -o "$key" || abort_before_change "Pre-download failed: Docker's apt key."
+ $SUDO_CMD install -d -m 0755 "$(dirname "$DOKPLOY_DOCKER_KEYRING")"
+ $SUDO_CMD install -m 0644 "$key" "$DOKPLOY_DOCKER_KEYRING"
+ rm -f "$key"
+ arch="$(dpkg --print-architecture)"
+ printf 'deb [arch=%s signed-by=%s] https://download.docker.com/linux/%s %s stable\\n' "$arch" "$DOKPLOY_DOCKER_KEYRING" "$id" "$codename" | $SUDO_CMD tee "$DOKPLOY_APT_SOURCES_DIR/docker.list" >/dev/null
+}
+
+docker_save_rollback_packages() {
+ local package="" installed="" candidate=""
+ if [ -z "$(docker_installed_version docker-ce)" ]; then
+ abort_before_change "Pre-download failed: current Docker packages for rollback (docker-ce is not installed)."
+ fi
+ DOCKER_ROLLBACK_DIR="$DOKPLOY_BACKUP_DIR/docker-rollback-$(date -u +%Y%m%dT%H%M%SZ)"
+ $SUDO_CMD mkdir -p "$DOKPLOY_BACKUP_DIR" || abort_before_change "Pre-download failed: current Docker packages for rollback."
+ $SUDO_CMD chmod 700 "$DOKPLOY_BACKUP_DIR" || abort_before_change "Pre-download failed: current Docker packages for rollback."
+ $SUDO_CMD sh -c 'umask 077 && mkdir -p "$1"' sh "$DOCKER_ROLLBACK_DIR" || abort_before_change "Pre-download failed: current Docker packages for rollback."
+ for package in docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin docker-ce-rootless-extras; do
+ installed="$(docker_installed_version "$package")"
+ if [ -z "$installed" ]; then
+ continue
+ fi
+ case "$package" in
+ docker-ce | docker-ce-cli) ;;
+ *)
+ candidate="$(docker_candidate_version "$package")"
+ if [ "$candidate" = "$installed" ]; then
+ continue
+ fi
+ ;;
+ esac
+ $SUDO_CMD sh -c 'umask 077 && cd "$1" && apt-get download "$2" >/dev/null' sh "$DOCKER_ROLLBACK_DIR" "$package=$installed" || abort_before_change "Pre-download failed: current Docker packages for rollback."
+ done
+}
+
+docker_prepare_apt() {
+ $SUDO_CMD apt-get update -qq || abort_before_change "Pre-download failed: the apt package lists could not be refreshed."
+ DOCKER_PACKAGE_VERSION="$(docker_pinned_candidate)"
+ if [ -z "$DOCKER_PACKAGE_VERSION" ]; then
+ docker_add_apt_repository
+ $SUDO_CMD apt-get update -qq || abort_before_change "Pre-download failed: the apt package lists could not be refreshed."
+ DOCKER_PACKAGE_VERSION="$(docker_pinned_candidate)"
+ if [ -z "$DOCKER_PACKAGE_VERSION" ]; then
+ abort_before_change "Pre-download failed: Docker ${PINNED_VERSIONS.docker} is not in the apt sources, even after adding Docker's repository."
+ fi
+ fi
+ docker_save_rollback_packages
+ download_with_retry $SUDO_CMD apt-get install -y -qq --download-only "docker-ce=$DOCKER_PACKAGE_VERSION" "docker-ce-cli=$DOCKER_PACKAGE_VERSION" containerd.io docker-buildx-plugin docker-compose-plugin || abort_before_change "Pre-download failed: the Docker packages."
+}
+
+docker_ready_on() {
+ [ "$($SUDO_CMD docker version --format '{{.Server.Version}}' 2>/dev/null)" = "$1" ] || return 1
+ if [ "$2" = 1 ]; then
+ [ "$($SUDO_CMD docker inspect -f '{{.State.Running}}' dokploy-traefik 2>/dev/null)" = "true" ] || return 1
+ fi
+}
+
+docker_wait_ready() {
+ local attempt=0
+ until docker_ready_on "$1" "$2"; do
+ attempt=$((attempt + 1))
+ if [ "$attempt" -ge 30 ]; then
+ return 1
+ fi
+ sleep 2
+ done
+}
+
+docker_roll_back() {
+ local previous="$1"
+ local traefik_existed="$2"
+ echo "Rolling Docker back to $previous from $DOCKER_ROLLBACK_DIR" >&2
+ if $SUDO_CMD sh -c 'apt-get install -y --allow-downgrades --no-download "$1"/*.deb' sh "$DOCKER_ROLLBACK_DIR"; then
+ if ! $SUDO_CMD docker info >/dev/null 2>&1; then
+ $SUDO_CMD systemctl start docker >/dev/null 2>&1 || true
+ fi
+ if [ "$traefik_existed" = 1 ] && [ "$($SUDO_CMD docker inspect -f '{{.State.Running}}' dokploy-traefik 2>/dev/null)" != "true" ]; then
+ $SUDO_CMD docker start dokploy-traefik >/dev/null 2>&1 || true
+ fi
+ if docker_wait_ready "$previous" "$traefik_existed"; then
+ echo "Docker was rolled back to $previous. The packages it used are in $DOCKER_ROLLBACK_DIR." >&2
+ exit 1
+ fi
+ fi
+ echo "Error: the Docker rollback did not finish. Finish it by hand (the Engine should report $previous):" >&2
+ echo " apt-get install -y --allow-downgrades --no-download $DOCKER_ROLLBACK_DIR/*.deb" >&2
+ echo " systemctl start docker" >&2
+ echo " docker start dokploy-traefik" >&2
+ echo " docker version --format '{{.Server.Version}}'" >&2
+ exit 1
+}
+
+docker_update_from_apt() {
+ local previous="" traefik_existed=0
+ previous="$($SUDO_CMD docker version --format '{{.Server.Version}}' 2>/dev/null || true)"
+ if $SUDO_CMD docker inspect dokploy-traefik >/dev/null 2>&1; then
+ traefik_existed=1
+ fi
+ if ! $SUDO_CMD apt-get install -y --no-download "docker-ce=$DOCKER_PACKAGE_VERSION" "docker-ce-cli=$DOCKER_PACKAGE_VERSION" containerd.io docker-buildx-plugin docker-compose-plugin; then
+ echo "Error: installing the pre-downloaded Docker packages failed." >&2
+ docker_roll_back "$previous" "$traefik_existed"
+ fi
+ if ! docker_wait_ready "${PINNED_VERSIONS.docker}" "$traefik_existed"; then
+ echo "Error: Docker ${PINNED_VERSIONS.docker} did not come up healthy after the install." >&2
+ docker_roll_back "$previous" "$traefik_existed"
+ fi
+}
+`;
+
+const dockerPredownload = () => `
+if command -v apt-get >/dev/null 2>&1; then
+ DOCKER_FROM_APT=1
+ if docker_check_repo_codename; then
+ docker_prepare_apt
+ else
+ DOCKER_SKIPPED=1
+ fi
+else
+ GET_DOCKER_SCRIPT="$(mktemp)"
+ download_with_retry curl -fsSL https://get.docker.com -o "$GET_DOCKER_SCRIPT" || abort_before_change "Pre-download failed: get.docker.com."
+ echo "No apt-get on this host: the Docker packages are not pre-downloaded; the Docker step fetches them."
+fi
+`;
+
// Runs before any step: a failed download stops the update while nothing
// running has been touched. Only Docker and Traefik replace running services;
// the other components are plain binaries with no running state to protect.
@@ -105,17 +316,7 @@ const predownloadGate = (components: UpdatableComponent[]) => {
);
}
if (components.includes("docker")) {
- const dockerVersionPattern = PINNED_VERSIONS.docker.replace(/\./g, "\\.");
- downloads.push(`GET_DOCKER_SCRIPT="$(mktemp)"
-download_with_retry curl -fsSL https://get.docker.com -o "$GET_DOCKER_SCRIPT" || abort_before_change "Pre-download failed: get.docker.com."
-if command -v apt-get >/dev/null 2>&1; then
- $SUDO_CMD apt-get update -qq || abort_before_change "Pre-download failed: the apt package lists could not be refreshed."
- DOCKER_PACKAGE_VERSION="$(apt-cache madison docker-ce | awk -F'|' '{ gsub(/ /, "", $2); print $2 }' | grep -m1 -E '^([0-9]+:)?${dockerVersionPattern}-' || true)"
- [ -n "$DOCKER_PACKAGE_VERSION" ] || abort_before_change "Pre-download failed: Docker ${PINNED_VERSIONS.docker} is not in the apt sources."
- download_with_retry $SUDO_CMD apt-get install -y -qq --download-only "docker-ce=$DOCKER_PACKAGE_VERSION" "docker-ce-cli=$DOCKER_PACKAGE_VERSION" containerd.io docker-buildx-plugin docker-compose-plugin || abort_before_change "Pre-download failed: the Docker packages."
-else
- echo "No apt-get on this host: the Docker packages are not pre-downloaded; the Docker step fetches them."
-fi`);
+ downloads.push(dockerPredownload());
}
if (downloads.length === 0) {
return "";
@@ -136,6 +337,7 @@ download_with_retry() {
done
return 1
}
+${components.includes("docker") ? dockerHelpers() : ""}
echo "Downloading update artifacts before anything is changed"
${downloads.join("\n")}
`;
@@ -145,10 +347,18 @@ const updateStepFor = (component: UpdatableComponent) => {
switch (component) {
case "docker":
return `
-echo "Updating Docker to ${PINNED_VERSIONS.docker}"
-$SUDO_CMD sh "$GET_DOCKER_SCRIPT" --version ${PINNED_VERSIONS.docker}
-rm -f "$GET_DOCKER_SCRIPT"
-echo "Docker version ${PINNED_VERSIONS.docker} installed ✅"
+if [ "$DOCKER_SKIPPED" = 1 ]; then
+ echo "Docker update skipped: its apt repository does not match this release (see above)."
+else
+ echo "Updating Docker to ${PINNED_VERSIONS.docker}"
+ if [ "$DOCKER_FROM_APT" = 1 ]; then
+ docker_update_from_apt
+ else
+ $SUDO_CMD sh "$GET_DOCKER_SCRIPT" --version ${PINNED_VERSIONS.docker}
+ rm -f "$GET_DOCKER_SCRIPT"
+ fi
+ echo "Docker version ${PINNED_VERSIONS.docker} installed ✅"
+fi
`;
case "rclone":
return `
@@ -171,13 +381,26 @@ restore_traefik() {
exit 1
}
trap restore_traefik ERR HUP INT TERM
+traefik_networks="$($SUDO_CMD docker inspect -f '{{range $k,$v := .NetworkSettings.Networks}}{{$k}} {{end}}' dokploy-traefik 2>/dev/null || true)"
if $SUDO_CMD docker inspect dokploy-traefik >/dev/null 2>&1; then
- $SUDO_CMD docker stop dokploy-traefik >/dev/null
$SUDO_CMD docker rm -f dokploy-traefik-previous >/dev/null 2>&1 || true
$SUDO_CMD docker rename dokploy-traefik dokploy-traefik-previous
traefik_previous_kept=1
fi
-${buildTraefikRunCommand(TRAEFIK_VERSION)}
+${buildTraefikCreateCommand(TRAEFIK_VERSION)}
+$SUDO_CMD docker network connect dokploy-network dokploy-traefik
+for traefik_network in $traefik_networks; do
+ case "$traefik_network" in
+ bridge | dokploy-network) continue ;;
+ esac
+ if $SUDO_CMD docker network inspect "$traefik_network" >/dev/null 2>&1; then
+ $SUDO_CMD docker network connect "$traefik_network" dokploy-traefik >/dev/null
+ fi
+done
+if [ "$traefik_previous_kept" = 1 ]; then
+ $SUDO_CMD docker stop dokploy-traefik-previous >/dev/null
+fi
+$SUDO_CMD docker start dokploy-traefik >/dev/null
sleep "\${TRAEFIK_SETTLE_SECONDS:-10}"
if [ "$($SUDO_CMD docker inspect -f '{{.State.Running}}' dokploy-traefik 2>/dev/null)" != "true" ]; then
restore_traefik
@@ -285,10 +508,15 @@ export const updateServerComponents = async (
serverId: string,
components: UpdatableComponent[],
onData?: (data: string) => void,
+ upgradeDocker = false,
) => {
+ const plan = filterDockerUpgrade(components, upgradeDocker);
+ if (plan.skippedDocker) {
+ onData?.(`${DOCKER_UPGRADE_SKIPPED_MESSAGE}\n`);
+ }
const { exitCode } = await runSshCommand(
serverId,
- buildComponentUpdateScript(components),
+ buildComponentUpdateScript(plan.components),
onData,
);
if (exitCode !== 0) {
diff --git a/packages/server/src/setup/traefik-setup.ts b/packages/server/src/setup/traefik-setup.ts
index 012996bc69e..d909ae66bb0 100644
--- a/packages/server/src/setup/traefik-setup.ts
+++ b/packages/server/src/setup/traefik-setup.ts
@@ -24,20 +24,26 @@ export const TRAEFIK_HTTP3_PORT =
export const TRAEFIK_VERSION =
process.env.TRAEFIK_VERSION || PINNED_VERSIONS.traefik;
+const traefikContainerOptions = (version: string) => `--name dokploy-traefik \
+ --restart always \
+ -v /etc/dokploy/traefik/traefik.yml:/etc/traefik/traefik.yml \
+ -v /etc/dokploy/traefik/dynamic:/etc/dokploy/traefik/dynamic \
+ -v /var/run/docker.sock:/var/run/docker.sock \
+ -p ${TRAEFIK_SSL_PORT}:${TRAEFIK_SSL_PORT} \
+ -p ${TRAEFIK_PORT}:${TRAEFIK_PORT} \
+ -p ${TRAEFIK_HTTP3_PORT}:${TRAEFIK_HTTP3_PORT}/udp \
+ traefik:v${version}`;
+
export const buildTraefikRunCommand = (version: string) => `
- $SUDO_CMD docker run -d \
- --name dokploy-traefik \
- --restart always \
- -v /etc/dokploy/traefik/traefik.yml:/etc/traefik/traefik.yml \
- -v /etc/dokploy/traefik/dynamic:/etc/dokploy/traefik/dynamic \
- -v /var/run/docker.sock:/var/run/docker.sock \
- -p ${TRAEFIK_SSL_PORT}:${TRAEFIK_SSL_PORT} \
- -p ${TRAEFIK_PORT}:${TRAEFIK_PORT} \
- -p ${TRAEFIK_HTTP3_PORT}:${TRAEFIK_HTTP3_PORT}/udp \
- traefik:v${version}
+ $SUDO_CMD docker run -d ${traefikContainerOptions(version)}
$SUDO_CMD docker network connect dokploy-network dokploy-traefik
`;
+// Created but not started, so networks are attached before the ports are taken.
+export const buildTraefikCreateCommand = (version: string) => `
+ $SUDO_CMD docker create ${traefikContainerOptions(version)}
+`;
+
export interface TraefikOptions {
env?: string[];
serverId?: string;
From 2b92166a29919fe7b46610198a08358f59b1da71 Mon Sep 17 00:00:00 2001
From: merge-check <140916359+agentHits@users.noreply.github.com>
Date: Thu, 8 Oct 2026 09:56:52 -0400
Subject: [PATCH 18/33] feat(servers): make the Docker Engine upgrade opt-in in
the Components dialog
The component update takes upgradeDocker (default false). The Components
dialog shows an unchecked "Upgrade Docker Engine (restarts all containers on
this server)" checkbox whenever Docker is among the outdated components, and
sends its value with the update. Without it, Docker is skipped and the update
says so in its log.
Co-Authored-By: Claude Haiku 5.5
---
.../servers/server-components-update.tsx | 28 +++++++++++++++++--
apps/dokploy/server/api/routers/server.ts | 12 ++++++--
2 files changed, 35 insertions(+), 5 deletions(-)
diff --git a/apps/dokploy/components/dashboard/settings/servers/server-components-update.tsx b/apps/dokploy/components/dashboard/settings/servers/server-components-update.tsx
index e9c19faac56..d807422d865 100644
--- a/apps/dokploy/components/dashboard/settings/servers/server-components-update.tsx
+++ b/apps/dokploy/components/dashboard/settings/servers/server-components-update.tsx
@@ -2,6 +2,7 @@ import { AlertTriangle, CheckCircle2, Loader2, RefreshCw } from "lucide-react";
import { useState } from "react";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
+import { Checkbox } from "@/components/ui/checkbox";
import {
Dialog,
DialogContent,
@@ -10,6 +11,7 @@ import {
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
+import { Label } from "@/components/ui/label";
import { cn } from "@/lib/utils";
import { api } from "@/utils/api";
@@ -39,6 +41,7 @@ export const ServerComponentsUpdate = ({ serverId }: Props) => {
const [isUpdating, setIsUpdating] = useState(false);
const [selected, setSelected] = useState([]);
const [logs, setLogs] = useState("");
+ const [upgradeDocker, setUpgradeDocker] = useState(false);
const { data, isPending, isError, error, refetch } =
api.server.components.useQuery(
@@ -57,7 +60,7 @@ export const ServerComponentsUpdate = ({ serverId }: Props) => {
const hasUpdates = outdated.length > 0;
api.server.updateComponentsWithLogs.useSubscription(
- { serverId, components: selected },
+ { serverId, components: selected, upgradeDocker },
{
enabled: isUpdating,
onData(log) {
@@ -87,7 +90,15 @@ export const ServerComponentsUpdate = ({ serverId }: Props) => {
};
return (
-
)}
+ {updatable.includes("docker") && (
+
+ setUpgradeDocker(checked === true)}
+ />
+
+
+ )}
+
{updatable.includes("traefik") && (
Traefik is recreated during the update. Services on this server are
diff --git a/apps/dokploy/server/api/routers/server.ts b/apps/dokploy/server/api/routers/server.ts
index 07c420aee7c..fb15250aa51 100644
--- a/apps/dokploy/server/api/routers/server.ts
+++ b/apps/dokploy/server/api/routers/server.ts
@@ -590,6 +590,7 @@ export const serverRouter = createTRPCRouter({
z.object({
serverId: z.string(),
components: z.array(z.enum(UPDATABLE_COMPONENTS)).min(1),
+ upgradeDocker: z.boolean().default(false),
}),
)
.subscription(async ({ input, ctx }) => {
@@ -602,9 +603,14 @@ export const serverRouter = createTRPCRouter({
});
}
return observable((emit) => {
- updateServerComponents(input.serverId, input.components, (log) => {
- emit.next(log);
- })
+ updateServerComponents(
+ input.serverId,
+ input.components,
+ (log) => {
+ emit.next(log);
+ },
+ input.upgradeDocker,
+ )
.catch((error) => {
emit.next(
`${error instanceof Error ? error.message : error}\n${COMPONENTS_UPDATE_FAILED}\n`,
From 2d57262c32887dabe394e61fd08957fec37df529 Mon Sep 17 00:00:00 2001
From: merge-check <140916359+agentHits@users.noreply.github.com>
Date: Thu, 8 Oct 2026 09:56:52 -0400
Subject: [PATCH 19/33] docs: document Docker engine upgrade and rollback
Explains that the panel host upgrade of the Docker Engine needs
DOCKER_ENGINE_UPGRADE=1 in the environment of the update command, what is
saved for rollback and where, the checks after the upgrade, and the commands
to roll back by hand. Describes the new order in which Traefik is replaced.
Co-Authored-By: Claude Haiku 5.5
---
docs/agenthits-install.md | 47 ++++++++++++++++++++++++++++++++++-----
1 file changed, 41 insertions(+), 6 deletions(-)
diff --git a/docs/agenthits-install.md b/docs/agenthits-install.md
index a2a62bba027..e33e7fcc99b 100644
--- a/docs/agenthits-install.md
+++ b/docs/agenthits-install.md
@@ -239,9 +239,12 @@ Windows (PowerShell от администратора):
0600) и не удаляется автоматически. Делается, только если Postgres будет заменён, до любой замены.
2. Redis: `docker service update` с остановкой старой задачи до запуска новой и
откатом при сбое. Ждёт ответа `PONG`.
-3. Traefik: старый контейнер останавливается и переименовывается в
- `dokploy-traefik-previous`, новый запускается и подключается к
- `dokploy-network`. Через `DOKPLOY_TRAEFIK_SETTLE` секунд (10 по умолчанию)
+3. Traefik: старый контейнер переименовывается в `dokploy-traefik-previous`, новый
+ создаётся и подключается к `dokploy-network` и ко всем сетям, к которым был
+ подключен старый контейнер (кроме `bridge`); сети читаются на хосте во время
+ обновления. Затем старый контейнер останавливается и запускается новый. Пауза,
+ когда Traefik не работает, тем самым ограничена остановкой старого контейнера
+ и запуском нового. Через `DOKPLOY_TRAEFIK_SETTLE` секунд (10 по умолчанию)
проверяется, что он работает; если нет, старый контейнер возвращается.
Старый контейнер остаётся остановленным, с `--restart no`, чтобы перезапуск
демона не запустил его рядом с новым. При следующем обновлении Traefik он
@@ -251,9 +254,27 @@ Windows (PowerShell от администратора):
5. Панель: `update.sh`. Ждёт `healthy`-статуса контейнера; при откате swarm или
нездоровом статусе команда завершается с ошибкой.
6. Docker Engine: только при `DOCKER_ENGINE_UPGRADE=1` и если установленная
- версия отличается от `DOCKER_VERSION`. Пакеты скачиваются заранее, установка
- идёт в самом конце. Перезапуск демона перезапускает все сервисы swarm на
- хосте: live-restore к сервисам swarm не применяется.
+ версия отличается от `DOCKER_VERSION`. Включайте переменную в окружении
+ команды `update`, например `DOCKER_ENGINE_UPGRADE=1 bash install-agenthits.sh update`;
+ она передаётся через launcher на macOS и в Windows (WSL). По умолчанию выключено.
+ Перед любыми изменениями installer сохраняет текущие пакеты Docker в
+ `DOKPLOY_BACKUP_DIR/docker-rollback-/` (каталог 0700, файлы 0600):
+ `docker-ce` и `docker-ce-cli` всегда, остальные пакеты, если их версия меняется.
+ Если сохранить пакеты не удалось, обновление останавливается с сообщением
+ `Pre-download failed: current Docker packages for rollback. Nothing was changed.`
+ Если источник Docker в `/etc/apt/sources.list.d` предназначен для другого
+ релиза, обновление тоже останавливается до изменений: installer не переписывает
+ источник сам. Новые пакеты скачиваются заранее, установка идёт в самом конце.
+ Перезапуск демона перезапускает все сервисы swarm на хосте: live-restore к
+ сервисам swarm не применяется.
+ После установки installer ждёт до `DOKPLOY_HEALTH_TIMEOUT` секунд, пока
+ Docker Engine отвечает версией `DOCKER_VERSION`, swarm активен, `dokploy`,
+ `dokploy-postgres` и `dokploy-redis` готовы, а `dokploy-traefik` запущен.
+ Если установка пакетов не удалась или проверка не прошла, installer
+ автоматически устанавливает сохранённые пакеты обратно, ждёт демон,
+ запускает `dokploy-traefik`, проверяет старую версию и сервисы и завершает
+ команду с ошибкой с указанием каталога копии. Если откат тоже не удался,
+ выводятся команды для ручного отката (см. ниже).
Смена major-версии Postgres (например, 17 → 18) этим обновлением не выполняется:
команда останавливается до любых изменений. Перенос данных между major-версиями
@@ -267,6 +288,20 @@ Windows (PowerShell от администратора):
предыдущий образ печатается в логе шага. Для Traefik:
`docker rm -f dokploy-traefik && docker rename dokploy-traefik-previous dokploy-traefik && docker update --restart always dokploy-traefik && docker start dokploy-traefik`.
+Ручной откат Docker Engine, если автоматический не завершился. Каталог копии
+имеет вид `DOKPLOY_BACKUP_DIR/docker-rollback-/`, например
+`/var/backups/dokploy/docker-rollback-20261008T120000Z`; подставьте свой:
+
+```bash
+apt-get install -y --allow-downgrades --no-download /var/backups/dokploy/docker-rollback-/*.deb
+systemctl start docker
+docker start dokploy-traefik
+docker version --format '{{.Server.Version}}'
+```
+
+Последняя команда должна показать версию Engine, которая была до обновления.
+`docker start dokploy-traefik` безопасен, даже если контейнер уже запущен.
+
В dashboard кнопка `Check for updates` для AgentHits fork сравнивает текущий
Docker service image digest с `ghcr.io/agenthits/dokploy:agenthits-dev`.
Если digest отличается, обновление через UI запускает `docker service update`
From 422f343cfc32c1b352372f70ac73382508bd49e2 Mon Sep 17 00:00:00 2001
From: merge-check <140916359+agentHits@users.noreply.github.com>
Date: Thu, 8 Oct 2026 10:04:49 -0400
Subject: [PATCH 20/33] fix(update): set the Traefik restart policy before the
swap stops the old container
Before the replaced Traefik container is stopped, its restart policy is set to
no, so a daemon restart cannot start it next to the new container. When the
old container is put back, its restart policy is set to always before it is
started. The panel host installer and the remote server update follow the
same order.
Co-Authored-By: Claude Haiku 5.5
---
.../__test__/server/server-components.test.ts | 53 +++++++++++++++++++
.../settings/install-agenthits-update.test.ts | 27 ++++++++++
install-agenthits.sh | 4 +-
.../server/src/setup/server-components.ts | 2 +
4 files changed, 84 insertions(+), 2 deletions(-)
diff --git a/apps/dokploy/__test__/server/server-components.test.ts b/apps/dokploy/__test__/server/server-components.test.ts
index ce0dbb00051..006f25a16c3 100644
--- a/apps/dokploy/__test__/server/server-components.test.ts
+++ b/apps/dokploy/__test__/server/server-components.test.ts
@@ -159,6 +159,24 @@ describe("buildComponentUpdateScript", () => {
expect(script).toContain("bridge | dokploy-network) continue ;;");
});
+ it("sets the restart policy of the replaced Traefik around the swap", () => {
+ const script = buildComponentUpdateScript(["traefik"]);
+ const restore = script.slice(
+ script.indexOf("restore_traefik() {"),
+ script.indexOf("trap restore_traefik ERR"),
+ );
+
+ expect(
+ script.indexOf("docker update --restart no dokploy-traefik-previous"),
+ ).toBeLessThan(script.indexOf("docker stop dokploy-traefik-previous"));
+ expect(
+ restore.indexOf("docker update --restart always dokploy-traefik"),
+ ).toBeGreaterThanOrEqual(0);
+ expect(
+ restore.indexOf("docker update --restart always dokploy-traefik"),
+ ).toBeLessThan(restore.indexOf("docker start dokploy-traefik"));
+ });
+
it("passes the pinned versions into the installers through env", () => {
const script = buildComponentUpdateScript(["nixpacks", "railpack"]);
@@ -908,6 +926,41 @@ describe("buildComponentUpdateScript run order", () => {
},
SPAWN_TEST_TIMEOUT_MS,
);
+ it(
+ "sets --restart no on the replaced Traefik before stopping it",
+ () => {
+ const run = runComponentScript(["traefik"]);
+
+ expect(run.status).toBe(0);
+ const noRestart = run.calls.indexOf(
+ "docker update --restart no dokploy-traefik-previous",
+ );
+ expect(noRestart).toBeGreaterThanOrEqual(0);
+ expect(noRestart).toBeLessThan(
+ run.calls.indexOf("docker stop dokploy-traefik-previous"),
+ );
+ },
+ SPAWN_TEST_TIMEOUT_MS,
+ );
+
+ it(
+ "restores --restart always on the Traefik it puts back",
+ () => {
+ const run = runComponentScript(["traefik"], {
+ env: { FAKE_NOT_RUNNING: "1" },
+ });
+
+ expect(run.status).not.toBe(0);
+ const restore = run.calls.indexOf(
+ "docker update --restart always dokploy-traefik",
+ );
+ expect(restore).toBeGreaterThanOrEqual(0);
+ expect(restore).toBeLessThan(
+ run.calls.lastIndexOf("docker start dokploy-traefik"),
+ );
+ },
+ SPAWN_TEST_TIMEOUT_MS,
+ );
});
describe("buildTraefikCreateCommand", () => {
diff --git a/apps/dokploy/__test__/settings/install-agenthits-update.test.ts b/apps/dokploy/__test__/settings/install-agenthits-update.test.ts
index 9b8a3cd44d4..38316e3d944 100644
--- a/apps/dokploy/__test__/settings/install-agenthits-update.test.ts
+++ b/apps/dokploy/__test__/settings/install-agenthits-update.test.ts
@@ -858,3 +858,30 @@ describe("install-agenthits.sh Traefik and host settings", () => {
expect(installer).not.toContain("docker run -d");
});
});
+
+describe("install-agenthits.sh Traefik restart policy", () => {
+ it("sets --restart no on the replaced Traefik before stopping it", () => {
+ const { result, calls } = runInstaller({ traefikImage: "traefik:v3.6.25" });
+
+ expect(result.status).toBe(0);
+ const noRestart = calls.indexOf(
+ "update --restart no dokploy-traefik-previous",
+ );
+ expect(noRestart).toBeGreaterThanOrEqual(0);
+ expect(noRestart).toBeLessThan(
+ calls.indexOf("stop dokploy-traefik-previous"),
+ );
+ });
+
+ it("restores --restart always on the Traefik it puts back", () => {
+ const { result, calls } = runInstaller({
+ traefikImage: "traefik:v3.6.25",
+ traefikRunning: "false",
+ });
+
+ expect(result.status).not.toBe(0);
+ const restore = calls.indexOf("update --restart always dokploy-traefik");
+ expect(restore).toBeGreaterThanOrEqual(0);
+ expect(restore).toBeLessThan(calls.lastIndexOf("start dokploy-traefik"));
+ });
+});
diff --git a/install-agenthits.sh b/install-agenthits.sh
index c465b56d207..9a99ff88a30 100755
--- a/install-agenthits.sh
+++ b/install-agenthits.sh
@@ -1360,12 +1360,12 @@ swap_dokploy_traefik() {
docker rm -f dokploy-traefik-previous >/dev/null 2>&1 || true
docker rename dokploy-traefik dokploy-traefik-previous
TRAEFIK_PREVIOUS_KEPT=1
- # A stopped container with restart=always is started again when the daemon restarts.
- docker update --restart no dokploy-traefik-previous >/dev/null
fi
create_dokploy_traefik "$TRAEFIK_IMAGE"
if [ "$TRAEFIK_PREVIOUS_KEPT" = "1" ]; then
+ # A stopped container with restart=always is started again when the daemon restarts.
+ docker update --restart no dokploy-traefik-previous >/dev/null
docker stop dokploy-traefik-previous >/dev/null
fi
docker start dokploy-traefik >/dev/null
diff --git a/packages/server/src/setup/server-components.ts b/packages/server/src/setup/server-components.ts
index 97526ca5885..f38f9989861 100644
--- a/packages/server/src/setup/server-components.ts
+++ b/packages/server/src/setup/server-components.ts
@@ -375,6 +375,7 @@ restore_traefik() {
if [ "$traefik_previous_kept" = 1 ]; then
$SUDO_CMD docker rm -f dokploy-traefik >/dev/null 2>&1 || true
$SUDO_CMD docker rename dokploy-traefik-previous dokploy-traefik || true
+ $SUDO_CMD docker update --restart always dokploy-traefik >/dev/null 2>&1 || true
fi
$SUDO_CMD docker start dokploy-traefik >/dev/null 2>&1 || true
fi
@@ -398,6 +399,7 @@ for traefik_network in $traefik_networks; do
fi
done
if [ "$traefik_previous_kept" = 1 ]; then
+ $SUDO_CMD docker update --restart no dokploy-traefik-previous >/dev/null
$SUDO_CMD docker stop dokploy-traefik-previous >/dev/null
fi
$SUDO_CMD docker start dokploy-traefik >/dev/null
From c0b76c9a39fe1e104ac20aa2d71a816db86dc80f Mon Sep 17 00:00:00 2001
From: merge-check <140916359+agentHits@users.noreply.github.com>
Date: Thu, 8 Oct 2026 10:07:33 -0400
Subject: [PATCH 21/33] fix(servers): skip only the Docker step on
Docker-specific pre-download failures
A Docker-specific failure skips the Docker step with one message and lets the
other components run: a Docker apt source for another release, no pinned
candidate after the repository check, an unsupported distribution, a failure
to save the rollback packages (including hosts without docker-ce), and a host
without apt-get. Every other pre-download failure, such as the Traefik image,
the apt lists or the Docker packages, still aborts the whole update before any
change. get.docker.com is no longer run; a host without apt-get skips Docker
instead.
Co-Authored-By: Claude Haiku 5.5
---
.../__test__/server/server-components.test.ts | 324 ++++++++++++++----
.../server/src/setup/server-components.ts | 110 +++---
2 files changed, 327 insertions(+), 107 deletions(-)
diff --git a/apps/dokploy/__test__/server/server-components.test.ts b/apps/dokploy/__test__/server/server-components.test.ts
index 006f25a16c3..fd8eee3b2ca 100644
--- a/apps/dokploy/__test__/server/server-components.test.ts
+++ b/apps/dokploy/__test__/server/server-components.test.ts
@@ -8,6 +8,7 @@ import {
readFileSync,
rmSync,
statSync,
+ symlinkSync,
writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
@@ -191,19 +192,17 @@ describe("buildComponentUpdateScript", () => {
it("installs the pinned Docker and RClone", () => {
const script = buildComponentUpdateScript(["docker", "rclone"]);
- expect(script).toContain(`--version ${PINNED_VERSIONS.docker}`);
+ expect(script).toContain(`Updating Docker to ${PINNED_VERSIONS.docker}`);
expect(script).toContain(
`rclone-v${PINNED_VERSIONS.rclone}-linux-$RCLONE_RELEASE_ARCH.zip`,
);
});
- it("installs Docker from the pre-downloaded apt packages, and runs get.docker.com only without apt", () => {
+ it("installs Docker from the pre-downloaded apt packages and never runs get.docker.com", () => {
const script = buildComponentUpdateScript(["docker"]);
expect(script).toContain("apt-get install -y --no-download");
- expect(script.indexOf("if command -v apt-get")).toBeLessThan(
- script.indexOf("get.docker.com -o"),
- );
+ expect(script).not.toContain("get.docker.com");
});
it("keeps a rollback for a failed Docker update in the generated script", () => {
@@ -215,14 +214,43 @@ describe("buildComponentUpdateScript", () => {
expect(script).toContain("docker-rollback-");
});
- it("skips only the Docker step when the apt repository is for another release", () => {
+ it("skips only the Docker step for each Docker-specific pre-download failure", () => {
const script = buildComponentUpdateScript(["docker", "traefik"]);
expect(script).toContain("DOCKER_SKIPPED=1");
expect(script).toContain(
- "Docker not updated: the Docker apt repository in",
+ "Docker not updated: $DOCKER_SKIP_REASON. The other components continue.",
+ );
+ expect(script).toContain(
+ "this host has no apt-get, so the Docker packages cannot be pre-downloaded or rolled back",
+ );
+ expect(script).toContain("is for $repo_codename, but this host runs");
+ expect(script).toContain(
+ "is not offered by the Docker apt repository configured on this host",
+ );
+ expect(script).toContain(
+ "is only added automatically on ubuntu and debian",
+ );
+ expect(script).toContain(
+ "the current Docker packages could not be saved for rollback",
+ );
+ expect(script).toContain(
+ "docker-ce is not installed, so there is no Docker Engine to roll back to",
+ );
+ });
+
+ it("aborts the whole update before any change for other pre-download failures", () => {
+ const script = buildComponentUpdateScript(["docker", "traefik"]);
+
+ expect(script).toContain(
+ `abort_before_change "Pre-download failed: traefik:v${TRAEFIK_VERSION}."`,
+ );
+ expect(script).toContain(
+ 'abort_before_change "Pre-download failed: the apt package lists could not be refreshed."',
+ );
+ expect(script).toContain(
+ 'abort_before_change "Pre-download failed: the Docker packages."',
);
- expect(script).toContain("Docker update skipped");
});
it("ends with the marker the UI waits for", () => {
@@ -282,6 +310,10 @@ describe("filterDockerUpgrade", () => {
});
});
+const BASH = spawnSync("sh", ["-c", "command -v bash"], {
+ encoding: "utf8",
+}).stdout.trim();
+
const FAKE_TOOLS: Record = {
docker: `#!/bin/sh
printf 'docker %s\\n' "$*" >> "$CALL_LOG"
@@ -309,13 +341,18 @@ exit 0
"apt-get": `#!/bin/sh
printf 'apt-get %s\\n' "$*" >> "$CALL_LOG"
case "$1" in
- update) touch "$FAKE_STATE/updated" ;;
+ update)
+ [ "$FAKE_UPDATE_FAILS" = 1 ] && exit 1
+ ;;
download)
[ "$FAKE_DOWNLOAD_FAILS" = 1 ] && exit 1
printf 'fake package\\n' > "\${2%%=*}.deb"
;;
install)
case "$*" in
+ *--download-only*)
+ [ "$FAKE_PREDOWNLOAD_FAILS" = 1 ] && exit 1
+ ;;
*--allow-downgrades*)
[ "$FAKE_ROLLBACK_FAILS" = 1 ] && exit 1
printf '%s\\n' "$FAKE_PREVIOUS_ENGINE" > "$FAKE_STATE/engine"
@@ -363,7 +400,8 @@ printf 'dpkg-query %s\\n' "$*" >> "$CALL_LOG"
case "$2" in
*Status-Status*)
case "$3" in
- docker-ce | docker-ce-cli | containerd.io | docker-buildx-plugin | docker-compose-plugin) echo installed ;;
+ docker-ce) [ "$FAKE_NO_DOCKER_CE" = 1 ] || echo installed ;;
+ docker-ce-cli | containerd.io | docker-buildx-plugin | docker-compose-plugin) echo installed ;;
esac
;;
*Version*)
@@ -400,6 +438,41 @@ exit 0
`,
};
+const SYSTEM_TOOLS = [
+ "id",
+ "awk",
+ "grep",
+ "date",
+ "mktemp",
+ "tee",
+ "dirname",
+ "install",
+ "rm",
+ "chmod",
+ "mkdir",
+ "sh",
+ "sed",
+ "cat",
+ "head",
+ "tr",
+ "cut",
+ "wc",
+ "sort",
+];
+
+const linkSystemTools = (binDir: string) => {
+ mkdirSync(binDir);
+ for (const name of SYSTEM_TOOLS) {
+ const found = spawnSync("sh", ["-c", `command -v ${name}`], {
+ encoding: "utf8",
+ }).stdout.trim();
+ if (found) {
+ symlinkSync(found, path.join(binDir, name));
+ }
+ }
+ return binDir;
+};
+
const DOCKER_LIST_JAMMY =
"deb [arch=amd64 signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu jammy stable\n";
const DOCKER_LIST_NOBLE =
@@ -411,6 +484,7 @@ type ScriptOptions = {
env?: Record;
osRelease?: string;
sourceFiles?: Record;
+ withoutApt?: boolean;
};
const listBackups = (backups: string) => {
@@ -461,15 +535,21 @@ const runComponentScript = (
writeFileSync(path.join(sources, name), body);
}
for (const [name, body] of Object.entries(FAKE_TOOLS)) {
+ if (options.withoutApt && name.startsWith("apt-")) {
+ continue;
+ }
writeFileSync(path.join(dir, name), body);
chmodSync(path.join(dir, name), 0o755);
}
- const result = spawnSync("bash", [], {
+ const pathValue = options.withoutApt
+ ? `${dir}:${linkSystemTools(path.join(dir, "bin"))}`
+ : `${dir}:${process.env.PATH}`;
+ const result = spawnSync(BASH, [], {
input: buildComponentUpdateScript(components),
encoding: "utf8",
env: {
...process.env,
- PATH: `${dir}:${process.env.PATH}`,
+ PATH: pathValue,
CALL_LOG: callLog,
FAKE_STATE: dir,
FAKE_PREVIOUS_ENGINE: "28.3.0",
@@ -519,6 +599,68 @@ describe("buildComponentUpdateScript run order", () => {
SPAWN_TEST_TIMEOUT_MS,
);
+ it(
+ "aborts the whole update before any change when the Traefik image cannot be pulled, with Docker selected",
+ () => {
+ const run = runComponentScript(["docker", "traefik"], {
+ env: { FAKE_PULL_FAILS: "1" },
+ });
+
+ expect(run.status).not.toBe(0);
+ expect(run.stderr).toContain("Nothing was changed");
+ expect(run.calls.some((call) => call.startsWith("apt-get"))).toBe(false);
+ expect(run.calls.some((call) => call.startsWith("docker rename"))).toBe(
+ false,
+ );
+ },
+ SPAWN_TEST_TIMEOUT_MS,
+ );
+
+ it(
+ "aborts the whole update before any change when the apt package lists cannot be refreshed",
+ () => {
+ const run = runComponentScript(["docker", "traefik"], {
+ env: { FAKE_UPDATE_FAILS: "1" },
+ });
+
+ expect(run.status).not.toBe(0);
+ expect(run.stderr).toContain(
+ "Pre-download failed: the apt package lists could not be refreshed. Nothing was changed.",
+ );
+ expect(run.calls.some((call) => call.startsWith("docker rename"))).toBe(
+ false,
+ );
+ expect(run.calls.some((call) => call.startsWith("docker create"))).toBe(
+ false,
+ );
+ expect(run.calls.some((call) => call.startsWith("apt-get install"))).toBe(
+ false,
+ );
+ },
+ SPAWN_TEST_TIMEOUT_MS,
+ );
+
+ it(
+ "aborts the whole update before any change when the Docker packages cannot be downloaded",
+ () => {
+ const run = runComponentScript(["docker", "traefik"], {
+ env: { FAKE_PREDOWNLOAD_FAILS: "1" },
+ });
+
+ expect(run.status).not.toBe(0);
+ expect(run.stderr).toContain(
+ "Pre-download failed: the Docker packages. Nothing was changed.",
+ );
+ expect(run.calls.some((call) => call.startsWith("docker rename"))).toBe(
+ false,
+ );
+ expect(run.calls.some((call) => call.includes("--no-download"))).toBe(
+ false,
+ );
+ },
+ SPAWN_TEST_TIMEOUT_MS,
+ );
+
it(
"downloads every artifact before the first step changes anything",
() => {
@@ -558,6 +700,7 @@ describe("buildComponentUpdateScript run order", () => {
at("docker rename dokploy-traefik dokploy-traefik-previous"),
at("docker create --name dokploy-traefik"),
at("docker network connect dokploy-network dokploy-traefik"),
+ at("docker update --restart no dokploy-traefik-previous"),
at("docker stop dokploy-traefik-previous"),
at("docker start dokploy-traefik"),
at("docker inspect -f {{.State.Running}} dokploy-traefik"),
@@ -569,6 +712,42 @@ describe("buildComponentUpdateScript run order", () => {
SPAWN_TEST_TIMEOUT_MS,
);
+ it(
+ "sets --restart no on the replaced Traefik before stopping it",
+ () => {
+ const run = runComponentScript(["traefik"]);
+
+ expect(run.status).toBe(0);
+ const noRestart = run.calls.indexOf(
+ "docker update --restart no dokploy-traefik-previous",
+ );
+ expect(noRestart).toBeGreaterThanOrEqual(0);
+ expect(noRestart).toBeLessThan(
+ run.calls.indexOf("docker stop dokploy-traefik-previous"),
+ );
+ },
+ SPAWN_TEST_TIMEOUT_MS,
+ );
+
+ it(
+ "restores --restart always on the Traefik it puts back",
+ () => {
+ const run = runComponentScript(["traefik"], {
+ env: { FAKE_NOT_RUNNING: "1" },
+ });
+
+ expect(run.status).not.toBe(0);
+ const restore = run.calls.indexOf(
+ "docker update --restart always dokploy-traefik",
+ );
+ expect(restore).toBeGreaterThanOrEqual(0);
+ expect(restore).toBeLessThan(
+ run.calls.lastIndexOf("docker start dokploy-traefik"),
+ );
+ },
+ SPAWN_TEST_TIMEOUT_MS,
+ );
+
it(
"restores the previous Traefik container when the new one is not running",
() => {
@@ -786,37 +965,76 @@ describe("buildComponentUpdateScript run order", () => {
);
it(
- "stops before any change when a current Docker package cannot be saved",
+ "skips only Docker when the current Docker packages cannot be saved",
() => {
- const run = runComponentScript(["docker"], {
+ const run = runComponentScript(["docker", "traefik"], {
env: { FAKE_DOWNLOAD_FAILS: "1" },
});
- expect(run.status).not.toBe(0);
- expect(run.stderr).toContain(
- "Pre-download failed: current Docker packages for rollback. Nothing was changed.",
+ expect(run.status).toBe(0);
+ expect(run.stdout).toContain(
+ "Docker not updated: the current Docker packages could not be saved for rollback. The other components continue.",
+ );
+ expect(run.stdout).toContain(
+ `Traefik version ${TRAEFIK_VERSION} installed`,
);
expect(run.calls.some((call) => call.startsWith("apt-get install"))).toBe(
false,
);
- expect(run.stdout).not.toContain("Updating Docker");
},
SPAWN_TEST_TIMEOUT_MS,
);
it(
- "skips only the Docker step when the apt repository is for another release",
+ "skips only Docker when docker-ce is not installed, as on hosts running docker.io",
() => {
const run = runComponentScript(["docker", "traefik"], {
- sourceFiles: { "docker.list": DOCKER_LIST_JAMMY },
+ env: { FAKE_NO_DOCKER_CE: "1" },
+ });
+
+ expect(run.status).toBe(0);
+ expect(run.stdout).toContain(
+ "Docker not updated: docker-ce is not installed, so there is no Docker Engine to roll back to.",
+ );
+ expect(run.stdout).toContain(
+ `Traefik version ${TRAEFIK_VERSION} installed`,
+ );
+ expect(run.calls.some((call) => call.startsWith("apt-get install"))).toBe(
+ false,
+ );
+ },
+ SPAWN_TEST_TIMEOUT_MS,
+ );
+
+ it(
+ "skips only Docker when apt-get is missing, and never runs get.docker.com",
+ () => {
+ const run = runComponentScript(["docker", "traefik"], {
+ withoutApt: true,
});
expect(run.status).toBe(0);
expect(run.stdout).toContain(
- "Docker not updated: the Docker apt repository in",
+ "Docker not updated: this host has no apt-get, so the Docker packages cannot be pre-downloaded or rolled back. The other components continue.",
);
+ expect(run.stdout).toContain(
+ `Traefik version ${TRAEFIK_VERSION} installed`,
+ );
+ expect(run.calls.some((call) => call.startsWith("curl"))).toBe(false);
+ },
+ SPAWN_TEST_TIMEOUT_MS,
+ );
+
+ it(
+ "skips only the Docker step when the apt repository is for another release",
+ () => {
+ const run = runComponentScript(["docker", "traefik"], {
+ sourceFiles: { "docker.list": DOCKER_LIST_JAMMY },
+ });
+
+ expect(run.status).toBe(0);
+ expect(run.stdout).toContain("Docker not updated:");
expect(run.stdout).toContain("is for jammy, but this host runs noble");
- expect(run.stdout).toContain("Docker update skipped");
expect(run.stdout).toContain(
`Traefik version ${TRAEFIK_VERSION} installed`,
);
@@ -889,15 +1107,20 @@ describe("buildComponentUpdateScript run order", () => {
);
it(
- "does not rewrite an existing Docker repository that does not offer the pinned version",
+ "skips only Docker, without rewriting it, when the repository does not offer the pinned version",
() => {
- const run = runComponentScript(["docker"], {
+ const run = runComponentScript(["docker", "traefik"], {
env: { FAKE_NO_CANDIDATE: "1" },
sourceFiles: { "docker.list": DOCKER_LIST_NOBLE },
});
- expect(run.status).not.toBe(0);
- expect(run.stderr).toContain("Nothing was changed");
+ expect(run.status).toBe(0);
+ expect(run.stdout).toContain(
+ "Docker not updated: Docker 29.8.2 is not offered by the Docker apt repository configured on this host.",
+ );
+ expect(run.stdout).toContain(
+ `Traefik version ${TRAEFIK_VERSION} installed`,
+ );
expect(run.sources["docker.list"]).toBe(DOCKER_LIST_NOBLE);
expect(run.keyringExists).toBe(false);
},
@@ -905,18 +1128,20 @@ describe("buildComponentUpdateScript run order", () => {
);
it(
- "stops before any change on an unsupported distribution without a Docker repository",
+ "skips only Docker on an unsupported distribution, without any change",
() => {
- const run = runComponentScript(["docker"], {
+ const run = runComponentScript(["docker", "traefik"], {
env: { FAKE_CANDIDATE_AFTER_REPO: "1" },
osRelease: "ID=fedora\nVERSION_CODENAME=40\n",
});
- expect(run.status).not.toBe(0);
- expect(run.stderr).toContain(
- "only added automatically on ubuntu and debian",
+ expect(run.status).toBe(0);
+ expect(run.stdout).toContain(
+ "is not in the apt sources, and Docker's repository is only added automatically on ubuntu and debian (this host is 'fedora').",
+ );
+ expect(run.stdout).toContain(
+ `Traefik version ${TRAEFIK_VERSION} installed`,
);
- expect(run.stderr).toContain("Nothing was changed");
expect(run.calls.some((call) => call.startsWith("curl"))).toBe(false);
expect(run.calls.some((call) => call.startsWith("apt-get install"))).toBe(
false,
@@ -926,41 +1151,6 @@ describe("buildComponentUpdateScript run order", () => {
},
SPAWN_TEST_TIMEOUT_MS,
);
- it(
- "sets --restart no on the replaced Traefik before stopping it",
- () => {
- const run = runComponentScript(["traefik"]);
-
- expect(run.status).toBe(0);
- const noRestart = run.calls.indexOf(
- "docker update --restart no dokploy-traefik-previous",
- );
- expect(noRestart).toBeGreaterThanOrEqual(0);
- expect(noRestart).toBeLessThan(
- run.calls.indexOf("docker stop dokploy-traefik-previous"),
- );
- },
- SPAWN_TEST_TIMEOUT_MS,
- );
-
- it(
- "restores --restart always on the Traefik it puts back",
- () => {
- const run = runComponentScript(["traefik"], {
- env: { FAKE_NOT_RUNNING: "1" },
- });
-
- expect(run.status).not.toBe(0);
- const restore = run.calls.indexOf(
- "docker update --restart always dokploy-traefik",
- );
- expect(restore).toBeGreaterThanOrEqual(0);
- expect(restore).toBeLessThan(
- run.calls.lastIndexOf("docker start dokploy-traefik"),
- );
- },
- SPAWN_TEST_TIMEOUT_MS,
- );
});
describe("buildTraefikCreateCommand", () => {
diff --git a/packages/server/src/setup/server-components.ts b/packages/server/src/setup/server-components.ts
index f38f9989861..d4067c31669 100644
--- a/packages/server/src/setup/server-components.ts
+++ b/packages/server/src/setup/server-components.ts
@@ -117,11 +117,16 @@ DOKPLOY_BACKUP_DIR="\${DOKPLOY_BACKUP_DIR:-/var/backups/dokploy}"
DOKPLOY_OS_RELEASE="\${DOKPLOY_OS_RELEASE:-/etc/os-release}"
DOKPLOY_APT_SOURCES_DIR="\${DOKPLOY_APT_SOURCES_DIR:-/etc/apt/sources.list.d}"
DOKPLOY_DOCKER_KEYRING="\${DOKPLOY_DOCKER_KEYRING:-/etc/apt/keyrings/docker.asc}"
-DOCKER_FROM_APT=0
DOCKER_SKIPPED=0
+DOCKER_SKIP_REASON=""
DOCKER_PACKAGE_VERSION=""
DOCKER_ROLLBACK_DIR=""
+docker_skip() {
+ DOCKER_SKIPPED=1
+ DOCKER_SKIP_REASON="$1"
+}
+
docker_installed_version() {
if [ "$(dpkg-query -W -f='\${db:Status-Status}' "$1" 2>/dev/null)" = "installed" ]; then
dpkg-query -W -f='\${Version}' "$1"
@@ -153,7 +158,7 @@ docker_check_repo_codename() {
host_codename="$( (. "$DOKPLOY_OS_RELEASE" && printf '%s' "\${VERSION_CODENAME:-}") 2>/dev/null || true)"
for repo_codename in $repo_codenames; do
if [ -z "$host_codename" ] || [ "$repo_codename" != "$host_codename" ]; then
- echo "Docker not updated: the Docker apt repository in $DOKPLOY_APT_SOURCES_DIR is for $repo_codename, but this host runs \${host_codename:-an unknown release}. Fix that source file yourself and run the update again. The other components continue."
+ docker_skip "the Docker apt repository in $DOKPLOY_APT_SOURCES_DIR is for $repo_codename, but this host runs \${host_codename:-an unknown release}; fix that source file yourself and run the update again"
return 1
fi
done
@@ -167,37 +172,57 @@ docker_pinned_candidate() {
docker_add_apt_repository() {
local id="" codename="" arch="" key=""
if [ -n "$(docker_repo_codenames)" ] || [ -e "$DOKPLOY_APT_SOURCES_DIR/docker.list" ]; then
- abort_before_change "Pre-download failed: Docker ${PINNED_VERSIONS.docker} is not offered by the Docker apt repository configured on this host."
+ docker_skip "Docker ${PINNED_VERSIONS.docker} is not offered by the Docker apt repository configured on this host"
+ return 1
fi
id="$( (. "$DOKPLOY_OS_RELEASE" && printf '%s' "\${ID:-}") 2>/dev/null || true)"
codename="$( (. "$DOKPLOY_OS_RELEASE" && printf '%s' "\${VERSION_CODENAME:-}") 2>/dev/null || true)"
case "$id" in
ubuntu | debian) ;;
*)
- abort_before_change "Pre-download failed: Docker ${PINNED_VERSIONS.docker} is not in the apt sources, and Docker's repository is only added automatically on ubuntu and debian (this host is '$id')."
+ docker_skip "Docker ${PINNED_VERSIONS.docker} is not in the apt sources, and Docker's repository is only added automatically on ubuntu and debian (this host is '$id')"
+ return 1
;;
esac
if [ -z "$codename" ]; then
- abort_before_change "Pre-download failed: VERSION_CODENAME is not set in $DOKPLOY_OS_RELEASE."
+ docker_skip "VERSION_CODENAME is not set in $DOKPLOY_OS_RELEASE, so Docker's repository cannot be added"
+ return 1
+ fi
+ if ! key="$(mktemp)"; then
+ docker_skip "a temporary file for Docker's apt key could not be created"
+ return 1
+ fi
+ if ! download_with_retry curl -fsSL "https://download.docker.com/linux/$id/gpg" -o "$key"; then
+ docker_skip "Docker's apt key could not be downloaded"
+ return 1
+ fi
+ if ! $SUDO_CMD install -d -m 0755 "$(dirname "$DOKPLOY_DOCKER_KEYRING")" || ! $SUDO_CMD install -m 0644 "$key" "$DOKPLOY_DOCKER_KEYRING"; then
+ docker_skip "Docker's apt key could not be installed"
+ return 1
fi
- key="$(mktemp)"
- download_with_retry curl -fsSL "https://download.docker.com/linux/$id/gpg" -o "$key" || abort_before_change "Pre-download failed: Docker's apt key."
- $SUDO_CMD install -d -m 0755 "$(dirname "$DOKPLOY_DOCKER_KEYRING")"
- $SUDO_CMD install -m 0644 "$key" "$DOKPLOY_DOCKER_KEYRING"
rm -f "$key"
- arch="$(dpkg --print-architecture)"
- printf 'deb [arch=%s signed-by=%s] https://download.docker.com/linux/%s %s stable\\n' "$arch" "$DOKPLOY_DOCKER_KEYRING" "$id" "$codename" | $SUDO_CMD tee "$DOKPLOY_APT_SOURCES_DIR/docker.list" >/dev/null
+ if ! arch="$(dpkg --print-architecture)"; then
+ docker_skip "the architecture could not be read for Docker's apt source"
+ return 1
+ fi
+ if ! printf 'deb [arch=%s signed-by=%s] https://download.docker.com/linux/%s %s stable\\n' "$arch" "$DOKPLOY_DOCKER_KEYRING" "$id" "$codename" | $SUDO_CMD tee "$DOKPLOY_APT_SOURCES_DIR/docker.list" >/dev/null; then
+ docker_skip "Docker's apt source could not be written"
+ return 1
+ fi
+ return 0
}
docker_save_rollback_packages() {
local package="" installed="" candidate=""
if [ -z "$(docker_installed_version docker-ce)" ]; then
- abort_before_change "Pre-download failed: current Docker packages for rollback (docker-ce is not installed)."
+ docker_skip "docker-ce is not installed, so there is no Docker Engine to roll back to"
+ return 1
fi
DOCKER_ROLLBACK_DIR="$DOKPLOY_BACKUP_DIR/docker-rollback-$(date -u +%Y%m%dT%H%M%SZ)"
- $SUDO_CMD mkdir -p "$DOKPLOY_BACKUP_DIR" || abort_before_change "Pre-download failed: current Docker packages for rollback."
- $SUDO_CMD chmod 700 "$DOKPLOY_BACKUP_DIR" || abort_before_change "Pre-download failed: current Docker packages for rollback."
- $SUDO_CMD sh -c 'umask 077 && mkdir -p "$1"' sh "$DOCKER_ROLLBACK_DIR" || abort_before_change "Pre-download failed: current Docker packages for rollback."
+ if ! $SUDO_CMD mkdir -p "$DOKPLOY_BACKUP_DIR" || ! $SUDO_CMD chmod 700 "$DOKPLOY_BACKUP_DIR" || ! $SUDO_CMD sh -c 'umask 077 && mkdir -p "$1"' sh "$DOCKER_ROLLBACK_DIR"; then
+ docker_skip "the current Docker packages could not be saved for rollback"
+ return 1
+ fi
for package in docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin docker-ce-rootless-extras; do
installed="$(docker_installed_version "$package")"
if [ -z "$installed" ]; then
@@ -212,23 +237,40 @@ docker_save_rollback_packages() {
fi
;;
esac
- $SUDO_CMD sh -c 'umask 077 && cd "$1" && apt-get download "$2" >/dev/null' sh "$DOCKER_ROLLBACK_DIR" "$package=$installed" || abort_before_change "Pre-download failed: current Docker packages for rollback."
+ if ! $SUDO_CMD sh -c 'umask 077 && cd "$1" && apt-get download "$2" >/dev/null' sh "$DOCKER_ROLLBACK_DIR" "$package=$installed"; then
+ docker_skip "the current Docker packages could not be saved for rollback"
+ return 1
+ fi
done
+ return 0
}
docker_prepare_apt() {
- $SUDO_CMD apt-get update -qq || abort_before_change "Pre-download failed: the apt package lists could not be refreshed."
+ if ! $SUDO_CMD apt-get update -qq; then
+ abort_before_change "Pre-download failed: the apt package lists could not be refreshed."
+ fi
DOCKER_PACKAGE_VERSION="$(docker_pinned_candidate)"
if [ -z "$DOCKER_PACKAGE_VERSION" ]; then
- docker_add_apt_repository
- $SUDO_CMD apt-get update -qq || abort_before_change "Pre-download failed: the apt package lists could not be refreshed."
+ if ! docker_add_apt_repository; then
+ return 1
+ fi
+ if ! $SUDO_CMD apt-get update -qq; then
+ docker_skip "the apt package lists could not be refreshed after adding Docker's repository"
+ return 1
+ fi
DOCKER_PACKAGE_VERSION="$(docker_pinned_candidate)"
if [ -z "$DOCKER_PACKAGE_VERSION" ]; then
- abort_before_change "Pre-download failed: Docker ${PINNED_VERSIONS.docker} is not in the apt sources, even after adding Docker's repository."
+ docker_skip "Docker ${PINNED_VERSIONS.docker} is not in the apt sources, even after adding Docker's repository"
+ return 1
fi
fi
- docker_save_rollback_packages
- download_with_retry $SUDO_CMD apt-get install -y -qq --download-only "docker-ce=$DOCKER_PACKAGE_VERSION" "docker-ce-cli=$DOCKER_PACKAGE_VERSION" containerd.io docker-buildx-plugin docker-compose-plugin || abort_before_change "Pre-download failed: the Docker packages."
+ if ! docker_save_rollback_packages; then
+ return 1
+ fi
+ if ! download_with_retry $SUDO_CMD apt-get install -y -qq --download-only "docker-ce=$DOCKER_PACKAGE_VERSION" "docker-ce-cli=$DOCKER_PACKAGE_VERSION" containerd.io docker-buildx-plugin docker-compose-plugin; then
+ abort_before_change "Pre-download failed: the Docker packages."
+ fi
+ return 0
}
docker_ready_on() {
@@ -291,17 +333,10 @@ docker_update_from_apt() {
`;
const dockerPredownload = () => `
-if command -v apt-get >/dev/null 2>&1; then
- DOCKER_FROM_APT=1
- if docker_check_repo_codename; then
- docker_prepare_apt
- else
- DOCKER_SKIPPED=1
- fi
-else
- GET_DOCKER_SCRIPT="$(mktemp)"
- download_with_retry curl -fsSL https://get.docker.com -o "$GET_DOCKER_SCRIPT" || abort_before_change "Pre-download failed: get.docker.com."
- echo "No apt-get on this host: the Docker packages are not pre-downloaded; the Docker step fetches them."
+if ! command -v apt-get >/dev/null 2>&1; then
+ docker_skip "this host has no apt-get, so the Docker packages cannot be pre-downloaded or rolled back"
+elif docker_check_repo_codename; then
+ docker_prepare_apt || true
fi
`;
@@ -348,15 +383,10 @@ const updateStepFor = (component: UpdatableComponent) => {
case "docker":
return `
if [ "$DOCKER_SKIPPED" = 1 ]; then
- echo "Docker update skipped: its apt repository does not match this release (see above)."
+ echo "Docker not updated: $DOCKER_SKIP_REASON. The other components continue."
else
echo "Updating Docker to ${PINNED_VERSIONS.docker}"
- if [ "$DOCKER_FROM_APT" = 1 ]; then
- docker_update_from_apt
- else
- $SUDO_CMD sh "$GET_DOCKER_SCRIPT" --version ${PINNED_VERSIONS.docker}
- rm -f "$GET_DOCKER_SCRIPT"
- fi
+ docker_update_from_apt
echo "Docker version ${PINNED_VERSIONS.docker} installed ✅"
fi
`;
From 2e8a971a4dc889cac5337824ccc44460bc5ff392 Mon Sep 17 00:00:00 2001
From: merge-check <140916359+agentHits@users.noreply.github.com>
Date: Thu, 8 Oct 2026 10:08:22 -0400
Subject: [PATCH 22/33] fix(update): pass the same settings through the WSL
launcher as through macOS
The WSL launcher forwarded fewer settings than the macOS launcher, so
DOKPLOY_BACKUP_DIR, DOKPLOY_HEALTH_TIMEOUT, DOKPLOY_HEALTH_INTERVAL,
DOKPLOY_TRAEFIK_SETTLE and AGENTHITS_PULL_RETRY_DELAY set on Windows never
reached the installer. They are added to $PassthroughVars. The macOS list
already had every setting. A test keeps the two lists equal.
Co-Authored-By: Claude Haiku 5.5
---
.../settings/install-agenthits-update.test.ts | 24 +++++++++++++++++++
install-agenthits.ps1 | 3 ++-
2 files changed, 26 insertions(+), 1 deletion(-)
diff --git a/apps/dokploy/__test__/settings/install-agenthits-update.test.ts b/apps/dokploy/__test__/settings/install-agenthits-update.test.ts
index 38316e3d944..89463164107 100644
--- a/apps/dokploy/__test__/settings/install-agenthits-update.test.ts
+++ b/apps/dokploy/__test__/settings/install-agenthits-update.test.ts
@@ -885,3 +885,27 @@ describe("install-agenthits.sh Traefik restart policy", () => {
expect(restore).toBeLessThan(calls.lastIndexOf("start dokploy-traefik"));
});
});
+
+describe("install-agenthits launchers", () => {
+ it("passes the same settings through the macOS and WSL launchers", () => {
+ const installer = readFileSync(installerScript, "utf8");
+ const powershell = readFileSync(powershellScript, "utf8");
+ const macos = installer.slice(
+ installer.indexOf("PASSTHROUGH_VARS=("),
+ installer.indexOf("\n)", installer.indexOf("PASSTHROUGH_VARS=(")),
+ );
+ const wsl = powershell.slice(
+ powershell.indexOf("$PassthroughVars = @("),
+ powershell.indexOf("\n)", powershell.indexOf("$PassthroughVars = @(")),
+ );
+ const macosSettings = [...macos.matchAll(/^\s*([A-Z][A-Z0-9_]*)$/gm)]
+ .map((match) => match[1])
+ .sort();
+ const wslSettings = [...wsl.matchAll(/'([A-Z][A-Z0-9_]*)'/g)]
+ .map((match) => match[1])
+ .sort();
+
+ expect(macosSettings).toContain("DOCKER_ENGINE_UPGRADE");
+ expect(wslSettings).toEqual(macosSettings);
+ });
+});
diff --git a/install-agenthits.ps1 b/install-agenthits.ps1
index 41a8ac351a4..9e9fc7da978 100644
--- a/install-agenthits.ps1
+++ b/install-agenthits.ps1
@@ -28,7 +28,8 @@ $PassthroughVars = @(
'DOKPLOY_IMAGE', 'DOKPLOY_RELEASE_TAG', 'DOKPLOY_OFFICIAL_VERSION', 'DOKPLOY_FORK_VERSION',
'TRAEFIK_IMAGE', 'POSTGRES_IMAGE', 'REDIS_IMAGE', 'POSTGRES_DATA_TARGET',
'AGENTHITS_SCRIPT_BASE_URL', 'DOCKER_VERSION', 'DOCKER_SWARM_INIT_ARGS', 'ADVERTISE_ADDR', 'PUBLIC_IP',
- 'DOKPLOY_SERVER_HARDENING', 'HARDEN_SSH', 'HARDEN_UFW', 'HARDEN_FAIL2BAN', 'DOCKER_ENGINE_UPGRADE'
+ 'DOKPLOY_SERVER_HARDENING', 'HARDEN_SSH', 'HARDEN_UFW', 'HARDEN_FAIL2BAN', 'DOCKER_ENGINE_UPGRADE',
+ 'DOKPLOY_BACKUP_DIR', 'DOKPLOY_HEALTH_TIMEOUT', 'DOKPLOY_HEALTH_INTERVAL', 'DOKPLOY_TRAEFIK_SETTLE', 'AGENTHITS_PULL_RETRY_DELAY'
)
# VM creator id of WSL in the Hyper-V firewall (documented by Microsoft).
$WslVmCreatorId = '{40E0AC32-46A5-438A-A0B2-2B479E8F2E90}'
From 009b6bd4fd0b5f6983a682c596def29f8ea429e1 Mon Sep 17 00:00:00 2001
From: merge-check <140916359+agentHits@users.noreply.github.com>
Date: Thu, 8 Oct 2026 10:14:21 -0400
Subject: [PATCH 23/33] fix(servers): leave the host as it was when a later
Docker step fails
When a run adds a Docker apt repository and a later Docker step fails, the key
and source file that this run wrote are removed. The steps covered are the key
download, the candidate check after the update, the download of the rollback
packages, and the download of the new packages. The first three skip only
Docker, and the other components still run. A failed download of the new
packages still aborts the update, now without the repository left behind.
A key or source file that existed before the run is never removed, and an
existing key is no longer overwritten.
Co-Authored-By: Claude Haiku 5.5
---
.../__test__/server/server-components.test.ts | 98 +++++++++++++++++++
.../server/src/setup/server-components.ts | 60 +++++++++---
2 files changed, 146 insertions(+), 12 deletions(-)
diff --git a/apps/dokploy/__test__/server/server-components.test.ts b/apps/dokploy/__test__/server/server-components.test.ts
index fd8eee3b2ca..91fca1b5b83 100644
--- a/apps/dokploy/__test__/server/server-components.test.ts
+++ b/apps/dokploy/__test__/server/server-components.test.ts
@@ -253,6 +253,19 @@ describe("buildComponentUpdateScript", () => {
);
});
+ it("removes the Docker key and source this run wrote when a later Docker step fails", () => {
+ const script = buildComponentUpdateScript(["docker"]);
+
+ expect(script).toContain('rm -f "$DOKPLOY_APT_SOURCES_DIR/docker.list"');
+ expect(script).toContain('rm -f "$DOKPLOY_DOCKER_KEYRING"');
+ expect(script).toMatch(
+ /--download-only[^\n]*; then\s+docker_undo_repo\s+abort_before_change/,
+ );
+ expect(script).toMatch(
+ /docker_save_rollback_packages; then\s+docker_undo_repo\s+return 1/,
+ );
+ });
+
it("ends with the marker the UI waits for", () => {
expect(buildComponentUpdateScript(["buildpacks"])).toContain(
COMPONENTS_UPDATE_DONE,
@@ -485,6 +498,7 @@ type ScriptOptions = {
osRelease?: string;
sourceFiles?: Record;
withoutApt?: boolean;
+ preexistingKeyring?: string;
};
const listBackups = (backups: string) => {
@@ -525,6 +539,10 @@ const runComponentScript = (
const keyring = path.join(dir, "keyrings", "docker.asc");
const backups = path.join(dir, "backups");
mkdirSync(sources);
+ if (options.preexistingKeyring !== undefined) {
+ mkdirSync(path.dirname(keyring));
+ writeFileSync(keyring, options.preexistingKeyring);
+ }
writeFileSync(callLog, "");
writeFileSync(path.join(dir, "engine"), "28.3.0\n");
writeFileSync(
@@ -569,6 +587,8 @@ const runComponentScript = (
calls: readFileSync(callLog, "utf8").split("\n").filter(Boolean),
backups: listBackups(backups),
keyringExists: existsSync(keyring),
+ keyring: existsSync(keyring) ? readFileSync(keyring, "utf8") : null,
+ keyringDirExists: existsSync(path.dirname(keyring)),
sources: readSources(sources),
};
} finally {
@@ -1151,6 +1171,84 @@ describe("buildComponentUpdateScript run order", () => {
},
SPAWN_TEST_TIMEOUT_MS,
);
+ it(
+ "removes the Docker key and source it wrote, then aborts, when the new packages cannot be downloaded",
+ () => {
+ const run = runComponentScript(["docker", "traefik"], {
+ env: { FAKE_CANDIDATE_AFTER_REPO: "1", FAKE_PREDOWNLOAD_FAILS: "1" },
+ });
+
+ expect(run.status).not.toBe(0);
+ expect(run.stderr).toContain(
+ "Pre-download failed: the Docker packages. Nothing was changed.",
+ );
+ expect(run.sources).toEqual({});
+ expect(run.keyringExists).toBe(false);
+ expect(run.keyringDirExists).toBe(false);
+ expect(run.calls.some((call) => call.startsWith("docker rename"))).toBe(
+ false,
+ );
+ },
+ SPAWN_TEST_TIMEOUT_MS,
+ );
+
+ it(
+ "removes what it wrote, then skips only Docker, when the rollback packages cannot be saved",
+ () => {
+ const run = runComponentScript(["docker", "traefik"], {
+ env: { FAKE_CANDIDATE_AFTER_REPO: "1", FAKE_DOWNLOAD_FAILS: "1" },
+ });
+
+ expect(run.status).toBe(0);
+ expect(run.stdout).toContain(
+ "Docker not updated: the current Docker packages could not be saved for rollback. The other components continue.",
+ );
+ expect(run.stdout).toContain(
+ `Traefik version ${TRAEFIK_VERSION} installed`,
+ );
+ expect(run.sources).toEqual({});
+ expect(run.keyringExists).toBe(false);
+ expect(run.keyringDirExists).toBe(false);
+ },
+ SPAWN_TEST_TIMEOUT_MS,
+ );
+
+ it(
+ "removes what it wrote, then skips only Docker, when the pinned version is still missing after the update",
+ () => {
+ const run = runComponentScript(["docker", "traefik"], {
+ env: { FAKE_NO_CANDIDATE: "1" },
+ });
+
+ expect(run.status).toBe(0);
+ expect(run.stdout).toContain(
+ "Docker not updated: Docker 29.8.2 is not in the apt sources, even after adding Docker's repository. The other components continue.",
+ );
+ expect(run.stdout).toContain(
+ `Traefik version ${TRAEFIK_VERSION} installed`,
+ );
+ expect(run.sources).toEqual({});
+ expect(run.keyringExists).toBe(false);
+ expect(run.keyringDirExists).toBe(false);
+ },
+ SPAWN_TEST_TIMEOUT_MS,
+ );
+
+ it(
+ "keeps a Docker key that existed before the run and removes only the source it wrote",
+ () => {
+ const run = runComponentScript(["docker", "traefik"], {
+ env: { FAKE_CANDIDATE_AFTER_REPO: "1", FAKE_DOWNLOAD_FAILS: "1" },
+ preexistingKeyring: "existing key\n",
+ });
+
+ expect(run.status).toBe(0);
+ expect(run.keyring).toBe("existing key\n");
+ expect(run.sources).toEqual({});
+ expect(run.calls.some((call) => call.startsWith("curl"))).toBe(false);
+ },
+ SPAWN_TEST_TIMEOUT_MS,
+ );
});
describe("buildTraefikCreateCommand", () => {
diff --git a/packages/server/src/setup/server-components.ts b/packages/server/src/setup/server-components.ts
index d4067c31669..57105dc92fd 100644
--- a/packages/server/src/setup/server-components.ts
+++ b/packages/server/src/setup/server-components.ts
@@ -121,6 +121,9 @@ DOCKER_SKIPPED=0
DOCKER_SKIP_REASON=""
DOCKER_PACKAGE_VERSION=""
DOCKER_ROLLBACK_DIR=""
+DOCKER_KEY_WRITTEN=0
+DOCKER_SOURCE_WRITTEN=0
+DOCKER_KEYDIR_CREATED=0
docker_skip() {
DOCKER_SKIPPED=1
@@ -169,6 +172,21 @@ docker_pinned_candidate() {
apt-cache madison docker-ce | awk -F'|' '{ gsub(/ /, "", $2); print $2 }' | grep -m1 -E '^([0-9]+:)?${dockerVersionPattern}-' || true
}
+docker_undo_repo() {
+ if [ "$DOCKER_SOURCE_WRITTEN" = 1 ]; then
+ $SUDO_CMD rm -f "$DOKPLOY_APT_SOURCES_DIR/docker.list" || true
+ DOCKER_SOURCE_WRITTEN=0
+ fi
+ if [ "$DOCKER_KEY_WRITTEN" = 1 ]; then
+ $SUDO_CMD rm -f "$DOKPLOY_DOCKER_KEYRING" || true
+ DOCKER_KEY_WRITTEN=0
+ fi
+ if [ "$DOCKER_KEYDIR_CREATED" = 1 ]; then
+ $SUDO_CMD rmdir "$(dirname "$DOKPLOY_DOCKER_KEYRING")" 2>/dev/null || true
+ DOCKER_KEYDIR_CREATED=0
+ fi
+}
+
docker_add_apt_repository() {
local id="" codename="" arch="" key=""
if [ -n "$(docker_repo_codenames)" ] || [ -e "$DOKPLOY_APT_SOURCES_DIR/docker.list" ]; then
@@ -188,24 +206,38 @@ docker_add_apt_repository() {
docker_skip "VERSION_CODENAME is not set in $DOKPLOY_OS_RELEASE, so Docker's repository cannot be added"
return 1
fi
- if ! key="$(mktemp)"; then
- docker_skip "a temporary file for Docker's apt key could not be created"
- return 1
- fi
- if ! download_with_retry curl -fsSL "https://download.docker.com/linux/$id/gpg" -o "$key"; then
- docker_skip "Docker's apt key could not be downloaded"
- return 1
- fi
- if ! $SUDO_CMD install -d -m 0755 "$(dirname "$DOKPLOY_DOCKER_KEYRING")" || ! $SUDO_CMD install -m 0644 "$key" "$DOKPLOY_DOCKER_KEYRING"; then
- docker_skip "Docker's apt key could not be installed"
- return 1
+ if [ ! -e "$DOKPLOY_DOCKER_KEYRING" ]; then
+ if [ ! -d "$(dirname "$DOKPLOY_DOCKER_KEYRING")" ]; then
+ DOCKER_KEYDIR_CREATED=1
+ fi
+ if ! key="$(mktemp)"; then
+ docker_undo_repo
+ docker_skip "a temporary file for Docker's apt key could not be created"
+ return 1
+ fi
+ if ! download_with_retry curl -fsSL "https://download.docker.com/linux/$id/gpg" -o "$key"; then
+ rm -f "$key"
+ docker_undo_repo
+ docker_skip "Docker's apt key could not be downloaded"
+ return 1
+ fi
+ DOCKER_KEY_WRITTEN=1
+ if ! $SUDO_CMD install -d -m 0755 "$(dirname "$DOKPLOY_DOCKER_KEYRING")" || ! $SUDO_CMD install -m 0644 "$key" "$DOKPLOY_DOCKER_KEYRING"; then
+ rm -f "$key"
+ docker_undo_repo
+ docker_skip "Docker's apt key could not be installed"
+ return 1
+ fi
+ rm -f "$key"
fi
- rm -f "$key"
if ! arch="$(dpkg --print-architecture)"; then
+ docker_undo_repo
docker_skip "the architecture could not be read for Docker's apt source"
return 1
fi
+ DOCKER_SOURCE_WRITTEN=1
if ! printf 'deb [arch=%s signed-by=%s] https://download.docker.com/linux/%s %s stable\\n' "$arch" "$DOKPLOY_DOCKER_KEYRING" "$id" "$codename" | $SUDO_CMD tee "$DOKPLOY_APT_SOURCES_DIR/docker.list" >/dev/null; then
+ docker_undo_repo
docker_skip "Docker's apt source could not be written"
return 1
fi
@@ -255,19 +287,23 @@ docker_prepare_apt() {
return 1
fi
if ! $SUDO_CMD apt-get update -qq; then
+ docker_undo_repo
docker_skip "the apt package lists could not be refreshed after adding Docker's repository"
return 1
fi
DOCKER_PACKAGE_VERSION="$(docker_pinned_candidate)"
if [ -z "$DOCKER_PACKAGE_VERSION" ]; then
+ docker_undo_repo
docker_skip "Docker ${PINNED_VERSIONS.docker} is not in the apt sources, even after adding Docker's repository"
return 1
fi
fi
if ! docker_save_rollback_packages; then
+ docker_undo_repo
return 1
fi
if ! download_with_retry $SUDO_CMD apt-get install -y -qq --download-only "docker-ce=$DOCKER_PACKAGE_VERSION" "docker-ce-cli=$DOCKER_PACKAGE_VERSION" containerd.io docker-buildx-plugin docker-compose-plugin; then
+ docker_undo_repo
abort_before_change "Pre-download failed: the Docker packages."
fi
return 0
From 9f37d61b6c1ef12194346d5d4dadec0314d0700d Mon Sep 17 00:00:00 2001
From: merge-check <140916359+agentHits@users.noreply.github.com>
Date: Thu, 8 Oct 2026 10:18:12 -0400
Subject: [PATCH 24/33] fix(update): remove the saved-packages directory when
Docker is skipped or the update aborts
The timestamped docker-rollback directory is created before the new packages
are downloaded. When Docker is skipped, or the update aborts before any change,
that directory is removed, but only if this run created it: it is made with a
plain mkdir, so an existing directory is never adopted or removed. The apt
package lists are left as they are, since they are a cache.
Co-Authored-By: Claude Haiku 5.5
---
.../__test__/server/server-components.test.ts | 45 ++++++++++++++++++-
.../settings/install-agenthits-update.test.ts | 36 ++++++++++++++-
install-agenthits.sh | 13 +++++-
.../server/src/setup/server-components.ts | 15 ++++++-
4 files changed, 105 insertions(+), 4 deletions(-)
diff --git a/apps/dokploy/__test__/server/server-components.test.ts b/apps/dokploy/__test__/server/server-components.test.ts
index 91fca1b5b83..99018ba0f90 100644
--- a/apps/dokploy/__test__/server/server-components.test.ts
+++ b/apps/dokploy/__test__/server/server-components.test.ts
@@ -259,13 +259,25 @@ describe("buildComponentUpdateScript", () => {
expect(script).toContain('rm -f "$DOKPLOY_APT_SOURCES_DIR/docker.list"');
expect(script).toContain('rm -f "$DOKPLOY_DOCKER_KEYRING"');
expect(script).toMatch(
- /--download-only[^\n]*; then\s+docker_undo_repo\s+abort_before_change/,
+ /--download-only[^\n]*; then\s+docker_undo_repo\s+docker_remove_rollback_dir\s+abort_before_change/,
);
expect(script).toMatch(
/docker_save_rollback_packages; then\s+docker_undo_repo\s+return 1/,
);
});
+ it("removes the saved-packages directory this run created when Docker is skipped or the update aborts", () => {
+ const script = buildComponentUpdateScript(["docker"]);
+
+ expect(script).toContain('rm -rf "$DOCKER_ROLLBACK_DIR"');
+ expect(script).toMatch(
+ /"\$DOCKER_SKIPPED" = 1 \]; then\s+docker_remove_rollback_dir\s+fi/,
+ );
+ expect(script).toMatch(
+ /docker_undo_repo\s+docker_remove_rollback_dir\s+abort_before_change "Pre-download failed: the Docker packages\./,
+ );
+ });
+
it("ends with the marker the UI waits for", () => {
expect(buildComponentUpdateScript(["buildpacks"])).toContain(
COMPONENTS_UPDATE_DONE,
@@ -1249,6 +1261,37 @@ describe("buildComponentUpdateScript run order", () => {
},
SPAWN_TEST_TIMEOUT_MS,
);
+ it(
+ "removes the saved packages when they cannot be saved, so Docker is skipped with nothing left behind",
+ () => {
+ const run = runComponentScript(["docker", "traefik"], {
+ env: { FAKE_DOWNLOAD_FAILS: "1" },
+ });
+
+ expect(run.status).toBe(0);
+ expect(run.stdout).toContain(
+ "Docker not updated: the current Docker packages could not be saved for rollback.",
+ );
+ expect(run.backups).toEqual([]);
+ },
+ SPAWN_TEST_TIMEOUT_MS,
+ );
+
+ it(
+ "removes the saved packages when the new packages cannot be downloaded, then aborts",
+ () => {
+ const run = runComponentScript(["docker", "traefik"], {
+ env: { FAKE_PREDOWNLOAD_FAILS: "1" },
+ });
+
+ expect(run.status).not.toBe(0);
+ expect(run.stderr).toContain(
+ "Pre-download failed: the Docker packages. Nothing was changed.",
+ );
+ expect(run.backups).toEqual([]);
+ },
+ SPAWN_TEST_TIMEOUT_MS,
+ );
});
describe("buildTraefikCreateCommand", () => {
diff --git a/apps/dokploy/__test__/settings/install-agenthits-update.test.ts b/apps/dokploy/__test__/settings/install-agenthits-update.test.ts
index 89463164107..27f8ac2f091 100644
--- a/apps/dokploy/__test__/settings/install-agenthits-update.test.ts
+++ b/apps/dokploy/__test__/settings/install-agenthits-update.test.ts
@@ -135,6 +135,9 @@ case "$1" in
;;
install)
case "$*" in
+ *--download-only*)
+ [ "$FAKE_PREDOWNLOAD_FAILS" = 1 ] && exit 1
+ ;;
*--allow-downgrades*)
[ "$FAKE_ROLLBACK_FAILS" = 1 ] && exit 1
printf '%s\\n' "$FAKE_PREVIOUS_ENGINE" > "$DOCKER_ENGINE_FILE"
@@ -239,6 +242,7 @@ type OperatorScenario = {
upgradeFails?: boolean;
rollbackFails?: boolean;
downloadFails?: boolean;
+ predownloadFails?: boolean;
hostCodename?: string;
repoCodename?: string;
};
@@ -328,6 +332,7 @@ const runInstaller = (
FAKE_UPGRADE_FAILS: scenario.upgradeFails ? "1" : "0",
FAKE_ROLLBACK_FAILS: scenario.rollbackFails ? "1" : "0",
FAKE_DOWNLOAD_FAILS: scenario.downloadFails ? "1" : "0",
+ FAKE_PREDOWNLOAD_FAILS: scenario.predownloadFails ? "1" : "0",
},
});
@@ -685,13 +690,18 @@ describe("install-agenthits.sh Docker Engine upgrade", () => {
it(
"stops before any change when a current Docker package cannot be saved",
() => {
- const { result, calls } = runInstaller({
+ const { result, calls, backupEntries } = runInstaller({
upgradeEngine: true,
engineVersion: "28.3.0",
downloadFails: true,
});
expect(result.status).not.toBe(0);
+ expect(
+ backupEntries.some((entry) =>
+ entry.name.startsWith("docker-rollback-"),
+ ),
+ ).toBe(false);
expect(result.stderr).toContain(
"Pre-download failed: current Docker packages for rollback. Nothing was changed.",
);
@@ -797,6 +807,30 @@ describe("install-agenthits.sh Docker Engine upgrade", () => {
},
SPAWN_TEST_TIMEOUT_MS,
);
+ it(
+ "removes the saved packages when the new packages cannot be downloaded, then aborts",
+ () => {
+ const { result, calls, backupEntries } = runInstaller({
+ upgradeEngine: true,
+ engineVersion: "28.3.0",
+ predownloadFails: true,
+ });
+
+ expect(result.status).not.toBe(0);
+ expect(result.stderr).toContain(
+ "Pre-download failed: the Docker packages. Nothing was changed.",
+ );
+ expect(
+ backupEntries.some((entry) =>
+ entry.name.startsWith("docker-rollback-"),
+ ),
+ ).toBe(false);
+ expect(calls.some((call) => call.startsWith("service update"))).toBe(
+ false,
+ );
+ },
+ SPAWN_TEST_TIMEOUT_MS,
+ );
});
describe("install-agenthits.sh Traefik and host settings", () => {
diff --git a/install-agenthits.sh b/install-agenthits.sh
index 9a99ff88a30..d57f5f04b8e 100755
--- a/install-agenthits.sh
+++ b/install-agenthits.sh
@@ -57,6 +57,7 @@ DOCKER_PACKAGE_VERSION=""
DOCKER_ENGINE_UPGRADE_NEEDED=0
DOCKER_ENGINE_PREVIOUS_VERSION=""
DOCKER_ROLLBACK_DIR=""
+DOCKER_ROLLBACK_DIR_CREATED=0
command_exists() {
command -v "$@" >/dev/null 2>&1
@@ -1049,6 +1050,13 @@ check_docker_repo_codename() {
}
# Saved before the Engine is touched, so a failed upgrade can install them back.
+remove_docker_rollback_dir() {
+ if [ "$DOCKER_ROLLBACK_DIR_CREATED" = "1" ]; then
+ rm -rf "$DOCKER_ROLLBACK_DIR"
+ DOCKER_ROLLBACK_DIR_CREATED=0
+ fi
+}
+
save_docker_rollback_packages() {
local package=""
local installed=""
@@ -1060,10 +1068,11 @@ save_docker_rollback_packages() {
DOCKER_ROLLBACK_DIR="$DOKPLOY_BACKUP_DIR/docker-rollback-$(date -u +%Y%m%dT%H%M%SZ)"
mkdir -p "$DOKPLOY_BACKUP_DIR"
chmod 700 "$DOKPLOY_BACKUP_DIR"
- if ! (umask 077 && mkdir -p "$DOCKER_ROLLBACK_DIR"); then
+ if ! (umask 077 && mkdir "$DOCKER_ROLLBACK_DIR"); then
echo "Pre-download failed: current Docker packages for rollback. Nothing was changed." >&2
exit 1
fi
+ DOCKER_ROLLBACK_DIR_CREATED=1
for package in docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin docker-ce-rootless-extras; do
installed="$(docker_package_installed_version "$package")"
if [ -z "$installed" ]; then
@@ -1079,6 +1088,7 @@ save_docker_rollback_packages() {
;;
esac
if ! (umask 077 && cd "$DOCKER_ROLLBACK_DIR" && apt-get download "$package=$installed"); then
+ remove_docker_rollback_dir
echo "Pre-download failed: current Docker packages for rollback. Nothing was changed." >&2
exit 1
fi
@@ -1102,6 +1112,7 @@ predownload_docker_packages() {
fi
save_docker_rollback_packages
if ! download_with_retry apt-get install -y -qq --download-only "docker-ce=$DOCKER_PACKAGE_VERSION" "docker-ce-cli=$DOCKER_PACKAGE_VERSION" containerd.io docker-buildx-plugin docker-compose-plugin; then
+ remove_docker_rollback_dir
echo "Pre-download failed: the Docker packages. Nothing was changed." >&2
exit 1
fi
diff --git a/packages/server/src/setup/server-components.ts b/packages/server/src/setup/server-components.ts
index 57105dc92fd..9a58ca62d40 100644
--- a/packages/server/src/setup/server-components.ts
+++ b/packages/server/src/setup/server-components.ts
@@ -124,6 +124,7 @@ DOCKER_ROLLBACK_DIR=""
DOCKER_KEY_WRITTEN=0
DOCKER_SOURCE_WRITTEN=0
DOCKER_KEYDIR_CREATED=0
+DOCKER_ROLLBACK_DIR_CREATED=0
docker_skip() {
DOCKER_SKIPPED=1
@@ -187,6 +188,13 @@ docker_undo_repo() {
fi
}
+docker_remove_rollback_dir() {
+ if [ "$DOCKER_ROLLBACK_DIR_CREATED" = 1 ]; then
+ $SUDO_CMD rm -rf "$DOCKER_ROLLBACK_DIR" || true
+ DOCKER_ROLLBACK_DIR_CREATED=0
+ fi
+}
+
docker_add_apt_repository() {
local id="" codename="" arch="" key=""
if [ -n "$(docker_repo_codenames)" ] || [ -e "$DOKPLOY_APT_SOURCES_DIR/docker.list" ]; then
@@ -251,10 +259,11 @@ docker_save_rollback_packages() {
return 1
fi
DOCKER_ROLLBACK_DIR="$DOKPLOY_BACKUP_DIR/docker-rollback-$(date -u +%Y%m%dT%H%M%SZ)"
- if ! $SUDO_CMD mkdir -p "$DOKPLOY_BACKUP_DIR" || ! $SUDO_CMD chmod 700 "$DOKPLOY_BACKUP_DIR" || ! $SUDO_CMD sh -c 'umask 077 && mkdir -p "$1"' sh "$DOCKER_ROLLBACK_DIR"; then
+ if ! $SUDO_CMD mkdir -p "$DOKPLOY_BACKUP_DIR" || ! $SUDO_CMD chmod 700 "$DOKPLOY_BACKUP_DIR" || ! $SUDO_CMD sh -c 'umask 077 && mkdir "$1"' sh "$DOCKER_ROLLBACK_DIR"; then
docker_skip "the current Docker packages could not be saved for rollback"
return 1
fi
+ DOCKER_ROLLBACK_DIR_CREATED=1
for package in docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin docker-ce-rootless-extras; do
installed="$(docker_installed_version "$package")"
if [ -z "$installed" ]; then
@@ -304,6 +313,7 @@ docker_prepare_apt() {
fi
if ! download_with_retry $SUDO_CMD apt-get install -y -qq --download-only "docker-ce=$DOCKER_PACKAGE_VERSION" "docker-ce-cli=$DOCKER_PACKAGE_VERSION" containerd.io docker-buildx-plugin docker-compose-plugin; then
docker_undo_repo
+ docker_remove_rollback_dir
abort_before_change "Pre-download failed: the Docker packages."
fi
return 0
@@ -374,6 +384,9 @@ if ! command -v apt-get >/dev/null 2>&1; then
elif docker_check_repo_codename; then
docker_prepare_apt || true
fi
+if [ "$DOCKER_SKIPPED" = 1 ]; then
+ docker_remove_rollback_dir
+fi
`;
// Runs before any step: a failed download stops the update while nothing
From d86d18b49bca8505cd4a43762de92f8c3c98c73d Mon Sep 17 00:00:00 2001
From: merge-check <140916359+agentHits@users.noreply.github.com>
Date: Thu, 8 Oct 2026 10:47:29 -0400
Subject: [PATCH 25/33] fix(update): replace the Traefik swap that renamed the
running container
Renaming the running Traefik fails on swarm hosts where it is attached to
several networks ("cannot create entry in table endpoint_table ... already
exists"). The swap no longer renames anything, on the remote server path or
in the panel host installer.
The old container is stopped and removed with docker rm (no force). The new
one is created with --restart no, connected to dokploy-network and to the
captured networks (bridge excluded), and started. After the settle time it
must report running with no restarts, and only then is its restart policy
raised to always.
A failure after the old container was stopped removes whatever carries the
name, recreates the old image with the same flags and networks, starts it and
checks it, then exits 1 naming the step that failed. A leftover
dokploy-traefik-previous from earlier runs is removed first.
Co-Authored-By: Claude Haiku 5.5
---
.../__test__/server/server-components.test.ts | 253 +++++++++---------
.../settings/install-agenthits-update.test.ts | 97 ++++---
install-agenthits.sh | 104 +++----
.../server/src/setup/server-components.ts | 99 ++++---
packages/server/src/setup/traefik-setup.ts | 21 +-
5 files changed, 319 insertions(+), 255 deletions(-)
diff --git a/apps/dokploy/__test__/server/server-components.test.ts b/apps/dokploy/__test__/server/server-components.test.ts
index 99018ba0f90..45bf5a1716f 100644
--- a/apps/dokploy/__test__/server/server-components.test.ts
+++ b/apps/dokploy/__test__/server/server-components.test.ts
@@ -30,6 +30,7 @@ import {
} from "@dokploy/server/setup/server-components";
import {
buildTraefikCreateCommand,
+ buildTraefikCreateWithImage,
buildTraefikRunCommand,
TRAEFIK_VERSION,
} from "@dokploy/server/setup/traefik-setup";
@@ -119,34 +120,32 @@ describe("buildComponentStatuses", () => {
});
describe("buildComponentUpdateScript", () => {
- it("recreates the Traefik container from the pinned image, keeping the old one until the new one runs", () => {
+ it("replaces the Traefik container from the pinned image without renaming the old one", () => {
const script = buildComponentUpdateScript(["traefik"]);
expect(script).toContain(`docker pull traefik:v${TRAEFIK_VERSION}`);
- expect(script).toContain(
- "docker rename dokploy-traefik dokploy-traefik-previous",
- );
- expect(script).toContain(buildTraefikCreateCommand(TRAEFIK_VERSION).trim());
+ expect(script).toContain(`traefik_create "traefik:v${TRAEFIK_VERSION}"`);
+ expect(script).toContain(buildTraefikCreateWithImage('"$1"').trim());
+ expect(script).not.toContain("docker rename");
});
-
- it("creates the replacement Traefik, attaches its networks, and starts it last", () => {
+ it("stops and removes the old Traefik, creates the new one, attaches its networks, and starts it last", () => {
const script = buildComponentUpdateScript(["traefik"]);
const order = [
- script.indexOf("docker rename dokploy-traefik dokploy-traefik-previous"),
- script.indexOf("docker create --name dokploy-traefik"),
- script.indexOf("docker network connect dokploy-network dokploy-traefik"),
+ script.indexOf("if ! $SUDO_CMD docker stop dokploy-traefik >/dev/null;"),
+ script.indexOf("if ! $SUDO_CMD docker rm dokploy-traefik >/dev/null;"),
+ script.indexOf('if ! traefik_create "traefik:v'),
+ script.indexOf('if ! traefik_connect "$traefik_extra_networks";'),
+ script.indexOf("if ! $SUDO_CMD docker start dokploy-traefik >/dev/null;"),
script.indexOf(
- 'docker network connect "$traefik_network" dokploy-traefik',
+ "if ! $SUDO_CMD docker update --restart always dokploy-traefik >/dev/null;",
),
- script.indexOf("docker stop dokploy-traefik-previous"),
- script.indexOf("\n$SUDO_CMD docker start dokploy-traefik >/dev/null\n"),
];
expect(order.every((position) => position >= 0)).toBe(true);
expect(order).toEqual([...order].sort((a, b) => a - b));
expect(script).not.toContain("docker run");
+ expect(script).not.toContain("docker rename");
});
-
it("reads the networks of the running Traefik on the host, before it is replaced", () => {
const script = buildComponentUpdateScript(["traefik"]);
const read = script.indexOf(
@@ -155,29 +154,23 @@ describe("buildComponentUpdateScript", () => {
expect(read).toBeGreaterThanOrEqual(0);
expect(read).toBeLessThan(
- script.indexOf("docker rename dokploy-traefik dokploy-traefik-previous"),
+ script.indexOf("if ! $SUDO_CMD docker stop dokploy-traefik >/dev/null;"),
);
expect(script).toContain("bridge | dokploy-network) continue ;;");
});
-
- it("sets the restart policy of the replaced Traefik around the swap", () => {
+ it("raises the restart policy of the new Traefik only after it passes the check", () => {
const script = buildComponentUpdateScript(["traefik"]);
- const restore = script.slice(
- script.indexOf("restore_traefik() {"),
- script.indexOf("trap restore_traefik ERR"),
+ const check = script.indexOf("if ! traefik_check_running; then");
+ const raise = script.indexOf(
+ "if ! $SUDO_CMD docker update --restart always dokploy-traefik >/dev/null; then",
);
- expect(
- script.indexOf("docker update --restart no dokploy-traefik-previous"),
- ).toBeLessThan(script.indexOf("docker stop dokploy-traefik-previous"));
- expect(
- restore.indexOf("docker update --restart always dokploy-traefik"),
- ).toBeGreaterThanOrEqual(0);
- expect(
- restore.indexOf("docker update --restart always dokploy-traefik"),
- ).toBeLessThan(restore.indexOf("docker start dokploy-traefik"));
+ expect(script).toMatch(
+ /docker create --name dokploy-traefik\s+--restart no\s/,
+ );
+ expect(check).toBeGreaterThanOrEqual(0);
+ expect(raise).toBeGreaterThan(check);
});
-
it("passes the pinned versions into the installers through env", () => {
const script = buildComponentUpdateScript(["nixpacks", "railpack"]);
@@ -344,7 +337,11 @@ const FAKE_TOOLS: Record = {
printf 'docker %s\\n' "$*" >> "$CALL_LOG"
case "$1" in
pull) [ "$FAKE_PULL_FAILS" = 1 ] && exit 1 ;;
- create) [ "$FAKE_CREATE_FAILS" = 1 ] && exit 1 ;;
+ create)
+ [ "$FAKE_CREATE_FAILS" = 1 ] && exit 1
+ for traefik_arg in "$@"; do traefik_image_arg="$traefik_arg"; done
+ printf '%s\\n' "$traefik_image_arg" > "$FAKE_STATE/traefik-image"
+ ;;
version) cat "$FAKE_STATE/engine" ;;
network)
case "$2" in
@@ -354,9 +351,15 @@ case "$1" in
;;
inspect)
case "$*" in
+ *Config.Image*) echo "$FAKE_OLD_IMAGE" ;;
*NetworkSettings*) echo "$FAKE_TRAEFIK_NETWORKS" ;;
+ *RestartCount*)
+ traefik_now="$(cat "$FAKE_STATE/traefik-image" 2>/dev/null || echo "$FAKE_OLD_IMAGE")"
+ if [ "$FAKE_NOT_RUNNING" = 1 ] && [ "$traefik_now" != "$FAKE_OLD_IMAGE" ]; then echo "false 0"; else echo "true 0"; fi
+ ;;
*State.Running*)
- if [ "$FAKE_NOT_RUNNING" = 1 ]; then echo false; else echo true; fi
+ traefik_now="$(cat "$FAKE_STATE/traefik-image" 2>/dev/null || echo "$FAKE_OLD_IMAGE")"
+ if [ "$FAKE_NOT_RUNNING" = 1 ] && [ "$traefik_now" != "$FAKE_OLD_IMAGE" ]; then echo false; else echo true; fi
;;
esac
;;
@@ -584,6 +587,7 @@ const runComponentScript = (
FAKE_STATE: dir,
FAKE_PREVIOUS_ENGINE: "28.3.0",
FAKE_TARGET_ENGINE: "29.8.2",
+ FAKE_OLD_IMAGE: "traefik:v3.6.25",
TRAEFIK_SETTLE_SECONDS: "0",
DOKPLOY_BACKUP_DIR: backups,
DOKPLOY_OS_RELEASE: osRelease,
@@ -706,7 +710,7 @@ describe("buildComponentUpdateScript run order", () => {
call.startsWith("docker pull traefik:v"),
);
const firstChange = run.calls.findIndex((call) =>
- call.startsWith("docker rename dokploy-traefik"),
+ call.startsWith("docker stop dokploy-traefik"),
);
expect(download).toBeGreaterThanOrEqual(0);
expect(pull).toBeGreaterThanOrEqual(0);
@@ -716,51 +720,47 @@ describe("buildComponentUpdateScript run order", () => {
SPAWN_TEST_TIMEOUT_MS,
);
- it(
- "replaces Traefik only after the new container has run",
- () => {
- const run = runComponentScript(["traefik"]);
-
- expect(run.status).toBe(0);
- expect(run.stdout).toContain(
- `Traefik version ${TRAEFIK_VERSION} installed`,
- );
- const at = (prefix: string) =>
- run.calls.findIndex((call) => call.startsWith(prefix));
- const order = [
- at("docker pull traefik:v"),
- at("docker rename dokploy-traefik dokploy-traefik-previous"),
- at("docker create --name dokploy-traefik"),
- at("docker network connect dokploy-network dokploy-traefik"),
- at("docker update --restart no dokploy-traefik-previous"),
- at("docker stop dokploy-traefik-previous"),
- at("docker start dokploy-traefik"),
- at("docker inspect -f {{.State.Running}} dokploy-traefik"),
- ];
- expect(order.every((position) => position >= 0)).toBe(true);
- expect(order).toEqual([...order].sort((a, b) => a - b));
- expect(run.calls.at(-1)).toBe("docker rm -f dokploy-traefik-previous");
- },
- SPAWN_TEST_TIMEOUT_MS,
- );
-
- it(
- "sets --restart no on the replaced Traefik before stopping it",
- () => {
- const run = runComponentScript(["traefik"]);
-
- expect(run.status).toBe(0);
- const noRestart = run.calls.indexOf(
- "docker update --restart no dokploy-traefik-previous",
- );
- expect(noRestart).toBeGreaterThanOrEqual(0);
- expect(noRestart).toBeLessThan(
- run.calls.indexOf("docker stop dokploy-traefik-previous"),
- );
- },
- SPAWN_TEST_TIMEOUT_MS,
- );
+ it("replaces Traefik only after the new container has run", () => {
+ const run = runComponentScript(["traefik"]);
+ expect(run.status).toBe(0);
+ expect(run.stdout).toContain(
+ `Traefik version ${TRAEFIK_VERSION} installed`,
+ );
+ const at = (prefix: string) =>
+ run.calls.findIndex((call) => call.startsWith(prefix));
+ const order = [
+ at("docker pull traefik:v"),
+ at("docker stop dokploy-traefik"),
+ at("docker rm dokploy-traefik"),
+ at("docker create --name dokploy-traefik"),
+ at("docker network connect dokploy-network dokploy-traefik"),
+ at("docker start dokploy-traefik"),
+ at(
+ "docker inspect -f {{.State.Running}} {{.RestartCount}} dokploy-traefik",
+ ),
+ at("docker update --restart always dokploy-traefik"),
+ ];
+ expect(order.every((position) => position >= 0)).toBe(true);
+ expect(order).toEqual([...order].sort((a, b) => a - b));
+ expect(run.calls.at(-1)).toBe(
+ "docker update --restart always dokploy-traefik",
+ );
+ });
+ it("creates the new Traefik with --restart no and raises the policy only after the start", () => {
+ const run = runComponentScript(["traefik"]);
+
+ expect(run.status).toBe(0);
+ const created =
+ run.calls.find((call) =>
+ call.startsWith("docker create --name dokploy-traefik"),
+ ) ?? "";
+ expect(created).toContain("--restart no");
+ expect(created).not.toContain("--restart always");
+ expect(
+ run.calls.indexOf("docker update --restart always dokploy-traefik"),
+ ).toBeGreaterThan(run.calls.indexOf("docker start dokploy-traefik"));
+ });
it(
"restores --restart always on the Traefik it puts back",
() => {
@@ -780,43 +780,39 @@ describe("buildComponentUpdateScript run order", () => {
SPAWN_TEST_TIMEOUT_MS,
);
- it(
- "restores the previous Traefik container when the new one is not running",
- () => {
- const run = runComponentScript(["traefik"], {
- env: { FAKE_NOT_RUNNING: "1" },
- });
-
- expect(run.status).not.toBe(0);
- expect(run.stderr).toContain("restoring the previous container");
- expect(run.calls).toContain(
- "docker rename dokploy-traefik-previous dokploy-traefik",
- );
- expect(run.calls.at(-1)).toBe("docker start dokploy-traefik");
- expect(run.stdout).not.toContain("installed");
- },
- SPAWN_TEST_TIMEOUT_MS,
- );
-
- it(
- "restores the previous Traefik container when docker create fails",
- () => {
- const run = runComponentScript(["traefik"], {
- env: { FAKE_CREATE_FAILS: "1" },
- });
+ it("restores the previous Traefik image when the new one is not running", () => {
+ const run = runComponentScript(["traefik"], {
+ env: { FAKE_NOT_RUNNING: "1" },
+ });
- expect(run.status).not.toBe(0);
- expect(run.calls).toContain(
- "docker rename dokploy-traefik-previous dokploy-traefik",
- );
- expect(
- run.calls.some((call) => call.startsWith("docker network connect")),
- ).toBe(false);
- expect(run.calls.at(-1)).toBe("docker start dokploy-traefik");
- },
- SPAWN_TEST_TIMEOUT_MS,
- );
+ expect(run.status).not.toBe(0);
+ expect(run.stderr).toContain(
+ "Error: Traefik update failed while checking the new Traefik container",
+ );
+ expect(run.stderr).toContain(
+ "The previous Traefik container is running again.",
+ );
+ expect(run.stdout).not.toContain("installed");
+ const recreated = run.calls.filter((call) =>
+ call.startsWith("docker create --name dokploy-traefik"),
+ );
+ expect(recreated).toHaveLength(2);
+ expect(recreated[1]?.endsWith("traefik:v3.6.25")).toBe(true);
+ });
+ it("reports the failed step when the new container cannot be created and the old one cannot be restored", () => {
+ const run = runComponentScript(["traefik"], {
+ env: { FAKE_CREATE_FAILS: "1" },
+ });
+ expect(run.status).not.toBe(0);
+ expect(run.stderr).toContain(
+ "Error: Traefik update failed while creating the new Traefik container.",
+ );
+ expect(run.stderr).toContain("failed as well");
+ expect(
+ run.calls.some((call) => call.startsWith("docker network connect")),
+ ).toBe(false);
+ });
it(
"connects every network of the old Traefik container before the new one starts",
() => {
@@ -868,23 +864,24 @@ describe("buildComponentUpdateScript run order", () => {
SPAWN_TEST_TIMEOUT_MS,
);
- it(
- "restores the previous Traefik container when a network cannot be connected",
- () => {
- const run = runComponentScript(["traefik"], {
- env: { FAKE_TRAEFIK_NETWORKS: "app-a", FAKE_CONNECT_FAILS: "app-a" },
- });
-
- expect(run.status).not.toBe(0);
- expect(run.stderr).toContain("restoring the previous container");
- expect(run.calls).toContain(
- "docker rename dokploy-traefik-previous dokploy-traefik",
- );
- expect(run.calls.at(-1)).toBe("docker start dokploy-traefik");
- },
- SPAWN_TEST_TIMEOUT_MS,
- );
+ it("reports the failed step when a network cannot be connected and the old container cannot be restored", () => {
+ const run = runComponentScript(["traefik"], {
+ env: { FAKE_TRAEFIK_NETWORKS: "app-a", FAKE_CONNECT_FAILS: "app-a" },
+ });
+ expect(run.status).not.toBe(0);
+ expect(run.stderr).toContain(
+ "Error: Traefik update failed while connecting the new Traefik container to its networks.",
+ );
+ expect(run.stderr).toContain("failed as well");
+ expect(
+ run.calls.some(
+ (call) =>
+ call.startsWith("docker create --name dokploy-traefik") &&
+ call.endsWith("traefik:v3.6.25"),
+ ),
+ ).toBe(true);
+ });
it(
"downloads the pinned Docker packages before the Docker step runs",
() => {
diff --git a/apps/dokploy/__test__/settings/install-agenthits-update.test.ts b/apps/dokploy/__test__/settings/install-agenthits-update.test.ts
index 27f8ac2f091..fed2f18b375 100644
--- a/apps/dokploy/__test__/settings/install-agenthits-update.test.ts
+++ b/apps/dokploy/__test__/settings/install-agenthits-update.test.ts
@@ -66,11 +66,23 @@ case "$1" in
case "$*" in
*Config.Image*) echo "$FAKE_TRAEFIK_IMAGE" ;;
*NetworkSettings*) echo "$FAKE_TRAEFIK_NETWORKS" ;;
- *State.Running*) echo "$FAKE_TRAEFIK_RUNNING" ;;
+ *RestartCount*)
+ traefik_now="$(cat "$DOCKER_TRAEFIK_IMAGE_FILE" 2>/dev/null || echo "$FAKE_TRAEFIK_IMAGE")"
+ if [ "$FAKE_TRAEFIK_RUNNING" = "false" ] && [ "$traefik_now" != "$FAKE_TRAEFIK_IMAGE" ]; then echo "false 0"; else echo "true 0"; fi
+ ;;
+ *State.Running*)
+ traefik_now="$(cat "$DOCKER_TRAEFIK_IMAGE_FILE" 2>/dev/null || echo "$FAKE_TRAEFIK_IMAGE")"
+ if [ "$FAKE_TRAEFIK_RUNNING" = "false" ] && [ "$traefik_now" != "$FAKE_TRAEFIK_IMAGE" ]; then echo false; else echo true; fi
+ ;;
*State.Health.Status*) echo healthy ;;
esac
exit 0
;;
+ create)
+ for traefik_arg in "$@"; do traefik_image_arg="$traefik_arg"; done
+ printf '%s\\n' "$traefik_image_arg" > "$DOCKER_TRAEFIK_IMAGE_FILE"
+ exit 0
+ ;;
network)
case "$2" in
inspect)
@@ -299,6 +311,7 @@ const runInstaller = (
DOCKER_CALL_LOG: callLog,
DOCKER_INDEX_FILE: indexFile,
DOCKER_ENGINE_FILE: engineFile,
+ DOCKER_TRAEFIK_IMAGE_FILE: path.join(dir, "traefik-image"),
AGENTHITS_SKIP_HOST_CHECK: "1",
DOKPLOY_BACKUP_DIR: backupDir,
DOKPLOY_APT_SOURCES_DIR: sourcesDir,
@@ -436,12 +449,9 @@ describe("install-agenthits.sh update", () => {
calls,
"service update --detach --update-order stop-first --update-failure-action rollback --image redis:8.10.2 dokploy-redis",
);
- const traefikRename = firstIndex(
- calls,
- "rename dokploy-traefik dokploy-traefik-previous",
- );
+ const traefikStop = firstIndex(calls, "stop dokploy-traefik");
+ const traefikRemove = firstIndex(calls, "rm dokploy-traefik");
const traefikCreate = firstIndex(calls, "create --name dokploy-traefik");
- const traefikStop = firstIndex(calls, "stop dokploy-traefik-previous");
const traefikStart = firstIndex(calls, "start dokploy-traefik");
const postgres = firstIndex(
calls,
@@ -455,9 +465,9 @@ describe("install-agenthits.sh update", () => {
for (const step of [
backup,
redis,
- traefikRename,
- traefikCreate,
traefikStop,
+ traefikRemove,
+ traefikCreate,
traefikStart,
postgres,
panel,
@@ -466,16 +476,15 @@ describe("install-agenthits.sh update", () => {
}
expect(lastPull).toBeLessThan(backup);
expect(backup).toBeLessThan(redis);
- expect(redis).toBeLessThan(traefikRename);
- expect(traefikRename).toBeLessThan(traefikCreate);
- expect(traefikCreate).toBeLessThan(traefikStop);
- expect(traefikStop).toBeLessThan(traefikStart);
+ expect(redis).toBeLessThan(traefikStop);
+ expect(traefikStop).toBeLessThan(traefikRemove);
+ expect(traefikRemove).toBeLessThan(traefikCreate);
+ expect(traefikCreate).toBeLessThan(traefikStart);
expect(traefikStart).toBeLessThan(postgres);
expect(postgres).toBeLessThan(panel);
},
SPAWN_TEST_TIMEOUT_MS,
);
-
it(
"stops before any swap when the Postgres backup fails",
() => {
@@ -520,7 +529,7 @@ describe("install-agenthits.sh update", () => {
);
it(
- "restores the previous Traefik container when the new one is not running",
+ "restores the previous Traefik image when the new one is not running",
() => {
const { result, calls } = runInstaller({
traefikImage: "traefik:v3.6.25",
@@ -528,18 +537,23 @@ describe("install-agenthits.sh update", () => {
});
expect(result.status).not.toBe(0);
- expect(result.stderr).toContain("restoring the previous container");
- expect(calls).toContain(
- "rename dokploy-traefik-previous dokploy-traefik",
+ expect(result.stderr).toContain(
+ "Error: Traefik update failed while checking the new Traefik container",
+ );
+ expect(result.stderr).toContain(
+ "The previous Traefik container is running again.",
);
- expect(calls.at(-1)).toBe("start dokploy-traefik");
+ const recreated = calls.filter((call) =>
+ call.startsWith("create --name dokploy-traefik"),
+ );
+ expect(recreated).toHaveLength(2);
+ expect(recreated[1]?.endsWith("traefik:v3.6.25")).toBe(true);
expect(calls.some((call) => call.startsWith("service update"))).toBe(
false,
);
},
SPAWN_TEST_TIMEOUT_MS,
);
-
it(
"reconnects every network of the old Traefik container before the new one starts",
() => {
@@ -585,7 +599,7 @@ describe("install-agenthits.sh update", () => {
);
it(
- "restores the previous Traefik container when a network cannot be connected",
+ "reports the failed step when a network cannot be connected and the old container cannot be restored",
() => {
const { result, calls } = runInstaller({
traefikImage: "traefik:v3.6.25",
@@ -594,18 +608,23 @@ describe("install-agenthits.sh update", () => {
});
expect(result.status).not.toBe(0);
- expect(result.stderr).toContain("restoring the previous container");
- expect(calls).toContain(
- "rename dokploy-traefik-previous dokploy-traefik",
+ expect(result.stderr).toContain(
+ "Error: Traefik update failed while creating the new Traefik container and connecting its networks.",
);
- expect(calls.at(-1)).toBe("start dokploy-traefik");
+ expect(result.stderr).toContain("failed as well");
+ expect(
+ calls.some(
+ (call) =>
+ call.startsWith("create --name dokploy-traefik") &&
+ call.endsWith("traefik:v3.6.25"),
+ ),
+ ).toBe(true);
expect(calls.some((call) => call.startsWith("service update"))).toBe(
false,
);
},
SPAWN_TEST_TIMEOUT_MS,
);
-
it(
"refuses a Postgres major version change before anything runs",
() => {
@@ -874,14 +893,15 @@ describe("install-agenthits.sh Traefik and host settings", () => {
expect(installer).toContain("docker-rollback-");
});
- it("reattaches the Traefik networks and starts the new container last in the installer", () => {
+ it("reattaches the Traefik networks, starts the new container last, and never renames", () => {
const installer = readFileSync(installerScript, "utf8");
const swap = installer.slice(installer.indexOf("swap_dokploy_traefik() {"));
const order = [
- swap.indexOf('TRAEFIK_EXTRA_NETWORKS="$(traefik_extra_networks)"'),
- swap.indexOf('create_dokploy_traefik "$TRAEFIK_IMAGE"'),
- swap.indexOf("docker stop dokploy-traefik-previous"),
+ swap.indexOf("docker stop dokploy-traefik"),
+ swap.indexOf("docker rm dokploy-traefik"),
+ swap.indexOf('create_dokploy_traefik "$TRAEFIK_IMAGE" no'),
swap.indexOf("docker start dokploy-traefik >/dev/null"),
+ swap.indexOf("docker update --restart always dokploy-traefik"),
];
expect(order.every((position) => position >= 0)).toBe(true);
@@ -889,24 +909,27 @@ describe("install-agenthits.sh Traefik and host settings", () => {
expect(installer).toContain(
'docker network connect "$network" dokploy-traefik',
);
+ expect(installer).not.toContain("docker rename");
expect(installer).not.toContain("docker run -d");
});
});
describe("install-agenthits.sh Traefik restart policy", () => {
- it("sets --restart no on the replaced Traefik before stopping it", () => {
+ it("creates the new Traefik with --restart no and raises the policy only after the check", () => {
const { result, calls } = runInstaller({ traefikImage: "traefik:v3.6.25" });
expect(result.status).toBe(0);
- const noRestart = calls.indexOf(
- "update --restart no dokploy-traefik-previous",
- );
- expect(noRestart).toBeGreaterThanOrEqual(0);
- expect(noRestart).toBeLessThan(
- calls.indexOf("stop dokploy-traefik-previous"),
+ const created =
+ calls.find((call) => call.startsWith("create --name dokploy-traefik")) ??
+ "";
+ expect(created).toContain("--restart no");
+ expect(calls.some((call) => call.startsWith("update --restart no"))).toBe(
+ false,
);
+ expect(
+ calls.indexOf("update --restart always dokploy-traefik"),
+ ).toBeGreaterThan(calls.indexOf("start dokploy-traefik"));
});
-
it("restores --restart always on the Traefik it puts back", () => {
const { result, calls } = runInstaller({
traefikImage: "traefik:v3.6.25",
diff --git a/install-agenthits.sh b/install-agenthits.sh
index d57f5f04b8e..5626c5a9fa3 100755
--- a/install-agenthits.sh
+++ b/install-agenthits.sh
@@ -58,6 +58,9 @@ DOCKER_ENGINE_UPGRADE_NEEDED=0
DOCKER_ENGINE_PREVIOUS_VERSION=""
DOCKER_ROLLBACK_DIR=""
DOCKER_ROLLBACK_DIR_CREATED=0
+TRAEFIK_EXTRA_NETWORKS=""
+TRAEFIK_OLD_IMAGE=""
+TRAEFIK_OLD_STOPPED=0
command_exists() {
command -v "$@" >/dev/null 2>&1
@@ -1302,51 +1305,54 @@ traefik_extra_networks() {
done
}
-# The container is created and connected to all its networks before it starts,
-# so the time without a running Traefik stays short.
+# Created but not started, with the given restart policy, and connected to all its
+# networks before it starts, so the time without a running Traefik stays short.
create_dokploy_traefik() {
+ local image="$1"
+ local restart="$2"
docker create \
--name dokploy-traefik \
- --restart always \
+ --restart "$restart" \
-v /etc/dokploy/traefik/traefik.yml:/etc/traefik/traefik.yml \
-v /etc/dokploy/traefik/dynamic:/etc/dokploy/traefik/dynamic \
-v /var/run/docker.sock:/var/run/docker.sock:ro \
-p 80:80/tcp \
-p 443:443/tcp \
-p 443:443/udp \
- "$1" >/dev/null
+ "$image" >/dev/null || return 1
- docker network connect dokploy-network dokploy-traefik
+ docker network connect dokploy-network dokploy-traefik || return 1
local network=""
for network in $TRAEFIK_EXTRA_NETWORKS; do
if docker network inspect "$network" >/dev/null 2>&1; then
- docker network connect "$network" dokploy-traefik
+ docker network connect "$network" dokploy-traefik || return 1
fi
done
}
run_dokploy_traefik() {
- create_dokploy_traefik "$1"
+ create_dokploy_traefik "$1" always
docker start dokploy-traefik >/dev/null
}
-TRAEFIK_TARGET_IMAGE=""
-TRAEFIK_EXTRA_NETWORKS=""
-TRAEFIK_PREVIOUS_KEPT=0
-TRAEFIK_SWAPPED=0
-
-# Ports 80 and 443 are free only after the old container stops, so the old one
-# is put back when the new one does not come up.
-restore_dokploy_traefik() {
- trap - ERR HUP INT TERM
- if [ "$TRAEFIK_SWAPPED" = "0" ]; then
- echo "Error: Traefik on $TRAEFIK_TARGET_IMAGE did not come up with all its networks; restoring the previous container." >&2
- if [ "$TRAEFIK_PREVIOUS_KEPT" = "1" ]; then
- docker rm -f dokploy-traefik >/dev/null 2>&1 || true
- docker rename dokploy-traefik-previous dokploy-traefik || true
- docker update --restart always dokploy-traefik >/dev/null 2>&1 || true
+traefik_check_running() {
+ [ "$(docker inspect --format '{{.State.Running}} {{.RestartCount}}' dokploy-traefik 2>/dev/null)" = "true 0" ]
+}
+
+# Names the step that failed. Once the old container is stopped, it is created again
+# from its image with the same networks and started, before the script exits.
+traefik_restore() {
+ local failed_step="$1"
+ trap - HUP INT TERM
+ echo "Error: Traefik update failed while $failed_step." >&2
+ if [ "$TRAEFIK_OLD_STOPPED" = "1" ]; then
+ echo "Restoring the previous Traefik container from $TRAEFIK_OLD_IMAGE." >&2
+ docker rm -f dokploy-traefik >/dev/null 2>&1 || true
+ if create_dokploy_traefik "$TRAEFIK_OLD_IMAGE" no && docker update --restart always dokploy-traefik >/dev/null && docker start dokploy-traefik >/dev/null && traefik_check_running; then
+ echo "The previous Traefik container is running again." >&2
+ else
+ echo "Error: restoring the previous Traefik container failed as well. Check with: docker ps -a --filter name=dokploy-traefik" >&2
fi
- docker start dokploy-traefik >/dev/null 2>&1 || true
fi
exit 1
}
@@ -1359,35 +1365,39 @@ swap_dokploy_traefik() {
return 0
fi
- TRAEFIK_TARGET_IMAGE="$TRAEFIK_IMAGE"
- TRAEFIK_PREVIOUS_KEPT=0
- TRAEFIK_SWAPPED=0
- # errtrace makes the ERR trap fire for a failed docker command inside create_dokploy_traefik.
- set -o errtrace
- trap restore_dokploy_traefik ERR HUP INT TERM
- TRAEFIK_EXTRA_NETWORKS="$(traefik_extra_networks)"
-
+ local existed=0
if docker inspect dokploy-traefik >/dev/null 2>&1; then
- docker rm -f dokploy-traefik-previous >/dev/null 2>&1 || true
- docker rename dokploy-traefik dokploy-traefik-previous
- TRAEFIK_PREVIOUS_KEPT=1
+ existed=1
fi
-
- create_dokploy_traefik "$TRAEFIK_IMAGE"
- if [ "$TRAEFIK_PREVIOUS_KEPT" = "1" ]; then
- # A stopped container with restart=always is started again when the daemon restarts.
- docker update --restart no dokploy-traefik-previous >/dev/null
- docker stop dokploy-traefik-previous >/dev/null
+ TRAEFIK_OLD_IMAGE="$current"
+ TRAEFIK_EXTRA_NETWORKS="$(traefik_extra_networks)"
+ TRAEFIK_OLD_STOPPED=0
+ trap 'traefik_restore "being interrupted"' HUP INT TERM
+ docker rm -f dokploy-traefik-previous >/dev/null 2>&1 || true
+ if [ "$existed" = "1" ]; then
+ if ! docker stop dokploy-traefik >/dev/null; then
+ traefik_restore "stopping the running Traefik container"
+ fi
+ TRAEFIK_OLD_STOPPED=1
+ if ! docker rm dokploy-traefik >/dev/null; then
+ traefik_restore "removing the stopped Traefik container"
+ fi
+ fi
+ if ! create_dokploy_traefik "$TRAEFIK_IMAGE" no; then
+ traefik_restore "creating the new Traefik container and connecting its networks"
+ fi
+ if ! docker start dokploy-traefik >/dev/null; then
+ traefik_restore "starting the new Traefik container"
fi
- docker start dokploy-traefik >/dev/null
sleep "$DOKPLOY_TRAEFIK_SETTLE"
- if [ "$(docker inspect --format '{{.State.Running}}' dokploy-traefik 2>/dev/null)" != "true" ]; then
- restore_dokploy_traefik
+ if ! traefik_check_running; then
+ traefik_restore "checking the new Traefik container (running, with no restarts)"
fi
-
- TRAEFIK_SWAPPED=1
- trap - ERR HUP INT TERM
- echo "Traefik runs $TRAEFIK_IMAGE (previous container kept stopped as dokploy-traefik-previous)"
+ if ! docker update --restart always dokploy-traefik >/dev/null; then
+ traefik_restore "setting the restart policy of the new Traefik container"
+ fi
+ trap - HUP INT TERM
+ echo "Traefik runs $TRAEFIK_IMAGE"
}
require_update_services() {
diff --git a/packages/server/src/setup/server-components.ts b/packages/server/src/setup/server-components.ts
index 9a58ca62d40..87f5181d72c 100644
--- a/packages/server/src/setup/server-components.ts
+++ b/packages/server/src/setup/server-components.ts
@@ -6,7 +6,7 @@ import {
} from "../utils/servers/destination";
import { PINNED_VERSIONS } from "./component-versions";
import { rcloneInstallCommand } from "./server-setup";
-import { buildTraefikCreateCommand, TRAEFIK_VERSION } from "./traefik-setup";
+import { buildTraefikCreateWithImage, TRAEFIK_VERSION } from "./traefik-setup";
export const COMPONENT_NAMES = [
"docker",
@@ -446,49 +446,76 @@ ${rcloneInstallCommand()}
case "traefik":
return `
echo "Updating Traefik to ${TRAEFIK_VERSION}"
-traefik_previous_kept=0
-traefik_swapped=0
-restore_traefik() {
- if [ "$traefik_swapped" = 0 ]; then
- echo "Traefik ${TRAEFIK_VERSION} did not start; restoring the previous container." >&2
- if [ "$traefik_previous_kept" = 1 ]; then
- $SUDO_CMD docker rm -f dokploy-traefik >/dev/null 2>&1 || true
- $SUDO_CMD docker rename dokploy-traefik-previous dokploy-traefik || true
- $SUDO_CMD docker update --restart always dokploy-traefik >/dev/null 2>&1 || true
+traefik_existed=0
+traefik_old_stopped=0
+traefik_old_image=""
+traefik_extra_networks=""
+if $SUDO_CMD docker inspect dokploy-traefik >/dev/null 2>&1; then
+ traefik_existed=1
+ traefik_old_image="$($SUDO_CMD docker inspect -f '{{.Config.Image}}' dokploy-traefik 2>/dev/null || true)"
+ traefik_extra_networks="$($SUDO_CMD docker inspect -f '{{range $k,$v := .NetworkSettings.Networks}}{{$k}} {{end}}' dokploy-traefik 2>/dev/null || true)"
+fi
+traefik_create() {
+${buildTraefikCreateWithImage('"$1"')}
+}
+traefik_connect() {
+ $SUDO_CMD docker network connect dokploy-network dokploy-traefik || return 1
+ local traefik_network=""
+ for traefik_network in $1; do
+ case "$traefik_network" in
+ bridge | dokploy-network) continue ;;
+ esac
+ if $SUDO_CMD docker network inspect "$traefik_network" >/dev/null 2>&1; then
+ $SUDO_CMD docker network connect "$traefik_network" dokploy-traefik >/dev/null || return 1
+ fi
+ done
+}
+traefik_check_running() {
+ [ "$($SUDO_CMD docker inspect -f '{{.State.Running}} {{.RestartCount}}' dokploy-traefik 2>/dev/null)" = "true 0" ]
+}
+traefik_restore() {
+ local failed_step="$1"
+ trap - HUP INT TERM
+ echo "Error: Traefik update failed while $failed_step." >&2
+ if [ "$traefik_old_stopped" = 1 ]; then
+ echo "Restoring the previous Traefik container from $traefik_old_image." >&2
+ $SUDO_CMD docker rm -f dokploy-traefik >/dev/null 2>&1 || true
+ if traefik_create "$traefik_old_image" && traefik_connect "$traefik_extra_networks" && $SUDO_CMD docker update --restart always dokploy-traefik >/dev/null && $SUDO_CMD docker start dokploy-traefik >/dev/null && traefik_check_running; then
+ echo "The previous Traefik container is running again." >&2
+ else
+ echo "Error: restoring the previous Traefik container failed as well. Check with: docker ps -a --filter name=dokploy-traefik" >&2
fi
- $SUDO_CMD docker start dokploy-traefik >/dev/null 2>&1 || true
fi
exit 1
}
-trap restore_traefik ERR HUP INT TERM
-traefik_networks="$($SUDO_CMD docker inspect -f '{{range $k,$v := .NetworkSettings.Networks}}{{$k}} {{end}}' dokploy-traefik 2>/dev/null || true)"
-if $SUDO_CMD docker inspect dokploy-traefik >/dev/null 2>&1; then
- $SUDO_CMD docker rm -f dokploy-traefik-previous >/dev/null 2>&1 || true
- $SUDO_CMD docker rename dokploy-traefik dokploy-traefik-previous
- traefik_previous_kept=1
-fi
-${buildTraefikCreateCommand(TRAEFIK_VERSION)}
-$SUDO_CMD docker network connect dokploy-network dokploy-traefik
-for traefik_network in $traefik_networks; do
- case "$traefik_network" in
- bridge | dokploy-network) continue ;;
- esac
- if $SUDO_CMD docker network inspect "$traefik_network" >/dev/null 2>&1; then
- $SUDO_CMD docker network connect "$traefik_network" dokploy-traefik >/dev/null
+trap 'traefik_restore "being interrupted"' HUP INT TERM
+$SUDO_CMD docker rm -f dokploy-traefik-previous >/dev/null 2>&1 || true
+if [ "$traefik_existed" = 1 ]; then
+ if ! $SUDO_CMD docker stop dokploy-traefik >/dev/null; then
+ traefik_restore "stopping the running Traefik container"
fi
-done
-if [ "$traefik_previous_kept" = 1 ]; then
- $SUDO_CMD docker update --restart no dokploy-traefik-previous >/dev/null
- $SUDO_CMD docker stop dokploy-traefik-previous >/dev/null
+ traefik_old_stopped=1
+ if ! $SUDO_CMD docker rm dokploy-traefik >/dev/null; then
+ traefik_restore "removing the stopped Traefik container"
+ fi
+fi
+if ! traefik_create "traefik:v${TRAEFIK_VERSION}"; then
+ traefik_restore "creating the new Traefik container"
+fi
+if ! traefik_connect "$traefik_extra_networks"; then
+ traefik_restore "connecting the new Traefik container to its networks"
+fi
+if ! $SUDO_CMD docker start dokploy-traefik >/dev/null; then
+ traefik_restore "starting the new Traefik container"
fi
-$SUDO_CMD docker start dokploy-traefik >/dev/null
sleep "\${TRAEFIK_SETTLE_SECONDS:-10}"
-if [ "$($SUDO_CMD docker inspect -f '{{.State.Running}}' dokploy-traefik 2>/dev/null)" != "true" ]; then
- restore_traefik
+if ! traefik_check_running; then
+ traefik_restore "checking the new Traefik container (running, with no restarts)"
fi
-traefik_swapped=1
-trap - ERR HUP INT TERM
-$SUDO_CMD docker rm -f dokploy-traefik-previous >/dev/null 2>&1 || true
+if ! $SUDO_CMD docker update --restart always dokploy-traefik >/dev/null; then
+ traefik_restore "setting the restart policy of the new Traefik container"
+fi
+trap - HUP INT TERM
echo "Traefik version ${TRAEFIK_VERSION} installed ✅"
`;
case "nixpacks":
diff --git a/packages/server/src/setup/traefik-setup.ts b/packages/server/src/setup/traefik-setup.ts
index d909ae66bb0..4bfe45c08a6 100644
--- a/packages/server/src/setup/traefik-setup.ts
+++ b/packages/server/src/setup/traefik-setup.ts
@@ -24,26 +24,33 @@ export const TRAEFIK_HTTP3_PORT =
export const TRAEFIK_VERSION =
process.env.TRAEFIK_VERSION || PINNED_VERSIONS.traefik;
-const traefikContainerOptions = (version: string) => `--name dokploy-traefik \
- --restart always \
+const traefikContainerOptions = (
+ image: string,
+ restart: string,
+) => `--name dokploy-traefik \
+ --restart ${restart} \
-v /etc/dokploy/traefik/traefik.yml:/etc/traefik/traefik.yml \
-v /etc/dokploy/traefik/dynamic:/etc/dokploy/traefik/dynamic \
-v /var/run/docker.sock:/var/run/docker.sock \
-p ${TRAEFIK_SSL_PORT}:${TRAEFIK_SSL_PORT} \
-p ${TRAEFIK_PORT}:${TRAEFIK_PORT} \
-p ${TRAEFIK_HTTP3_PORT}:${TRAEFIK_HTTP3_PORT}/udp \
- traefik:v${version}`;
+ ${image}`;
export const buildTraefikRunCommand = (version: string) => `
- $SUDO_CMD docker run -d ${traefikContainerOptions(version)}
+ $SUDO_CMD docker run -d ${traefikContainerOptions(`traefik:v${version}`, "always")}
$SUDO_CMD docker network connect dokploy-network dokploy-traefik
`;
-// Created but not started, so networks are attached before the ports are taken.
-export const buildTraefikCreateCommand = (version: string) => `
- $SUDO_CMD docker create ${traefikContainerOptions(version)}
+// Created with no restart policy and not started: networks are attached before the
+// ports are taken, and the policy is raised only after the new container runs.
+export const buildTraefikCreateWithImage = (image: string) => `
+ $SUDO_CMD docker create ${traefikContainerOptions(image, "no")}
`;
+export const buildTraefikCreateCommand = (version: string) =>
+ buildTraefikCreateWithImage(`traefik:v${version}`);
+
export interface TraefikOptions {
env?: string[];
serverId?: string;
From c517f617a7ee8659e7ffc5cc1c6926b5bc82489a Mon Sep 17 00:00:00 2001
From: merge-check <140916359+agentHits@users.noreply.github.com>
Date: Thu, 8 Oct 2026 10:49:15 -0400
Subject: [PATCH 26/33] fix(servers): fail the Nixpacks and Railpack updates
when their install script does not download
The steps ran bash -c "$(curl ...)". When the download failed, the command
substitution came out empty, bash ran nothing, and the update reported
success. Each install script is now downloaded to a file with curl -fsSL,
checked for content and syntax, and only then run; any failure exits non-zero.
These steps do not touch running services, so they need no pre-download.
Co-Authored-By: Claude Haiku 5.5
---
.../__test__/server/server-components.test.ts | 54 ++++++++++++++++++-
.../server/src/setup/server-components.ts | 26 ++++++++-
2 files changed, 76 insertions(+), 4 deletions(-)
diff --git a/apps/dokploy/__test__/server/server-components.test.ts b/apps/dokploy/__test__/server/server-components.test.ts
index 45bf5a1716f..81900c65683 100644
--- a/apps/dokploy/__test__/server/server-components.test.ts
+++ b/apps/dokploy/__test__/server/server-components.test.ts
@@ -175,13 +175,25 @@ describe("buildComponentUpdateScript", () => {
const script = buildComponentUpdateScript(["nixpacks", "railpack"]);
expect(script).toContain(
- `env NIXPACKS_VERSION=${NIXPACKS_VERSION} bash -c`,
+ `env NIXPACKS_VERSION=${NIXPACKS_VERSION} bash "$nixpacks_installer"`,
);
expect(script).toContain(
- `env RAILPACK_VERSION=${RAILPACK_VERSION} bash -c`,
+ `env RAILPACK_VERSION=${RAILPACK_VERSION} bash "$railpack_installer"`,
);
});
+ it("downloads the Nixpacks and Railpack install scripts to files and checks them before running them", () => {
+ const script = buildComponentUpdateScript(["nixpacks", "railpack"]);
+
+ expect(script).toContain(
+ 'curl -fsSL https://nixpacks.com/install.sh -o "$nixpacks_installer"',
+ );
+ expect(script).toContain(
+ 'curl -fsSL https://railpack.com/install.sh -o "$railpack_installer"',
+ );
+ expect(script).toContain('bash -n "$nixpacks_installer"');
+ expect(script).not.toContain('bash -c "$(curl');
+ });
it("installs the pinned Docker and RClone", () => {
const script = buildComponentUpdateScript(["docker", "rclone"]);
@@ -451,6 +463,11 @@ printf 'systemctl %s\\n' "$*" >> "$CALL_LOG"
`,
curl: `#!/bin/sh
printf 'curl %s\\n' "$*" >> "$CALL_LOG"
+if [ -n "$FAKE_CURL_FAILS_FOR" ]; then
+ case "$*" in
+ *"$FAKE_CURL_FAILS_FOR"*) exit 22 ;;
+ esac
+fi
out=""
while [ "$#" -gt 0 ]; do
if [ "$1" = "-o" ]; then out="$2"; fi
@@ -1289,6 +1306,39 @@ describe("buildComponentUpdateScript run order", () => {
},
SPAWN_TEST_TIMEOUT_MS,
);
+ it(
+ "fails the update when the Nixpacks install script cannot be downloaded",
+ () => {
+ const run = runComponentScript(["nixpacks"], {
+ env: { FAKE_CURL_FAILS_FOR: "nixpacks.com" },
+ });
+
+ expect(run.status).not.toBe(0);
+ expect(run.stderr).toContain(
+ "the Nixpacks install script could not be downloaded or checked",
+ );
+ expect(run.stdout).not.toContain("Nixpacks version");
+ },
+ SPAWN_TEST_TIMEOUT_MS,
+ );
+
+ it(
+ "installs Railpack from the downloaded script when the download succeeds",
+ () => {
+ const run = runComponentScript(["railpack"]);
+
+ expect(run.status).toBe(0);
+ expect(
+ run.calls.some((call) =>
+ call.startsWith("curl -fsSL https://railpack.com/install.sh -o "),
+ ),
+ ).toBe(true);
+ expect(run.stdout).toContain(
+ `Railpack version ${RAILPACK_VERSION} installed`,
+ );
+ },
+ SPAWN_TEST_TIMEOUT_MS,
+ );
});
describe("buildTraefikCreateCommand", () => {
diff --git a/packages/server/src/setup/server-components.ts b/packages/server/src/setup/server-components.ts
index 87f5181d72c..ef4a786e481 100644
--- a/packages/server/src/setup/server-components.ts
+++ b/packages/server/src/setup/server-components.ts
@@ -521,13 +521,35 @@ echo "Traefik version ${TRAEFIK_VERSION} installed ✅"
case "nixpacks":
return `
echo "Updating Nixpacks to ${PINNED_VERSIONS.nixpacks}"
-$SUDO_CMD env NIXPACKS_VERSION=${PINNED_VERSIONS.nixpacks} bash -c "$(curl -fsSL https://nixpacks.com/install.sh)"
+nixpacks_installer="$(mktemp)"
+if ! curl -fsSL https://nixpacks.com/install.sh -o "$nixpacks_installer" || [ ! -s "$nixpacks_installer" ] || ! bash -n "$nixpacks_installer"; then
+ rm -f "$nixpacks_installer"
+ echo "Error: the Nixpacks install script could not be downloaded or checked; Nixpacks was not changed." >&2
+ exit 1
+fi
+if ! $SUDO_CMD env NIXPACKS_VERSION=${PINNED_VERSIONS.nixpacks} bash "$nixpacks_installer"; then
+ rm -f "$nixpacks_installer"
+ echo "Error: the Nixpacks install script failed." >&2
+ exit 1
+fi
+rm -f "$nixpacks_installer"
echo "Nixpacks version ${PINNED_VERSIONS.nixpacks} installed ✅"
`;
case "railpack":
return `
echo "Updating Railpack to ${PINNED_VERSIONS.railpack}"
-$SUDO_CMD env RAILPACK_VERSION=${PINNED_VERSIONS.railpack} bash -c "$(curl -fsSL https://railpack.com/install.sh)"
+railpack_installer="$(mktemp)"
+if ! curl -fsSL https://railpack.com/install.sh -o "$railpack_installer" || [ ! -s "$railpack_installer" ] || ! bash -n "$railpack_installer"; then
+ rm -f "$railpack_installer"
+ echo "Error: the Railpack install script could not be downloaded or checked; Railpack was not changed." >&2
+ exit 1
+fi
+if ! $SUDO_CMD env RAILPACK_VERSION=${PINNED_VERSIONS.railpack} bash "$railpack_installer"; then
+ rm -f "$railpack_installer"
+ echo "Error: the Railpack install script failed." >&2
+ exit 1
+fi
+rm -f "$railpack_installer"
echo "Railpack version ${PINNED_VERSIONS.railpack} installed ✅"
`;
case "buildpacks":
From f7fa0c317883c3d2b5fa7e1e1a819c6ad03ec90b Mon Sep 17 00:00:00 2001
From: merge-check <140916359+agentHits@users.noreply.github.com>
Date: Thu, 8 Oct 2026 10:49:39 -0400
Subject: [PATCH 27/33] docs: describe the Traefik replacement without renaming
The install guide still described the swap as renaming the running Traefik
container and gave a manual rollback that used docker rename. It now describes
stop, remove, create with --restart no, connect, start, settle check, and then
--restart always, and gives a manual rollback that recreates the container from
the previous image.
Co-Authored-By: Claude Haiku 5.5
---
docs/agenthits-install.md | 22 +++++++++++-----------
1 file changed, 11 insertions(+), 11 deletions(-)
diff --git a/docs/agenthits-install.md b/docs/agenthits-install.md
index e33e7fcc99b..7e1de11a34f 100644
--- a/docs/agenthits-install.md
+++ b/docs/agenthits-install.md
@@ -239,16 +239,16 @@ Windows (PowerShell от администратора):
0600) и не удаляется автоматически. Делается, только если Postgres будет заменён, до любой замены.
2. Redis: `docker service update` с остановкой старой задачи до запуска новой и
откатом при сбое. Ждёт ответа `PONG`.
-3. Traefik: старый контейнер переименовывается в `dokploy-traefik-previous`, новый
- создаётся и подключается к `dokploy-network` и ко всем сетям, к которым был
- подключен старый контейнер (кроме `bridge`); сети читаются на хосте во время
- обновления. Затем старый контейнер останавливается и запускается новый. Пауза,
- когда Traefik не работает, тем самым ограничена остановкой старого контейнера
- и запуском нового. Через `DOKPLOY_TRAEFIK_SETTLE` секунд (10 по умолчанию)
- проверяется, что он работает; если нет, старый контейнер возвращается.
- Старый контейнер остаётся остановленным, с `--restart no`, чтобы перезапуск
- демона не запустил его рядом с новым. При следующем обновлении Traefik он
- заменяется.
+3. Traefik: старый контейнер останавливается и удаляется (без переименования:
+ переименование запущенного контейнера, подключенного к нескольким сетям,
+ на Swarm-хосте не проходит). Перед этим installer запоминает образ и сети
+ старого контейнера на хосте (кроме `bridge`). Новый контейнер создается с
+ `--restart no`, подключается к `dokploy-network` и к тем же сетям и только
+ после этого запускается. Через `DOKPLOY_TRAEFIK_SETTLE` секунд (10 по
+ умолчанию) проверяется, что он запущен и без перезапусков; и только тогда
+ ему выставляется `--restart always`. Если на любом шаге после остановки
+ старого контейнера что-то не сходится, старый образ создается заново с теми
+ же сетями и запускается, а команда завершается ошибкой с названием шага.
4. Postgres: `docker service update` с остановкой старой задачи до запуска новой,
потому что две задачи не могут работать с одним volume. Ждёт `pg_isready`.
5. Панель: `update.sh`. Ждёт `healthy`-статуса контейнера; при откате swarm или
@@ -286,7 +286,7 @@ Windows (PowerShell от администратора):
Ручной откат. Для сервисов swarm: `docker service update --image <предыдущий образ> <сервис>`,
предыдущий образ печатается в логе шага. Для Traefik:
-`docker rm -f dokploy-traefik && docker rename dokploy-traefik-previous dokploy-traefik && docker update --restart always dokploy-traefik && docker start dokploy-traefik`.
+`docker rm -f dokploy-traefik`, затем создайте контейнер из предыдущего образа (он печатается в логе шага) с теми же флагами и подключите его сети: `docker network connect <сеть> dokploy-traefik`. Сети и флаги до обновления видны в `docker inspect dokploy-traefik`. Запуск: `docker start dokploy-traefik`.
Ручной откат Docker Engine, если автоматический не завершился. Каталог копии
имеет вид `DOKPLOY_BACKUP_DIR/docker-rollback-/`, например
From 65da65a7fd70b061e0c7787f04dce4344df991f5 Mon Sep 17 00:00:00 2001
From: merge-check <140916359+agentHits@users.noreply.github.com>
Date: Thu, 8 Oct 2026 10:58:13 -0400
Subject: [PATCH 28/33] fix(servers): fail the server setup when the Nixpacks
or Railpack install script does not download
The setup ran bash -c "$(curl -fsSL ...)". When the download failed, the
command substitution came out empty, bash ran nothing, and the setup went on
to report Nixpacks or Railpack as installed. Each install script is now
downloaded to a file with curl -fsSL, checked for content and syntax, and only
then run with the pinned version in its environment; any failure stops the
setup with a non-zero exit.
Tests check the generated setup script, and run the Nixpacks, Railpack, RClone
and Buildpacks steps with a failing download and a working one.
Co-Authored-By: Claude Haiku 5.5
---
.../__test__/server/server-setup.test.ts | 152 +++++++++++++++++-
packages/server/src/setup/server-setup.ts | 45 +++++-
2 files changed, 193 insertions(+), 4 deletions(-)
diff --git a/apps/dokploy/__test__/server/server-setup.test.ts b/apps/dokploy/__test__/server/server-setup.test.ts
index 6a23aa8f923..0a7d1c13dff 100644
--- a/apps/dokploy/__test__/server/server-setup.test.ts
+++ b/apps/dokploy/__test__/server/server-setup.test.ts
@@ -1,8 +1,19 @@
-import { execFileSync, execSync } from "node:child_process";
-import { chmodSync, mkdtempSync, writeFileSync } from "node:fs";
+import { execFileSync, execSync, spawnSync } from "node:child_process";
+import {
+ chmodSync,
+ mkdirSync,
+ mkdtempSync,
+ rmSync,
+ symlinkSync,
+ writeFileSync,
+} from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
import { defaultCommand, reportDockerVersion } from "@dokploy/server";
+import {
+ NIXPACKS_VERSION,
+ RAILPACK_VERSION,
+} from "@dokploy/server/setup/component-versions";
import { describe, expect, it } from "vitest";
const resolveBin = (name: string) =>
@@ -96,3 +107,140 @@ describe("defaultCommand", () => {
},
);
});
+
+const BASH = resolveBin("bash");
+const SETUP_STEP_TOOLS = ["bash", "env", "mktemp", "rm", "tar", "uname"];
+
+// The lines of one step of the setup script: from the echo that names it to the next step.
+const setupStep = (script: string, marker: string) => {
+ const start = script.lastIndexOf("\n", script.indexOf(marker)) + 1;
+ const next = script.indexOf('\necho -e "', start);
+ return script.slice(start, next === -1 ? script.length : next + 1);
+};
+
+// Runs one step as the setup script runs it: under set -e, with a PATH that holds only
+// the fake curl and the tools the step needs, so nixpacks, railpack and pack are absent.
+const runSetupStep = (step: string, curlMode: "ok" | "fail") => {
+ const dir = mkdtempSync(path.join(tmpdir(), "dokploy-setup-step-"));
+ try {
+ const binDir = path.join(dir, "bin");
+ mkdirSync(binDir);
+ for (const tool of SETUP_STEP_TOOLS) {
+ symlinkSync(resolveBin(tool), path.join(binDir, tool));
+ }
+ const curl = path.join(dir, "curl");
+ writeFileSync(
+ curl,
+ `#!/bin/sh
+if [ "$FAKE_CURL_MODE" = fail ]; then exit 22; fi
+out=""
+while [ "$#" -gt 0 ]; do
+ if [ "$1" = "-o" ]; then out="$2"; fi
+ shift
+done
+printf '%s\\n' 'echo fake installer "$NIXPACKS_VERSION$RAILPACK_VERSION"' > "$out"
+`,
+ );
+ chmodSync(curl, 0o755);
+ const script = [
+ "set -e",
+ 'command_exists() { command -v "$@" >/dev/null 2>&1; }',
+ 'SUDO_CMD=""',
+ "SYS_ARCH=x86_64",
+ step,
+ ].join("\n");
+ return spawnSync(BASH, [], {
+ input: script,
+ encoding: "utf8",
+ env: {
+ ...process.env,
+ PATH: `${dir}:${binDir}`,
+ FAKE_CURL_MODE: curlMode,
+ },
+ });
+ } finally {
+ rmSync(dir, { recursive: true, force: true });
+ }
+};
+
+describe("install scripts in the server setup", () => {
+ it.each([false, true])(
+ "downloads the Nixpacks and Railpack install scripts to files and checks them before running them (isBuildServer=%s)",
+ (isBuildServer) => {
+ const script = defaultCommand(isBuildServer);
+
+ expect(script).toContain(
+ 'curl -fsSL https://nixpacks.com/install.sh -o "$nixpacks_installer"',
+ );
+ expect(script).toContain('bash -n "$nixpacks_installer"');
+ expect(script).toContain(
+ `env NIXPACKS_VERSION=${NIXPACKS_VERSION} bash "$nixpacks_installer"`,
+ );
+ expect(script).toContain(
+ 'curl -fsSL https://railpack.com/install.sh -o "$railpack_installer"',
+ );
+ expect(script).toContain('bash -n "$railpack_installer"');
+ expect(script).toContain(
+ `env RAILPACK_VERSION=${RAILPACK_VERSION} bash "$railpack_installer"`,
+ );
+ expect(script).not.toContain('bash -c "$(curl');
+ },
+ );
+
+ it("stops the setup when the Nixpacks install script cannot be downloaded", () => {
+ const result = runSetupStep(
+ setupStep(defaultCommand(true), 'Installing Nixpacks"'),
+ "fail",
+ );
+
+ expect(result.status).not.toBe(0);
+ expect(result.stderr).toContain(
+ "the Nixpacks install script could not be downloaded or checked",
+ );
+ expect(result.stdout).not.toContain("Nixpacks version");
+ });
+
+ it("stops the setup when the Railpack install script cannot be downloaded", () => {
+ const result = runSetupStep(
+ setupStep(defaultCommand(true), 'Installing Railpack"'),
+ "fail",
+ );
+
+ expect(result.status).not.toBe(0);
+ expect(result.stderr).toContain(
+ "the Railpack install script could not be downloaded or checked",
+ );
+ expect(result.stdout).not.toContain("Railpack version");
+ });
+
+ it("runs the downloaded Railpack install script with the pinned version", () => {
+ const result = runSetupStep(
+ setupStep(defaultCommand(true), 'Installing Railpack"'),
+ "ok",
+ );
+
+ expect(result.status).toBe(0);
+ expect(result.stdout).toContain(`fake installer ${RAILPACK_VERSION}`);
+ expect(result.stdout).toContain(
+ `Railpack version ${RAILPACK_VERSION} installed`,
+ );
+ });
+
+ it("stops the setup when the RClone download fails", () => {
+ const result = runSetupStep(
+ setupStep(defaultCommand(true), 'Installing RClone. "'),
+ "fail",
+ );
+
+ expect(result.status).not.toBe(0);
+ });
+
+ it("stops the setup when the Buildpacks download fails", () => {
+ const result = runSetupStep(
+ setupStep(defaultCommand(true), 'Installing Buildpacks"'),
+ "fail",
+ );
+
+ expect(result.status).not.toBe(0);
+ });
+});
diff --git a/packages/server/src/setup/server-setup.ts b/packages/server/src/setup/server-setup.ts
index f7c57ea1e8b..32b8c8647a2 100644
--- a/packages/server/src/setup/server-setup.ts
+++ b/packages/server/src/setup/server-setup.ts
@@ -832,12 +832,47 @@ export const createTraefikInstance = () => {
return command;
};
+// A failed download must fail the setup. Piping curl into bash, or running it inside
+// $(...), runs nothing when the download fails and still reports success.
+const installScriptRun = ({
+ label,
+ url,
+ name,
+ variable,
+ version,
+}: {
+ label: string;
+ url: string;
+ name: string;
+ variable: string;
+ version: string;
+}) => `
+ ${name}_installer="$(mktemp)"
+ if ! curl -fsSL ${url} -o "$${name}_installer" || [ ! -s "$${name}_installer" ] || ! bash -n "$${name}_installer"; then
+ rm -f "$${name}_installer"
+ echo "Error: the ${label} install script could not be downloaded or checked; ${label} was not installed." >&2
+ exit 1
+ fi
+ if ! $SUDO_CMD env ${variable}=${version} bash "$${name}_installer"; then
+ rm -f "$${name}_installer"
+ echo "Error: the ${label} install script failed." >&2
+ exit 1
+ fi
+ rm -f "$${name}_installer"
+`;
+
const installNixpacks = () => `
if command_exists nixpacks; then
echo "Nixpacks already installed ✅"
else
export NIXPACKS_VERSION=${NIXPACKS_VERSION}
- $SUDO_CMD env NIXPACKS_VERSION=${NIXPACKS_VERSION} bash -c "$(curl -fsSL https://nixpacks.com/install.sh)"
+${installScriptRun({
+ label: "Nixpacks",
+ url: "https://nixpacks.com/install.sh",
+ name: "nixpacks",
+ variable: "NIXPACKS_VERSION",
+ version: NIXPACKS_VERSION,
+})}
echo "Nixpacks version $NIXPACKS_VERSION installed ✅"
fi
`;
@@ -847,7 +882,13 @@ const installRailpack = () => `
echo "Railpack already installed ✅"
else
export RAILPACK_VERSION=${RAILPACK_VERSION}
- $SUDO_CMD env RAILPACK_VERSION=${RAILPACK_VERSION} bash -c "$(curl -fsSL https://railpack.com/install.sh)"
+${installScriptRun({
+ label: "Railpack",
+ url: "https://railpack.com/install.sh",
+ name: "railpack",
+ variable: "RAILPACK_VERSION",
+ version: RAILPACK_VERSION,
+})}
echo "Railpack version $RAILPACK_VERSION installed ✅"
fi
`;
From e84c65404dba555dab500ea73db0bedf95611ae4 Mon Sep 17 00:00:00 2001
From: merge-check <140916359+agentHits@users.noreply.github.com>
Date: Thu, 8 Oct 2026 10:58:13 -0400
Subject: [PATCH 29/33] fix(update): fail the installer when the Nixpacks or
Railpack install script does not download
install_toolchain ran bash -c "$(curl -fsSL ...)" for Nixpacks and Railpack, so
a failed download ran nothing and the install went on. Both scripts now go
through one helper that downloads to a file, checks it, runs it with the
pinned version in its environment, and exits non-zero on any failure.
Co-Authored-By: Claude Haiku 5.5
---
.../settings/install-agenthits-update.test.ts | 88 +++++++++++++++++++
install-agenthits.sh | 25 +++++-
2 files changed, 111 insertions(+), 2 deletions(-)
diff --git a/apps/dokploy/__test__/settings/install-agenthits-update.test.ts b/apps/dokploy/__test__/settings/install-agenthits-update.test.ts
index fed2f18b375..361f3ff5856 100644
--- a/apps/dokploy/__test__/settings/install-agenthits-update.test.ts
+++ b/apps/dokploy/__test__/settings/install-agenthits-update.test.ts
@@ -7,6 +7,7 @@ import {
readFileSync,
rmSync,
statSync,
+ symlinkSync,
writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
@@ -966,3 +967,90 @@ describe("install-agenthits launchers", () => {
expect(wslSettings).toEqual(macosSettings);
});
});
+
+describe("install-agenthits.sh toolchain downloads", () => {
+ const installer = readFileSync(installerScript, "utf8");
+ const helperText = () => {
+ const start = installer.indexOf("run_install_script() {");
+ return installer.slice(start, installer.indexOf("\n}\n", start) + 3);
+ };
+ const FAKE_INSTALL_CURL = `#!/bin/sh
+if [ "$FAKE_CURL_MODE" = fail ]; then exit 22; fi
+out=""
+while [ "$#" -gt 0 ]; do
+ if [ "$1" = "-o" ]; then out="$2"; fi
+ shift
+done
+printf '%s\\n' 'echo fake nixpacks "$NIXPACKS_VERSION"' > "$out"
+`;
+
+ const runHelper = (args: string[], curlMode: "ok" | "fail") => {
+ const dir = mkdtempSync(path.join(tmpdir(), "dokploy-install-script-"));
+ try {
+ const binDir = path.join(dir, "bin");
+ mkdirSync(binDir);
+ for (const tool of ["bash", "env", "mktemp", "rm"]) {
+ symlinkSync(realPath(tool), path.join(binDir, tool));
+ }
+ const curl = path.join(dir, "curl");
+ writeFileSync(curl, FAKE_INSTALL_CURL);
+ chmodSync(curl, 0o755);
+ const script = [
+ "set -euo pipefail",
+ helperText(),
+ `run_install_script ${args.join(" ")}`,
+ ].join("\n");
+ return spawnSync(realPath("bash"), [], {
+ input: script,
+ encoding: "utf8",
+ env: {
+ ...process.env,
+ PATH: `${dir}:${binDir}`,
+ FAKE_CURL_MODE: curlMode,
+ },
+ });
+ } finally {
+ rmSync(dir, { recursive: true, force: true });
+ }
+ };
+
+ it("does not pipe the Nixpacks and Railpack install scripts into bash", () => {
+ expect(installer).toContain(
+ 'run_install_script https://nixpacks.com/install.sh Nixpacks "NIXPACKS_VERSION=$NIXPACKS_VERSION"',
+ );
+ expect(installer).toContain(
+ 'run_install_script https://railpack.com/install.sh Railpack "RAILPACK_VERSION=$RAILPACK_VERSION"',
+ );
+ expect(installer).not.toContain('bash -c "$(curl');
+ });
+
+ it("stops the install when the install script cannot be downloaded", () => {
+ const result = runHelper(
+ [
+ "https://nixpacks.com/install.sh",
+ "Nixpacks",
+ "NIXPACKS_VERSION=29.4.0",
+ ],
+ "fail",
+ );
+
+ expect(result.status).not.toBe(0);
+ expect(result.stderr).toContain(
+ "the Nixpacks install script could not be downloaded or checked",
+ );
+ });
+
+ it("runs the downloaded install script with the pinned version", () => {
+ const result = runHelper(
+ [
+ "https://nixpacks.com/install.sh",
+ "Nixpacks",
+ "NIXPACKS_VERSION=29.4.0",
+ ],
+ "ok",
+ );
+
+ expect(result.status).toBe(0);
+ expect(result.stdout).toContain("fake nixpacks 29.4.0");
+ });
+});
diff --git a/install-agenthits.sh b/install-agenthits.sh
index 5626c5a9fa3..1965c73d1ff 100755
--- a/install-agenthits.sh
+++ b/install-agenthits.sh
@@ -378,6 +378,27 @@ tool_version_matches() {
command_exists "$1" && "$@" 2>/dev/null | grep -q -- "$expected"
}
+# A failed download must stop the install: piping the script into bash, or running it
+# inside $(...), runs nothing when the download fails and still reports success.
+run_install_script() {
+ local url="$1"
+ local label="$2"
+ local assignment="$3"
+ local script=""
+ script="$(mktemp)"
+ if ! curl -fsSL "$url" -o "$script" || [ ! -s "$script" ] || ! bash -n "$script"; then
+ rm -f "$script"
+ echo "Error: the $label install script could not be downloaded or checked; $label was not installed." >&2
+ exit 1
+ fi
+ if ! env "$assignment" bash "$script"; then
+ rm -f "$script"
+ echo "Error: the $label install script failed." >&2
+ exit 1
+ fi
+ rm -f "$script"
+}
+
install_toolchain() {
local arch="" pack_suffix=""
case "$(uname -m)" in
@@ -395,10 +416,10 @@ install_toolchain() {
esac
if ! tool_version_matches "$NIXPACKS_VERSION" nixpacks --version; then
- NIXPACKS_VERSION="$NIXPACKS_VERSION" bash -c "$(curl -fsSL https://nixpacks.com/install.sh)"
+ run_install_script https://nixpacks.com/install.sh Nixpacks "NIXPACKS_VERSION=$NIXPACKS_VERSION"
fi
if ! tool_version_matches "$RAILPACK_VERSION" railpack --version; then
- RAILPACK_VERSION="$RAILPACK_VERSION" bash -c "$(curl -fsSL https://railpack.com/install.sh)"
+ run_install_script https://railpack.com/install.sh Railpack "RAILPACK_VERSION=$RAILPACK_VERSION"
fi
if ! tool_version_matches "$BUILDPACKS_VERSION" pack --version; then
curl -sSL "https://github.com/buildpacks/pack/releases/download/v${BUILDPACKS_VERSION}/pack-v${BUILDPACKS_VERSION}-linux${pack_suffix}.tgz" | tar -C /usr/local/bin/ --no-same-owner -xz pack
From 2ea6bf64bf4574fde438485a3e61b4bc011972f5 Mon Sep 17 00:00:00 2001
From: merge-check <140916359+agentHits@users.noreply.github.com>
Date: Thu, 8 Oct 2026 10:58:13 -0400
Subject: [PATCH 30/33] test(servers): check that failed Buildpacks and RClone
downloads fail the update
The Buildpacks step pipes curl into tar and the RClone step downloads with
curl -fsSL before unzip. Both already exit non-zero when the download fails,
because tar rejects empty and HTML input and set -e stops the script. These
run tests keep it that way.
Co-Authored-By: Claude Haiku 5.5
---
.../__test__/server/server-components.test.ts | 26 +++++++++++++++++++
1 file changed, 26 insertions(+)
diff --git a/apps/dokploy/__test__/server/server-components.test.ts b/apps/dokploy/__test__/server/server-components.test.ts
index 81900c65683..affcc1f6585 100644
--- a/apps/dokploy/__test__/server/server-components.test.ts
+++ b/apps/dokploy/__test__/server/server-components.test.ts
@@ -605,6 +605,7 @@ const runComponentScript = (
FAKE_PREVIOUS_ENGINE: "28.3.0",
FAKE_TARGET_ENGINE: "29.8.2",
FAKE_OLD_IMAGE: "traefik:v3.6.25",
+ TMPDIR: dir,
TRAEFIK_SETTLE_SECONDS: "0",
DOKPLOY_BACKUP_DIR: backups,
DOKPLOY_OS_RELEASE: osRelease,
@@ -1339,6 +1340,31 @@ describe("buildComponentUpdateScript run order", () => {
},
SPAWN_TEST_TIMEOUT_MS,
);
+ it(
+ "fails the update when the RClone download fails",
+ () => {
+ const run = runComponentScript(["rclone"], {
+ env: { FAKE_CURL_FAILS_FOR: "downloads.rclone.org" },
+ });
+
+ expect(run.status).not.toBe(0);
+ expect(run.stdout).not.toContain("RClone version");
+ },
+ SPAWN_TEST_TIMEOUT_MS,
+ );
+
+ it(
+ "fails the update when the Buildpacks download fails",
+ () => {
+ const run = runComponentScript(["buildpacks"], {
+ env: { FAKE_CURL_FAILS_FOR: "github.com" },
+ });
+
+ expect(run.status).not.toBe(0);
+ expect(run.stdout).not.toContain("Buildpacks version");
+ },
+ SPAWN_TEST_TIMEOUT_MS,
+ );
});
describe("buildTraefikCreateCommand", () => {
From 6adc198355eec8076e16670c0086991f83efdeac Mon Sep 17 00:00:00 2001
From: merge-check <140916359+agentHits@users.noreply.github.com>
Date: Thu, 8 Oct 2026 11:15:45 -0400
Subject: [PATCH 31/33] fix(update): reset Traefik's restart policy before the
Docker rollback starts it
A Docker rollback starts dokploy-traefik again. If the Traefik swap had left its
restart policy at "no", a daemon restart during the rollback would leave Traefik
down. The rollback now sets --restart always before it starts Traefik, on the
remote-server path and in the panel host installer.
Co-Authored-By: Claude Haiku 5.5
---
.../__test__/server/server-components.test.ts | 30 +++++++++++++++++++
.../settings/install-agenthits-update.test.ts | 29 ++++++++++++++++++
install-agenthits.sh | 7 +++--
.../server/src/setup/server-components.ts | 3 ++
4 files changed, 67 insertions(+), 2 deletions(-)
diff --git a/apps/dokploy/__test__/server/server-components.test.ts b/apps/dokploy/__test__/server/server-components.test.ts
index affcc1f6585..6f405cc1fce 100644
--- a/apps/dokploy/__test__/server/server-components.test.ts
+++ b/apps/dokploy/__test__/server/server-components.test.ts
@@ -283,6 +283,22 @@ describe("buildComponentUpdateScript", () => {
);
});
+ it("resets the Traefik restart policy before the Docker rollback starts it", () => {
+ const script = buildComponentUpdateScript(["docker"]);
+ const rollback = script.slice(
+ script.indexOf("docker_roll_back() {"),
+ script.indexOf("docker_update_from_apt() {"),
+ );
+
+ const reset = rollback.indexOf(
+ "docker update --restart always dokploy-traefik",
+ );
+ expect(reset).toBeGreaterThanOrEqual(0);
+ expect(reset).toBeLessThan(
+ rollback.indexOf("docker start dokploy-traefik"),
+ );
+ });
+
it("ends with the marker the UI waits for", () => {
expect(buildComponentUpdateScript(["buildpacks"])).toContain(
COMPONENTS_UPDATE_DONE,
@@ -1365,6 +1381,20 @@ describe("buildComponentUpdateScript run order", () => {
},
SPAWN_TEST_TIMEOUT_MS,
);
+ it(
+ "resets the Traefik restart policy during the Docker rollback",
+ () => {
+ const run = runComponentScript(["docker"], {
+ env: { FAKE_UPGRADE_KEEPS_OLD: "1" },
+ });
+
+ expect(run.status).not.toBe(0);
+ expect(run.calls).toContain(
+ "docker update --restart always dokploy-traefik",
+ );
+ },
+ SPAWN_TEST_TIMEOUT_MS,
+ );
});
describe("buildTraefikCreateCommand", () => {
diff --git a/apps/dokploy/__test__/settings/install-agenthits-update.test.ts b/apps/dokploy/__test__/settings/install-agenthits-update.test.ts
index 361f3ff5856..cc1817298cc 100644
--- a/apps/dokploy/__test__/settings/install-agenthits-update.test.ts
+++ b/apps/dokploy/__test__/settings/install-agenthits-update.test.ts
@@ -1054,3 +1054,32 @@ printf '%s\\n' 'echo fake nixpacks "$NIXPACKS_VERSION"' > "$out"
expect(result.stdout).toContain("fake nixpacks 29.4.0");
});
});
+
+describe("install-agenthits.sh Docker rollback restart policy", () => {
+ it("resets Traefik's restart policy before the Docker rollback starts it", () => {
+ const installer = readFileSync(installerScript, "utf8");
+ const rollback = installer.slice(
+ installer.indexOf("rollback_docker_engine() {"),
+ installer.indexOf("fail_docker_engine_upgrade() {"),
+ );
+
+ const reset = rollback.indexOf(
+ "docker update --restart always dokploy-traefik",
+ );
+ expect(reset).toBeGreaterThanOrEqual(0);
+ expect(reset).toBeLessThan(
+ rollback.indexOf("docker start dokploy-traefik"),
+ );
+ });
+
+ it("resets Traefik's restart policy during the Docker rollback", () => {
+ const { result, calls } = runInstaller({
+ upgradeEngine: true,
+ engineVersion: "28.3.0",
+ upgradeKeepsOld: true,
+ });
+
+ expect(result.status).not.toBe(0);
+ expect(calls).toContain("update --restart always dokploy-traefik");
+ });
+});
diff --git a/install-agenthits.sh b/install-agenthits.sh
index 1965c73d1ff..8a870532a28 100755
--- a/install-agenthits.sh
+++ b/install-agenthits.sh
@@ -1465,8 +1465,11 @@ rollback_docker_engine() {
if ! wait_until_ready docker_daemon_running; then
return 1
fi
- if docker inspect dokploy-traefik >/dev/null 2>&1 && ! traefik_running; then
- docker start dokploy-traefik >/dev/null || true
+ if docker inspect dokploy-traefik >/dev/null 2>&1; then
+ docker update --restart always dokploy-traefik >/dev/null || true
+ if ! traefik_running; then
+ docker start dokploy-traefik >/dev/null || true
+ fi
fi
wait_until_ready docker_stack_ready "$DOCKER_ENGINE_PREVIOUS_VERSION"
}
diff --git a/packages/server/src/setup/server-components.ts b/packages/server/src/setup/server-components.ts
index ef4a786e481..c37bc4cc5e0 100644
--- a/packages/server/src/setup/server-components.ts
+++ b/packages/server/src/setup/server-components.ts
@@ -345,6 +345,9 @@ docker_roll_back() {
if ! $SUDO_CMD docker info >/dev/null 2>&1; then
$SUDO_CMD systemctl start docker >/dev/null 2>&1 || true
fi
+ if [ "$traefik_existed" = 1 ]; then
+ $SUDO_CMD docker update --restart always dokploy-traefik >/dev/null 2>&1 || true
+ fi
if [ "$traefik_existed" = 1 ] && [ "$($SUDO_CMD docker inspect -f '{{.State.Running}}' dokploy-traefik 2>/dev/null)" != "true" ]; then
$SUDO_CMD docker start dokploy-traefik >/dev/null 2>&1 || true
fi
From d08ed849c7fc03820c477f1566aefa1f2ac7dc77 Mon Sep 17 00:00:00 2001
From: merge-check <140916359+agentHits@users.noreply.github.com>
Date: Thu, 8 Oct 2026 11:17:03 -0400
Subject: [PATCH 32/33] fix(update): verify Docker within its own window, 30 s
on remote servers and 120 s on the panel host
A failed Docker upgrade on a remote server waited 60 seconds before it rolled
back. The wait is now 30 one-second checks, and the rollback starts as soon as
they expire. On the panel host, the Docker checks use DOKPLOY_DOCKER_VERIFY_TIMEOUT
(120 seconds by default) instead of DOKPLOY_HEALTH_TIMEOUT, so the health timeout
of the panel update does not change how long Docker is given. The new variable
passes through the macOS and WSL launchers, and the install guide describes both
windows.
Co-Authored-By: Claude Haiku 5.5
---
.../__test__/server/server-components.test.ts | 12 ++++++
.../settings/install-agenthits-update.test.ts | 39 +++++++++++++++++++
docs/agenthits-install.md | 4 +-
install-agenthits.ps1 | 3 +-
install-agenthits.sh | 19 +++++----
.../server/src/setup/server-components.ts | 2 +-
6 files changed, 68 insertions(+), 11 deletions(-)
diff --git a/apps/dokploy/__test__/server/server-components.test.ts b/apps/dokploy/__test__/server/server-components.test.ts
index 6f405cc1fce..2c817768711 100644
--- a/apps/dokploy/__test__/server/server-components.test.ts
+++ b/apps/dokploy/__test__/server/server-components.test.ts
@@ -299,6 +299,18 @@ describe("buildComponentUpdateScript", () => {
);
});
+ it("waits 30 seconds for the new Docker before rolling back", () => {
+ const script = buildComponentUpdateScript(["docker"]);
+ const wait = script.slice(
+ script.indexOf("docker_wait_ready() {"),
+ script.indexOf("docker_roll_back() {"),
+ );
+
+ expect(wait).toContain('if [ "$attempt" -ge 30 ]; then');
+ expect(wait).toContain("sleep 1");
+ expect(wait).not.toContain("sleep 2");
+ });
+
it("ends with the marker the UI waits for", () => {
expect(buildComponentUpdateScript(["buildpacks"])).toContain(
COMPONENTS_UPDATE_DONE,
diff --git a/apps/dokploy/__test__/settings/install-agenthits-update.test.ts b/apps/dokploy/__test__/settings/install-agenthits-update.test.ts
index cc1817298cc..d18c3552e09 100644
--- a/apps/dokploy/__test__/settings/install-agenthits-update.test.ts
+++ b/apps/dokploy/__test__/settings/install-agenthits-update.test.ts
@@ -321,6 +321,7 @@ const runInstaller = (
AGENTHITS_PULL_RETRY_DELAY: "0",
DOKPLOY_HEALTH_INTERVAL: "0",
DOKPLOY_HEALTH_TIMEOUT: "2",
+ DOKPLOY_DOCKER_VERIFY_TIMEOUT: "2",
DOKPLOY_TRAEFIK_SETTLE: "0",
REDIS_IMAGE: "redis:8.10.2",
POSTGRES_IMAGE: "postgres:18.6",
@@ -1083,3 +1084,41 @@ describe("install-agenthits.sh Docker rollback restart policy", () => {
expect(calls).toContain("update --restart always dokploy-traefik");
});
});
+
+describe("install-agenthits.sh Docker verification window", () => {
+ it("checks Docker within DOKPLOY_DOCKER_VERIFY_TIMEOUT, 120 seconds by default", () => {
+ const installer = readFileSync(installerScript, "utf8");
+ const rollback = installer.slice(
+ installer.indexOf("rollback_docker_engine() {"),
+ installer.indexOf("fail_docker_engine_upgrade() {"),
+ );
+ const upgrade = installer.slice(
+ installer.indexOf("upgrade_docker_engine() {"),
+ installer.indexOf("# Order: Redis, Traefik, Postgres"),
+ );
+
+ expect(installer).toContain(
+ 'DOKPLOY_DOCKER_VERIFY_TIMEOUT="${DOKPLOY_DOCKER_VERIFY_TIMEOUT:-120}"',
+ );
+ for (const section of [rollback, upgrade]) {
+ expect(section).toContain('wait_within "$DOKPLOY_DOCKER_VERIFY_TIMEOUT"');
+ expect(section).not.toContain("DOKPLOY_HEALTH_TIMEOUT");
+ }
+ });
+
+ it("passes DOKPLOY_DOCKER_VERIFY_TIMEOUT through the macOS and WSL launchers", () => {
+ const installer = readFileSync(installerScript, "utf8");
+ const powershell = readFileSync(powershellScript, "utf8");
+ const macos = installer.slice(
+ installer.indexOf("PASSTHROUGH_VARS=("),
+ installer.indexOf("\n)", installer.indexOf("PASSTHROUGH_VARS=(")),
+ );
+ const wsl = powershell.slice(
+ powershell.indexOf("$PassthroughVars = @("),
+ powershell.indexOf("\n)", powershell.indexOf("$PassthroughVars = @(")),
+ );
+
+ expect(macos).toContain("DOKPLOY_DOCKER_VERIFY_TIMEOUT");
+ expect(wsl).toContain("'DOKPLOY_DOCKER_VERIFY_TIMEOUT'");
+ });
+});
diff --git a/docs/agenthits-install.md b/docs/agenthits-install.md
index 7e1de11a34f..3a29ecf39e3 100644
--- a/docs/agenthits-install.md
+++ b/docs/agenthits-install.md
@@ -267,7 +267,7 @@ Windows (PowerShell от администратора):
источник сам. Новые пакеты скачиваются заранее, установка идёт в самом конце.
Перезапуск демона перезапускает все сервисы swarm на хосте: live-restore к
сервисам swarm не применяется.
- После установки installer ждёт до `DOKPLOY_HEALTH_TIMEOUT` секунд, пока
+ После установки installer ждёт до `DOKPLOY_DOCKER_VERIFY_TIMEOUT` секунд (120 по умолчанию), пока
Docker Engine отвечает версией `DOCKER_VERSION`, swarm активен, `dokploy`,
`dokploy-postgres` и `dokploy-redis` готовы, а `dokploy-traefik` запущен.
Если установка пакетов не удалась или проверка не прошла, installer
@@ -276,6 +276,8 @@ Windows (PowerShell от администратора):
команду с ошибкой с указанием каталога копии. Если откат тоже не удался,
выводятся команды для ручного отката (см. ниже).
+Серверы, которые панель обновляет по SSH (раздел Components), ждут 30 секунд, пока Docker отвечает закрепленной версией. Если за это время Docker не проходит проверку, Docker откатывается сразу после истечения окна, а `dokploy-traefik` запускается снова с `--restart always`.
+
Смена major-версии Postgres (например, 17 → 18) этим обновлением не выполняется:
команда останавливается до любых изменений. Перенос данных между major-версиями
делается отдельно (dump и restore или `pg_upgrade`).
diff --git a/install-agenthits.ps1 b/install-agenthits.ps1
index 9e9fc7da978..28459c51718 100644
--- a/install-agenthits.ps1
+++ b/install-agenthits.ps1
@@ -29,7 +29,8 @@ $PassthroughVars = @(
'TRAEFIK_IMAGE', 'POSTGRES_IMAGE', 'REDIS_IMAGE', 'POSTGRES_DATA_TARGET',
'AGENTHITS_SCRIPT_BASE_URL', 'DOCKER_VERSION', 'DOCKER_SWARM_INIT_ARGS', 'ADVERTISE_ADDR', 'PUBLIC_IP',
'DOKPLOY_SERVER_HARDENING', 'HARDEN_SSH', 'HARDEN_UFW', 'HARDEN_FAIL2BAN', 'DOCKER_ENGINE_UPGRADE',
- 'DOKPLOY_BACKUP_DIR', 'DOKPLOY_HEALTH_TIMEOUT', 'DOKPLOY_HEALTH_INTERVAL', 'DOKPLOY_TRAEFIK_SETTLE', 'AGENTHITS_PULL_RETRY_DELAY'
+ 'DOKPLOY_BACKUP_DIR', 'DOKPLOY_HEALTH_TIMEOUT', 'DOKPLOY_HEALTH_INTERVAL', 'DOKPLOY_TRAEFIK_SETTLE', 'AGENTHITS_PULL_RETRY_DELAY',
+ 'DOKPLOY_DOCKER_VERIFY_TIMEOUT'
)
# VM creator id of WSL in the Hyper-V firewall (documented by Microsoft).
$WslVmCreatorId = '{40E0AC32-46A5-438A-A0B2-2B479E8F2E90}'
diff --git a/install-agenthits.sh b/install-agenthits.sh
index 8a870532a28..e8596c94978 100755
--- a/install-agenthits.sh
+++ b/install-agenthits.sh
@@ -37,6 +37,7 @@ PASSTHROUGH_VARS=(
DOCKER_ENGINE_UPGRADE
DOKPLOY_BACKUP_DIR
DOKPLOY_HEALTH_TIMEOUT
+ DOKPLOY_DOCKER_VERIFY_TIMEOUT
DOKPLOY_HEALTH_INTERVAL
DOKPLOY_TRAEFIK_SETTLE
AGENTHITS_PULL_RETRY_DELAY
@@ -48,6 +49,7 @@ HARDEN_FAIL2BAN="${HARDEN_FAIL2BAN:-0}"
DOCKER_ENGINE_UPGRADE="${DOCKER_ENGINE_UPGRADE:-0}"
DOKPLOY_BACKUP_DIR="${DOKPLOY_BACKUP_DIR:-/var/backups/dokploy}"
DOKPLOY_HEALTH_TIMEOUT="${DOKPLOY_HEALTH_TIMEOUT:-240}"
+DOKPLOY_DOCKER_VERIFY_TIMEOUT="${DOKPLOY_DOCKER_VERIFY_TIMEOUT:-120}"
DOKPLOY_HEALTH_INTERVAL="${DOKPLOY_HEALTH_INTERVAL:-3}"
DOKPLOY_TRAEFIK_SETTLE="${DOKPLOY_TRAEFIK_SETTLE:-10}"
AGENTHITS_PULL_RETRY_DELAY="${AGENTHITS_PULL_RETRY_DELAY:-10}"
@@ -1233,10 +1235,11 @@ docker_stack_ready() {
[ "$(docker_engine_version)" = "$1" ] && swarm_node_active && panel_ready && postgres_ready && redis_ready && traefik_running
}
-wait_until_ready() {
- local check="$1"
- shift
- local deadline=$(($(date +%s) + DOKPLOY_HEALTH_TIMEOUT))
+wait_within() {
+ local seconds="$1"
+ local check="$2"
+ shift 2
+ local deadline=$(($(date +%s) + seconds))
until "$check" "$@"; do
if [ "$(date +%s)" -ge "$deadline" ]; then
return 1
@@ -1462,7 +1465,7 @@ rollback_docker_engine() {
if ! docker_daemon_running; then
start_docker_service || true
fi
- if ! wait_until_ready docker_daemon_running; then
+ if ! wait_within "$DOKPLOY_DOCKER_VERIFY_TIMEOUT" docker_daemon_running; then
return 1
fi
if docker inspect dokploy-traefik >/dev/null 2>&1; then
@@ -1471,7 +1474,7 @@ rollback_docker_engine() {
docker start dokploy-traefik >/dev/null || true
fi
fi
- wait_until_ready docker_stack_ready "$DOCKER_ENGINE_PREVIOUS_VERSION"
+ wait_within "$DOKPLOY_DOCKER_VERIFY_TIMEOUT" docker_stack_ready "$DOCKER_ENGINE_PREVIOUS_VERSION"
}
fail_docker_engine_upgrade() {
@@ -1498,8 +1501,8 @@ upgrade_docker_engine() {
if ! docker_daemon_running; then
start_docker_service || true
fi
- if ! wait_until_ready docker_stack_ready "$DOCKER_VERSION"; then
- fail_docker_engine_upgrade "Error: the host was not healthy on Docker Engine $DOCKER_VERSION within $DOKPLOY_HEALTH_TIMEOUT seconds."
+ if ! wait_within "$DOKPLOY_DOCKER_VERIFY_TIMEOUT" docker_stack_ready "$DOCKER_VERSION"; then
+ fail_docker_engine_upgrade "Error: the host was not healthy on Docker Engine $DOCKER_VERSION within $DOKPLOY_DOCKER_VERIFY_TIMEOUT seconds."
fi
echo "Docker Engine is $DOCKER_VERSION"
}
diff --git a/packages/server/src/setup/server-components.ts b/packages/server/src/setup/server-components.ts
index c37bc4cc5e0..c5c7342eaf4 100644
--- a/packages/server/src/setup/server-components.ts
+++ b/packages/server/src/setup/server-components.ts
@@ -333,7 +333,7 @@ docker_wait_ready() {
if [ "$attempt" -ge 30 ]; then
return 1
fi
- sleep 2
+ sleep 1
done
}
From 9da23cc1708761424abbe506aa11e4ded6a4d093 Mon Sep 17 00:00:00 2001
From: merge-check <140916359+agentHits@users.noreply.github.com>
Date: Thu, 8 Oct 2026 11:18:23 -0400
Subject: [PATCH 33/33] fix(update): poll the new Traefik during the settle
time and stop at the first failed check
The swap waited the full settle time before checking the new Traefik container,
so a container that exited early still kept the ports dark until the wait ended.
Both paths now check it once a second over the settle time, with State.Running
and RestartCount, and roll back as soon as it is not running. The final check
after the settle time stays, and the restart policy is raised only after it.
Co-Authored-By: Claude Haiku 5.5
---
.../__test__/server/server-components.test.ts | 44 +++++++++++++++
.../settings/install-agenthits-update.test.ts | 55 ++++++++++++++++++-
docs/agenthits-install.md | 4 +-
install-agenthits.sh | 15 ++++-
.../server/src/setup/server-components.ts | 14 ++++-
5 files changed, 124 insertions(+), 8 deletions(-)
diff --git a/apps/dokploy/__test__/server/server-components.test.ts b/apps/dokploy/__test__/server/server-components.test.ts
index 2c817768711..2f209f67b3f 100644
--- a/apps/dokploy/__test__/server/server-components.test.ts
+++ b/apps/dokploy/__test__/server/server-components.test.ts
@@ -311,6 +311,14 @@ describe("buildComponentUpdateScript", () => {
expect(wait).not.toContain("sleep 2");
});
+ it("polls the new Traefik once a second over the settle time, then checks it once more", () => {
+ const script = buildComponentUpdateScript(["traefik"]);
+
+ expect(script).toContain("traefik_wait_settled() {");
+ expect(script).toContain("if ! traefik_wait_settled; then");
+ expect(script).not.toContain('sleep "${TRAEFIK_SETTLE_SECONDS');
+ });
+
it("ends with the marker the UI waits for", () => {
expect(buildComponentUpdateScript(["buildpacks"])).toContain(
COMPONENTS_UPDATE_DONE,
@@ -1407,6 +1415,42 @@ describe("buildComponentUpdateScript run order", () => {
},
SPAWN_TEST_TIMEOUT_MS,
);
+ it(
+ "checks the new Traefik once a second over the settle time, then once more",
+ () => {
+ const run = runComponentScript(["traefik"], {
+ env: { TRAEFIK_SETTLE_SECONDS: "3" },
+ });
+
+ expect(run.status).toBe(0);
+ const checks = run.calls.filter((call) =>
+ call.startsWith(
+ "docker inspect -f {{.State.Running}} {{.RestartCount}}",
+ ),
+ );
+ expect(checks).toHaveLength(4);
+ },
+ SPAWN_TEST_TIMEOUT_MS,
+ );
+
+ it(
+ "stops at the first check that finds the new Traefik not running",
+ () => {
+ const run = runComponentScript(["traefik"], {
+ env: { FAKE_NOT_RUNNING: "1", TRAEFIK_SETTLE_SECONDS: "10" },
+ });
+
+ expect(run.status).not.toBe(0);
+ expect(run.stderr).toContain("while checking the new Traefik container");
+ const checks = run.calls.filter((call) =>
+ call.startsWith(
+ "docker inspect -f {{.State.Running}} {{.RestartCount}}",
+ ),
+ );
+ expect(checks).toHaveLength(2);
+ },
+ SPAWN_TEST_TIMEOUT_MS,
+ );
});
describe("buildTraefikCreateCommand", () => {
diff --git a/apps/dokploy/__test__/settings/install-agenthits-update.test.ts b/apps/dokploy/__test__/settings/install-agenthits-update.test.ts
index d18c3552e09..321c30bc35a 100644
--- a/apps/dokploy/__test__/settings/install-agenthits-update.test.ts
+++ b/apps/dokploy/__test__/settings/install-agenthits-update.test.ts
@@ -258,6 +258,7 @@ type OperatorScenario = {
predownloadFails?: boolean;
hostCodename?: string;
repoCodename?: string;
+ settleSeconds?: string;
};
const runInstaller = (
@@ -322,7 +323,7 @@ const runInstaller = (
DOKPLOY_HEALTH_INTERVAL: "0",
DOKPLOY_HEALTH_TIMEOUT: "2",
DOKPLOY_DOCKER_VERIFY_TIMEOUT: "2",
- DOKPLOY_TRAEFIK_SETTLE: "0",
+ DOKPLOY_TRAEFIK_SETTLE: scenario.settleSeconds ?? "0",
REDIS_IMAGE: "redis:8.10.2",
POSTGRES_IMAGE: "postgres:18.6",
TRAEFIK_IMAGE: "traefik:v3.7.14",
@@ -1122,3 +1123,55 @@ describe("install-agenthits.sh Docker verification window", () => {
expect(wsl).toContain("'DOKPLOY_DOCKER_VERIFY_TIMEOUT'");
});
});
+
+describe("install-agenthits.sh Traefik settle check", () => {
+ it("polls the new Traefik once a second over the settle time, then checks it once more", () => {
+ const installer = readFileSync(installerScript, "utf8");
+
+ expect(installer).toContain("traefik_wait_settled() {");
+ expect(installer).toContain("if ! traefik_wait_settled; then");
+ expect(installer).not.toContain('sleep "$DOKPLOY_TRAEFIK_SETTLE"');
+ });
+
+ it(
+ "checks the new Traefik once a second over the settle time, then once more",
+ () => {
+ const { result, calls } = runInstaller({
+ traefikImage: "traefik:v3.6.25",
+ settleSeconds: "2",
+ });
+
+ expect(result.status).toBe(0);
+ const checks = calls.filter((call) =>
+ call.startsWith(
+ "inspect --format {{.State.Running}} {{.RestartCount}}",
+ ),
+ );
+ expect(checks).toHaveLength(3);
+ },
+ SPAWN_TEST_TIMEOUT_MS,
+ );
+
+ it(
+ "stops at the first check that finds the new Traefik not running",
+ () => {
+ const { result, calls } = runInstaller({
+ traefikImage: "traefik:v3.6.25",
+ traefikRunning: "false",
+ settleSeconds: "10",
+ });
+
+ expect(result.status).not.toBe(0);
+ expect(result.stderr).toContain(
+ "while checking the new Traefik container",
+ );
+ const checks = calls.filter((call) =>
+ call.startsWith(
+ "inspect --format {{.State.Running}} {{.RestartCount}}",
+ ),
+ );
+ expect(checks).toHaveLength(2);
+ },
+ SPAWN_TEST_TIMEOUT_MS,
+ );
+});
diff --git a/docs/agenthits-install.md b/docs/agenthits-install.md
index 3a29ecf39e3..986927067c8 100644
--- a/docs/agenthits-install.md
+++ b/docs/agenthits-install.md
@@ -244,9 +244,7 @@ Windows (PowerShell от администратора):
на Swarm-хосте не проходит). Перед этим installer запоминает образ и сети
старого контейнера на хосте (кроме `bridge`). Новый контейнер создается с
`--restart no`, подключается к `dokploy-network` и к тем же сетям и только
- после этого запускается. Через `DOKPLOY_TRAEFIK_SETTLE` секунд (10 по
- умолчанию) проверяется, что он запущен и без перезапусков; и только тогда
- ему выставляется `--restart always`. Если на любом шаге после остановки
+ после этого запускается. В течение `DOKPLOY_TRAEFIK_SETTLE` секунд (10 по умолчанию) installer раз в секунду проверяет, что он запущен и без перезапусков. Как только это перестает выполняться, обновление откатывается, не дожидаясь конца окна. В конце проверка повторяется, и только после нее ему выставляется `--restart always`. Если на любом шаге после остановки
старого контейнера что-то не сходится, старый образ создается заново с теми
же сетями и запускается, а команда завершается ошибкой с названием шага.
4. Postgres: `docker service update` с остановкой старой задачи до запуска новой,
diff --git a/install-agenthits.sh b/install-agenthits.sh
index e8596c94978..456486a8c98 100755
--- a/install-agenthits.sh
+++ b/install-agenthits.sh
@@ -1365,6 +1365,18 @@ traefik_check_running() {
# Names the step that failed. Once the old container is stopped, it is created again
# from its image with the same networks and started, before the script exits.
+traefik_wait_settled() {
+ local second=0
+ while [ "$second" -lt "$DOKPLOY_TRAEFIK_SETTLE" ]; do
+ sleep 1
+ if ! traefik_check_running; then
+ return 1
+ fi
+ second=$((second + 1))
+ done
+ traefik_check_running
+}
+
traefik_restore() {
local failed_step="$1"
trap - HUP INT TERM
@@ -1413,8 +1425,7 @@ swap_dokploy_traefik() {
if ! docker start dokploy-traefik >/dev/null; then
traefik_restore "starting the new Traefik container"
fi
- sleep "$DOKPLOY_TRAEFIK_SETTLE"
- if ! traefik_check_running; then
+ if ! traefik_wait_settled; then
traefik_restore "checking the new Traefik container (running, with no restarts)"
fi
if ! docker update --restart always dokploy-traefik >/dev/null; then
diff --git a/packages/server/src/setup/server-components.ts b/packages/server/src/setup/server-components.ts
index c5c7342eaf4..c4f01d65fee 100644
--- a/packages/server/src/setup/server-components.ts
+++ b/packages/server/src/setup/server-components.ts
@@ -476,6 +476,17 @@ traefik_connect() {
traefik_check_running() {
[ "$($SUDO_CMD docker inspect -f '{{.State.Running}} {{.RestartCount}}' dokploy-traefik 2>/dev/null)" = "true 0" ]
}
+traefik_wait_settled() {
+ local second=0
+ while [ "$second" -lt "\${TRAEFIK_SETTLE_SECONDS:-10}" ]; do
+ sleep 1
+ if ! traefik_check_running; then
+ return 1
+ fi
+ second=$((second + 1))
+ done
+ traefik_check_running
+}
traefik_restore() {
local failed_step="$1"
trap - HUP INT TERM
@@ -511,8 +522,7 @@ fi
if ! $SUDO_CMD docker start dokploy-traefik >/dev/null; then
traefik_restore "starting the new Traefik container"
fi
-sleep "\${TRAEFIK_SETTLE_SECONDS:-10}"
-if ! traefik_check_running; then
+if ! traefik_wait_settled; then
traefik_restore "checking the new Traefik container (running, with no restarts)"
fi
if ! $SUDO_CMD docker update --restart always dokploy-traefik >/dev/null; then