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 ( + + + + + + + + Server components + + + Versions installed on this server compared with the versions pinned + in Dokploy. + + + +
+ {isPending && ( +
+ Checking versions... +
+ )} + {isError && ( +

{error.message}

+ )} + {data?.map((component) => ( +
+ + {component.name} + + + installed: {component.installed ?? "not found"} + + + target: {component.target ?? "not pinned"} + + {component.outdated ? ( + + ) : ( + + )} +
+ ))} +
+ + {updatable.includes("docker") && ( +

+ Docker is upgraded and its daemon restarts. Containers with a + restart policy restart, and swarm services are briefly unavailable. +

+ )} + + {updatable.includes("traefik") && ( +

+ Traefik is recreated during the update. Services on this server are + unreachable for a few seconds. +

+ )} + + + + {logs && ( +
+						{logs}
+					
+ )} +
+
+ ); +}; 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 ( - + { + setIsOpen(open); + if (!open) { + setUpgradeDocker(false); + } + }} + >