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..537d97959e5 --- /dev/null +++ b/apps/dokploy/__test__/server/component-versions-sync.test.ts @@ -0,0 +1,175 @@ +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("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"); + + 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..2f209f67b3f --- /dev/null +++ b/apps/dokploy/__test__/server/server-components.test.ts @@ -0,0 +1,1478 @@ +import { spawnSync } from "node:child_process"; +import { + chmodSync, + existsSync, + mkdirSync, + mkdtempSync, + readdirSync, + readFileSync, + rmSync, + statSync, + symlinkSync, + writeFileSync, +} from "node:fs"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import { + BUILDPACKS_VERSION, + NIXPACKS_VERSION, + PINNED_VERSIONS, + RAILPACK_VERSION, +} from "@dokploy/server/setup/component-versions"; +import { + buildComponentStatuses, + buildComponentUpdateScript, + COMPONENTS_UPDATE_DONE, + DOCKER_UPGRADE_SKIPPED_MESSAGE, + filterDockerUpgrade, + parseComponentVersions, + type UpdatableComponent, +} from "@dokploy/server/setup/server-components"; +import { + buildTraefikCreateCommand, + buildTraefikCreateWithImage, + 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("replaces the Traefik container from the pinned image without renaming the old one", () => { + const script = buildComponentUpdateScript(["traefik"]); + + expect(script).toContain(`docker pull traefik:v${TRAEFIK_VERSION}`); + expect(script).toContain(`traefik_create "traefik:v${TRAEFIK_VERSION}"`); + expect(script).toContain(buildTraefikCreateWithImage('"$1"').trim()); + expect(script).not.toContain("docker rename"); + }); + it("stops and removes the old Traefik, creates the new one, attaches its networks, and starts it last", () => { + const script = buildComponentUpdateScript(["traefik"]); + const order = [ + script.indexOf("if ! $SUDO_CMD docker stop dokploy-traefik >/dev/null;"), + script.indexOf("if ! $SUDO_CMD docker rm dokploy-traefik >/dev/null;"), + script.indexOf('if ! traefik_create "traefik:v'), + script.indexOf('if ! traefik_connect "$traefik_extra_networks";'), + script.indexOf("if ! $SUDO_CMD docker start dokploy-traefik >/dev/null;"), + script.indexOf( + "if ! $SUDO_CMD docker update --restart always dokploy-traefik >/dev/null;", + ), + ]; + + expect(order.every((position) => position >= 0)).toBe(true); + expect(order).toEqual([...order].sort((a, b) => a - b)); + expect(script).not.toContain("docker run"); + expect(script).not.toContain("docker rename"); + }); + it("reads the networks of the running Traefik on the host, before it is replaced", () => { + const script = buildComponentUpdateScript(["traefik"]); + const read = script.indexOf( + "{{range $k,$v := .NetworkSettings.Networks}}{{$k}} {{end}}", + ); + + expect(read).toBeGreaterThanOrEqual(0); + expect(read).toBeLessThan( + script.indexOf("if ! $SUDO_CMD docker stop dokploy-traefik >/dev/null;"), + ); + expect(script).toContain("bridge | dokploy-network) continue ;;"); + }); + it("raises the restart policy of the new Traefik only after it passes the check", () => { + const script = buildComponentUpdateScript(["traefik"]); + const check = script.indexOf("if ! traefik_check_running; then"); + const raise = script.indexOf( + "if ! $SUDO_CMD docker update --restart always dokploy-traefik >/dev/null; then", + ); + + expect(script).toMatch( + /docker create --name dokploy-traefik\s+--restart no\s/, + ); + expect(check).toBeGreaterThanOrEqual(0); + expect(raise).toBeGreaterThan(check); + }); + it("passes the pinned versions into the installers through env", () => { + const script = buildComponentUpdateScript(["nixpacks", "railpack"]); + + expect(script).toContain( + `env NIXPACKS_VERSION=${NIXPACKS_VERSION} bash "$nixpacks_installer"`, + ); + expect(script).toContain( + `env RAILPACK_VERSION=${RAILPACK_VERSION} bash "$railpack_installer"`, + ); + }); + + it("downloads the Nixpacks and Railpack install scripts to files and checks them before running them", () => { + const script = buildComponentUpdateScript(["nixpacks", "railpack"]); + + expect(script).toContain( + 'curl -fsSL https://nixpacks.com/install.sh -o "$nixpacks_installer"', + ); + expect(script).toContain( + 'curl -fsSL https://railpack.com/install.sh -o "$railpack_installer"', + ); + expect(script).toContain('bash -n "$nixpacks_installer"'); + expect(script).not.toContain('bash -c "$(curl'); + }); + it("installs the pinned Docker and RClone", () => { + const script = buildComponentUpdateScript(["docker", "rclone"]); + + expect(script).toContain(`Updating Docker to ${PINNED_VERSIONS.docker}`); + expect(script).toContain( + `rclone-v${PINNED_VERSIONS.rclone}-linux-$RCLONE_RELEASE_ARCH.zip`, + ); + }); + + it("installs Docker from the pre-downloaded apt packages and never runs get.docker.com", () => { + const script = buildComponentUpdateScript(["docker"]); + + expect(script).toContain("apt-get install -y --no-download"); + expect(script).not.toContain("get.docker.com"); + }); + + 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 for each Docker-specific pre-download failure", () => { + const script = buildComponentUpdateScript(["docker", "traefik"]); + + expect(script).toContain("DOCKER_SKIPPED=1"); + expect(script).toContain( + "Docker not updated: $DOCKER_SKIP_REASON. The other components continue.", + ); + expect(script).toContain( + "this host has no apt-get, so the Docker packages cannot be pre-downloaded or rolled back", + ); + expect(script).toContain("is for $repo_codename, but this host runs"); + expect(script).toContain( + "is not offered by the Docker apt repository configured on this host", + ); + expect(script).toContain( + "is only added automatically on ubuntu and debian", + ); + expect(script).toContain( + "the current Docker packages could not be saved for rollback", + ); + expect(script).toContain( + "docker-ce is not installed, so there is no Docker Engine to roll back to", + ); + }); + + it("aborts the whole update before any change for other pre-download failures", () => { + const script = buildComponentUpdateScript(["docker", "traefik"]); + + expect(script).toContain( + `abort_before_change "Pre-download failed: traefik:v${TRAEFIK_VERSION}."`, + ); + expect(script).toContain( + 'abort_before_change "Pre-download failed: the apt package lists could not be refreshed."', + ); + expect(script).toContain( + 'abort_before_change "Pre-download failed: the Docker packages."', + ); + }); + + it("removes the Docker key and source this run wrote when a later Docker step fails", () => { + const script = buildComponentUpdateScript(["docker"]); + + expect(script).toContain('rm -f "$DOKPLOY_APT_SOURCES_DIR/docker.list"'); + expect(script).toContain('rm -f "$DOKPLOY_DOCKER_KEYRING"'); + expect(script).toMatch( + /--download-only[^\n]*; then\s+docker_undo_repo\s+docker_remove_rollback_dir\s+abort_before_change/, + ); + expect(script).toMatch( + /docker_save_rollback_packages; then\s+docker_undo_repo\s+return 1/, + ); + }); + + it("removes the saved-packages directory this run created when Docker is skipped or the update aborts", () => { + const script = buildComponentUpdateScript(["docker"]); + + expect(script).toContain('rm -rf "$DOCKER_ROLLBACK_DIR"'); + expect(script).toMatch( + /"\$DOCKER_SKIPPED" = 1 \]; then\s+docker_remove_rollback_dir\s+fi/, + ); + expect(script).toMatch( + /docker_undo_repo\s+docker_remove_rollback_dir\s+abort_before_change "Pre-download failed: the Docker packages\./, + ); + }); + + it("resets the Traefik restart policy before the Docker rollback starts it", () => { + const script = buildComponentUpdateScript(["docker"]); + const rollback = script.slice( + script.indexOf("docker_roll_back() {"), + script.indexOf("docker_update_from_apt() {"), + ); + + const reset = rollback.indexOf( + "docker update --restart always dokploy-traefik", + ); + expect(reset).toBeGreaterThanOrEqual(0); + expect(reset).toBeLessThan( + rollback.indexOf("docker start dokploy-traefik"), + ); + }); + + it("waits 30 seconds for the new Docker before rolling back", () => { + const script = buildComponentUpdateScript(["docker"]); + const wait = script.slice( + script.indexOf("docker_wait_ready() {"), + script.indexOf("docker_roll_back() {"), + ); + + expect(wait).toContain('if [ "$attempt" -ge 30 ]; then'); + expect(wait).toContain("sleep 1"); + expect(wait).not.toContain("sleep 2"); + }); + + it("polls the new Traefik once a second over the settle time, then checks it once more", () => { + const script = buildComponentUpdateScript(["traefik"]); + + expect(script).toContain("traefik_wait_settled() {"); + expect(script).toContain("if ! traefik_wait_settled; then"); + expect(script).not.toContain('sleep "${TRAEFIK_SETTLE_SECONDS'); + }); + + it("ends with the marker the UI waits for", () => { + expect(buildComponentUpdateScript(["buildpacks"])).toContain( + COMPONENTS_UPDATE_DONE, + ); + }); + + it("generates valid bash for every updatable component", () => { + const script = buildComponentUpdateScript([ + "docker", + "traefik", + "rclone", + "nixpacks", + "railpack", + "buildpacks", + ]); + const result = spawnSync("bash", ["-n"], { input: script }); + + expect(result.stderr.toString()).toBe(""); + expect(result.status).toBe(0); + }); +}); + +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 BASH = spawnSync("sh", ["-c", "command -v bash"], { + encoding: "utf8", +}).stdout.trim(); + +const FAKE_TOOLS: Record = { + docker: `#!/bin/sh +printf 'docker %s\\n' "$*" >> "$CALL_LOG" +case "$1" in + pull) [ "$FAKE_PULL_FAILS" = 1 ] && exit 1 ;; + create) + [ "$FAKE_CREATE_FAILS" = 1 ] && exit 1 + for traefik_arg in "$@"; do traefik_image_arg="$traefik_arg"; done + printf '%s\\n' "$traefik_image_arg" > "$FAKE_STATE/traefik-image" + ;; + version) cat "$FAKE_STATE/engine" ;; + network) + case "$2" in + inspect) case " $FAKE_MISSING_NETWORKS " in *" $3 "*) exit 1 ;; esac ;; + connect) case " $FAKE_CONNECT_FAILS " in *" $3 "*) exit 1 ;; esac ;; + esac + ;; + inspect) + case "$*" in + *Config.Image*) echo "$FAKE_OLD_IMAGE" ;; + *NetworkSettings*) echo "$FAKE_TRAEFIK_NETWORKS" ;; + *RestartCount*) + traefik_now="$(cat "$FAKE_STATE/traefik-image" 2>/dev/null || echo "$FAKE_OLD_IMAGE")" + if [ "$FAKE_NOT_RUNNING" = 1 ] && [ "$traefik_now" != "$FAKE_OLD_IMAGE" ]; then echo "false 0"; else echo "true 0"; fi + ;; + *State.Running*) + traefik_now="$(cat "$FAKE_STATE/traefik-image" 2>/dev/null || echo "$FAKE_OLD_IMAGE")" + if [ "$FAKE_NOT_RUNNING" = 1 ] && [ "$traefik_now" != "$FAKE_OLD_IMAGE" ]; then echo false; else echo true; fi + ;; + esac + ;; +esac +exit 0 +`, + "apt-get": `#!/bin/sh +printf 'apt-get %s\\n' "$*" >> "$CALL_LOG" +case "$1" in + update) + [ "$FAKE_UPDATE_FAILS" = 1 ] && exit 1 + ;; + download) + [ "$FAKE_DOWNLOAD_FAILS" = 1 ] && exit 1 + printf 'fake package\\n' > "\${2%%=*}.deb" + ;; + install) + case "$*" in + *--download-only*) + [ "$FAKE_PREDOWNLOAD_FAILS" = 1 ] && exit 1 + ;; + *--allow-downgrades*) + [ "$FAKE_ROLLBACK_FAILS" = 1 ] && exit 1 + printf '%s\\n' "$FAKE_PREVIOUS_ENGINE" > "$FAKE_STATE/engine" + ;; + *--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" +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) [ "$FAKE_NO_DOCKER_CE" = 1 ] || echo installed ;; + 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" +if [ -n "$FAKE_CURL_FAILS_FOR" ]; then + case "$*" in + *"$FAKE_CURL_FAILS_FOR"*) exit 22 ;; + esac +fi +out="" +while [ "$#" -gt 0 ]; do + if [ "$1" = "-o" ]; then out="$2"; fi + shift +done +printf '%s\\n' 'echo fake get-docker "$@"' > "$out" +`, + sudo: `#!/bin/sh +exec "$@" +`, + sleep: `#!/bin/sh +exit 0 +`, +}; + +const SYSTEM_TOOLS = [ + "id", + "awk", + "grep", + "date", + "mktemp", + "tee", + "dirname", + "install", + "rm", + "chmod", + "mkdir", + "sh", + "sed", + "cat", + "head", + "tr", + "cut", + "wc", + "sort", +]; + +const linkSystemTools = (binDir: string) => { + mkdirSync(binDir); + for (const name of SYSTEM_TOOLS) { + const found = spawnSync("sh", ["-c", `command -v ${name}`], { + encoding: "utf8", + }).stdout.trim(); + if (found) { + symlinkSync(found, path.join(binDir, name)); + } + } + return binDir; +}; + +const DOCKER_LIST_JAMMY = + "deb [arch=amd64 signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu jammy stable\n"; +const DOCKER_LIST_NOBLE = + "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; + withoutApt?: boolean; + preexistingKeyring?: string; +}; + +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[], + 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); + if (options.preexistingKeyring !== undefined) { + mkdirSync(path.dirname(keyring)); + writeFileSync(keyring, options.preexistingKeyring); + } + 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)) { + if (options.withoutApt && name.startsWith("apt-")) { + continue; + } + writeFileSync(path.join(dir, name), body); + chmodSync(path.join(dir, name), 0o755); + } + const pathValue = options.withoutApt + ? `${dir}:${linkSystemTools(path.join(dir, "bin"))}` + : `${dir}:${process.env.PATH}`; + const result = spawnSync(BASH, [], { + input: buildComponentUpdateScript(components), + encoding: "utf8", + env: { + ...process.env, + PATH: pathValue, + CALL_LOG: callLog, + FAKE_STATE: dir, + FAKE_PREVIOUS_ENGINE: "28.3.0", + FAKE_TARGET_ENGINE: "29.8.2", + FAKE_OLD_IMAGE: "traefik:v3.6.25", + TMPDIR: dir, + TRAEFIK_SETTLE_SECONDS: "0", + DOKPLOY_BACKUP_DIR: backups, + DOKPLOY_OS_RELEASE: osRelease, + DOKPLOY_APT_SOURCES_DIR: sources, + DOKPLOY_DOCKER_KEYRING: keyring, + ...options.env, + }, + }); + return { + status: result.status, + stdout: result.stdout, + stderr: result.stderr, + calls: readFileSync(callLog, "utf8").split("\n").filter(Boolean), + backups: listBackups(backups), + keyringExists: existsSync(keyring), + keyring: existsSync(keyring) ? readFileSync(keyring, "utf8") : null, + keyringDirExists: existsSync(path.dirname(keyring)), + sources: readSources(sources), + }; + } finally { + 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 run = runComponentScript(["traefik"], { + env: { FAKE_PULL_FAILS: "1" }, + }); + + expect(run.status).not.toBe(0); + expect(run.stderr).toContain("Nothing was changed"); + expect( + run.calls.some((call) => + /^docker (stop|rename|create|start) /.test(call), + ), + ).toBe(false); + }, + SPAWN_TEST_TIMEOUT_MS, + ); + + it( + "aborts the whole update before any change when the Traefik image cannot be pulled, with Docker selected", + () => { + const run = runComponentScript(["docker", "traefik"], { + env: { FAKE_PULL_FAILS: "1" }, + }); + + expect(run.status).not.toBe(0); + expect(run.stderr).toContain("Nothing was changed"); + expect(run.calls.some((call) => call.startsWith("apt-get"))).toBe(false); + expect(run.calls.some((call) => call.startsWith("docker rename"))).toBe( + false, + ); + }, + SPAWN_TEST_TIMEOUT_MS, + ); + + it( + "aborts the whole update before any change when the apt package lists cannot be refreshed", + () => { + const run = runComponentScript(["docker", "traefik"], { + env: { FAKE_UPDATE_FAILS: "1" }, + }); + + expect(run.status).not.toBe(0); + expect(run.stderr).toContain( + "Pre-download failed: the apt package lists could not be refreshed. Nothing was changed.", + ); + expect(run.calls.some((call) => call.startsWith("docker rename"))).toBe( + false, + ); + expect(run.calls.some((call) => call.startsWith("docker create"))).toBe( + false, + ); + expect(run.calls.some((call) => call.startsWith("apt-get install"))).toBe( + false, + ); + }, + SPAWN_TEST_TIMEOUT_MS, + ); + + it( + "aborts the whole update before any change when the Docker packages cannot be downloaded", + () => { + const run = runComponentScript(["docker", "traefik"], { + env: { FAKE_PREDOWNLOAD_FAILS: "1" }, + }); + + expect(run.status).not.toBe(0); + expect(run.stderr).toContain( + "Pre-download failed: the Docker packages. Nothing was changed.", + ); + expect(run.calls.some((call) => call.startsWith("docker rename"))).toBe( + false, + ); + expect(run.calls.some((call) => call.includes("--no-download"))).toBe( + false, + ); + }, + SPAWN_TEST_TIMEOUT_MS, + ); + + it( + "downloads every artifact before the first step changes anything", + () => { + const run = runComponentScript(["traefik", "docker"]); + + expect(run.status).toBe(0); + const download = run.calls.findIndex((call) => + call.includes("--download-only"), + ); + const pull = run.calls.findIndex((call) => + call.startsWith("docker pull traefik:v"), + ); + const firstChange = run.calls.findIndex((call) => + call.startsWith("docker stop dokploy-traefik"), + ); + expect(download).toBeGreaterThanOrEqual(0); + expect(pull).toBeGreaterThanOrEqual(0); + expect(pull).toBeLessThan(firstChange); + expect(download).toBeLessThan(firstChange); + }, + SPAWN_TEST_TIMEOUT_MS, + ); + + it("replaces Traefik only after the new container has run", () => { + const run = runComponentScript(["traefik"]); + + expect(run.status).toBe(0); + expect(run.stdout).toContain( + `Traefik version ${TRAEFIK_VERSION} installed`, + ); + const at = (prefix: string) => + run.calls.findIndex((call) => call.startsWith(prefix)); + const order = [ + at("docker pull traefik:v"), + at("docker stop dokploy-traefik"), + at("docker rm dokploy-traefik"), + at("docker create --name dokploy-traefik"), + at("docker network connect dokploy-network dokploy-traefik"), + at("docker start dokploy-traefik"), + at( + "docker inspect -f {{.State.Running}} {{.RestartCount}} dokploy-traefik", + ), + at("docker update --restart always dokploy-traefik"), + ]; + expect(order.every((position) => position >= 0)).toBe(true); + expect(order).toEqual([...order].sort((a, b) => a - b)); + expect(run.calls.at(-1)).toBe( + "docker update --restart always dokploy-traefik", + ); + }); + it("creates the new Traefik with --restart no and raises the policy only after the start", () => { + const run = runComponentScript(["traefik"]); + + expect(run.status).toBe(0); + const created = + run.calls.find((call) => + call.startsWith("docker create --name dokploy-traefik"), + ) ?? ""; + expect(created).toContain("--restart no"); + expect(created).not.toContain("--restart always"); + expect( + run.calls.indexOf("docker update --restart always dokploy-traefik"), + ).toBeGreaterThan(run.calls.indexOf("docker start dokploy-traefik")); + }); + it( + "restores --restart always on the Traefik it puts back", + () => { + const run = runComponentScript(["traefik"], { + env: { FAKE_NOT_RUNNING: "1" }, + }); + + expect(run.status).not.toBe(0); + const restore = run.calls.indexOf( + "docker update --restart always dokploy-traefik", + ); + expect(restore).toBeGreaterThanOrEqual(0); + expect(restore).toBeLessThan( + run.calls.lastIndexOf("docker start dokploy-traefik"), + ); + }, + SPAWN_TEST_TIMEOUT_MS, + ); + + it("restores the previous Traefik image when the new one is not running", () => { + const run = runComponentScript(["traefik"], { + env: { FAKE_NOT_RUNNING: "1" }, + }); + + expect(run.status).not.toBe(0); + expect(run.stderr).toContain( + "Error: Traefik update failed while checking the new Traefik container", + ); + expect(run.stderr).toContain( + "The previous Traefik container is running again.", + ); + expect(run.stdout).not.toContain("installed"); + const recreated = run.calls.filter((call) => + call.startsWith("docker create --name dokploy-traefik"), + ); + expect(recreated).toHaveLength(2); + expect(recreated[1]?.endsWith("traefik:v3.6.25")).toBe(true); + }); + it("reports the failed step when the new container cannot be created and the old one cannot be restored", () => { + const run = runComponentScript(["traefik"], { + env: { FAKE_CREATE_FAILS: "1" }, + }); + + expect(run.status).not.toBe(0); + expect(run.stderr).toContain( + "Error: Traefik update failed while creating the new Traefik container.", + ); + expect(run.stderr).toContain("failed as well"); + expect( + run.calls.some((call) => call.startsWith("docker network connect")), + ).toBe(false); + }); + it( + "connects every network of the old Traefik container before the new one starts", + () => { + 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("reports the failed step when a network cannot be connected and the old container cannot be restored", () => { + const run = runComponentScript(["traefik"], { + env: { FAKE_TRAEFIK_NETWORKS: "app-a", FAKE_CONNECT_FAILS: "app-a" }, + }); + + expect(run.status).not.toBe(0); + expect(run.stderr).toContain( + "Error: Traefik update failed while connecting the new Traefik container to its networks.", + ); + expect(run.stderr).toContain("failed as well"); + expect( + run.calls.some( + (call) => + call.startsWith("docker create --name dokploy-traefik") && + call.endsWith("traefik:v3.6.25"), + ), + ).toBe(true); + }); + it( + "downloads the pinned Docker packages before the Docker step runs", + () => { + const run = runComponentScript(["docker"]); + + 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( + 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( + "rolls Docker back when the daemon does not come back on the pinned version", + () => { + const run = runComponentScript(["docker"], { + env: { FAKE_UPGRADE_KEEPS_OLD: "1" }, + }); + + 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( + "skips only Docker when the current Docker packages cannot be saved", + () => { + const run = runComponentScript(["docker", "traefik"], { + env: { FAKE_DOWNLOAD_FAILS: "1" }, + }); + + expect(run.status).toBe(0); + expect(run.stdout).toContain( + "Docker not updated: the current Docker packages could not be saved for rollback. The other components continue.", + ); + expect(run.stdout).toContain( + `Traefik version ${TRAEFIK_VERSION} installed`, + ); + expect(run.calls.some((call) => call.startsWith("apt-get install"))).toBe( + false, + ); + }, + SPAWN_TEST_TIMEOUT_MS, + ); + + it( + "skips only Docker when docker-ce is not installed, as on hosts running docker.io", + () => { + const run = runComponentScript(["docker", "traefik"], { + env: { FAKE_NO_DOCKER_CE: "1" }, + }); + + expect(run.status).toBe(0); + expect(run.stdout).toContain( + "Docker not updated: docker-ce is not installed, so there is no Docker Engine to roll back to.", + ); + expect(run.stdout).toContain( + `Traefik version ${TRAEFIK_VERSION} installed`, + ); + expect(run.calls.some((call) => call.startsWith("apt-get install"))).toBe( + false, + ); + }, + SPAWN_TEST_TIMEOUT_MS, + ); + + it( + "skips only Docker when apt-get is missing, and never runs get.docker.com", + () => { + const run = runComponentScript(["docker", "traefik"], { + withoutApt: true, + }); + + expect(run.status).toBe(0); + expect(run.stdout).toContain( + "Docker not updated: this host has no apt-get, so the Docker packages cannot be pre-downloaded or rolled back. The other components continue.", + ); + expect(run.stdout).toContain( + `Traefik version ${TRAEFIK_VERSION} installed`, + ); + expect(run.calls.some((call) => call.startsWith("curl"))).toBe(false); + }, + SPAWN_TEST_TIMEOUT_MS, + ); + + it( + "skips only the Docker step when the apt repository is for another release", + () => { + const run = runComponentScript(["docker", "traefik"], { + sourceFiles: { "docker.list": DOCKER_LIST_JAMMY }, + }); + + expect(run.status).toBe(0); + expect(run.stdout).toContain("Docker not updated:"); + expect(run.stdout).toContain("is for jammy, but this host runs noble"); + expect(run.stdout).toContain( + `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( + "skips only Docker, without rewriting it, when the repository does not offer the pinned version", + () => { + const run = runComponentScript(["docker", "traefik"], { + env: { FAKE_NO_CANDIDATE: "1" }, + sourceFiles: { "docker.list": DOCKER_LIST_NOBLE }, + }); + + expect(run.status).toBe(0); + expect(run.stdout).toContain( + "Docker not updated: Docker 29.8.2 is not offered by the Docker apt repository configured on this host.", + ); + expect(run.stdout).toContain( + `Traefik version ${TRAEFIK_VERSION} installed`, + ); + expect(run.sources["docker.list"]).toBe(DOCKER_LIST_NOBLE); + expect(run.keyringExists).toBe(false); + }, + SPAWN_TEST_TIMEOUT_MS, + ); + + it( + "skips only Docker on an unsupported distribution, without any change", + () => { + const run = runComponentScript(["docker", "traefik"], { + env: { FAKE_CANDIDATE_AFTER_REPO: "1" }, + osRelease: "ID=fedora\nVERSION_CODENAME=40\n", + }); + + expect(run.status).toBe(0); + expect(run.stdout).toContain( + "is not in the apt sources, and Docker's repository is only added automatically on ubuntu and debian (this host is 'fedora').", + ); + expect(run.stdout).toContain( + `Traefik version ${TRAEFIK_VERSION} installed`, + ); + expect(run.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, + ); + it( + "removes the Docker key and source it wrote, then aborts, when the new packages cannot be downloaded", + () => { + const run = runComponentScript(["docker", "traefik"], { + env: { FAKE_CANDIDATE_AFTER_REPO: "1", FAKE_PREDOWNLOAD_FAILS: "1" }, + }); + + expect(run.status).not.toBe(0); + expect(run.stderr).toContain( + "Pre-download failed: the Docker packages. Nothing was changed.", + ); + expect(run.sources).toEqual({}); + expect(run.keyringExists).toBe(false); + expect(run.keyringDirExists).toBe(false); + expect(run.calls.some((call) => call.startsWith("docker rename"))).toBe( + false, + ); + }, + SPAWN_TEST_TIMEOUT_MS, + ); + + it( + "removes what it wrote, then skips only Docker, when the rollback packages cannot be saved", + () => { + const run = runComponentScript(["docker", "traefik"], { + env: { FAKE_CANDIDATE_AFTER_REPO: "1", FAKE_DOWNLOAD_FAILS: "1" }, + }); + + expect(run.status).toBe(0); + expect(run.stdout).toContain( + "Docker not updated: the current Docker packages could not be saved for rollback. The other components continue.", + ); + expect(run.stdout).toContain( + `Traefik version ${TRAEFIK_VERSION} installed`, + ); + expect(run.sources).toEqual({}); + expect(run.keyringExists).toBe(false); + expect(run.keyringDirExists).toBe(false); + }, + SPAWN_TEST_TIMEOUT_MS, + ); + + it( + "removes what it wrote, then skips only Docker, when the pinned version is still missing after the update", + () => { + const run = runComponentScript(["docker", "traefik"], { + env: { FAKE_NO_CANDIDATE: "1" }, + }); + + expect(run.status).toBe(0); + expect(run.stdout).toContain( + "Docker not updated: Docker 29.8.2 is not in the apt sources, even after adding Docker's repository. The other components continue.", + ); + expect(run.stdout).toContain( + `Traefik version ${TRAEFIK_VERSION} installed`, + ); + expect(run.sources).toEqual({}); + expect(run.keyringExists).toBe(false); + expect(run.keyringDirExists).toBe(false); + }, + SPAWN_TEST_TIMEOUT_MS, + ); + + it( + "keeps a Docker key that existed before the run and removes only the source it wrote", + () => { + const run = runComponentScript(["docker", "traefik"], { + env: { FAKE_CANDIDATE_AFTER_REPO: "1", FAKE_DOWNLOAD_FAILS: "1" }, + preexistingKeyring: "existing key\n", + }); + + expect(run.status).toBe(0); + expect(run.keyring).toBe("existing key\n"); + expect(run.sources).toEqual({}); + expect(run.calls.some((call) => call.startsWith("curl"))).toBe(false); + }, + SPAWN_TEST_TIMEOUT_MS, + ); + it( + "removes the saved packages when they cannot be saved, so Docker is skipped with nothing left behind", + () => { + const run = runComponentScript(["docker", "traefik"], { + env: { FAKE_DOWNLOAD_FAILS: "1" }, + }); + + expect(run.status).toBe(0); + expect(run.stdout).toContain( + "Docker not updated: the current Docker packages could not be saved for rollback.", + ); + expect(run.backups).toEqual([]); + }, + SPAWN_TEST_TIMEOUT_MS, + ); + + it( + "removes the saved packages when the new packages cannot be downloaded, then aborts", + () => { + const run = runComponentScript(["docker", "traefik"], { + env: { FAKE_PREDOWNLOAD_FAILS: "1" }, + }); + + expect(run.status).not.toBe(0); + expect(run.stderr).toContain( + "Pre-download failed: the Docker packages. Nothing was changed.", + ); + expect(run.backups).toEqual([]); + }, + SPAWN_TEST_TIMEOUT_MS, + ); + it( + "fails the update when the Nixpacks install script cannot be downloaded", + () => { + const run = runComponentScript(["nixpacks"], { + env: { FAKE_CURL_FAILS_FOR: "nixpacks.com" }, + }); + + expect(run.status).not.toBe(0); + expect(run.stderr).toContain( + "the Nixpacks install script could not be downloaded or checked", + ); + expect(run.stdout).not.toContain("Nixpacks version"); + }, + SPAWN_TEST_TIMEOUT_MS, + ); + + it( + "installs Railpack from the downloaded script when the download succeeds", + () => { + const run = runComponentScript(["railpack"]); + + expect(run.status).toBe(0); + expect( + run.calls.some((call) => + call.startsWith("curl -fsSL https://railpack.com/install.sh -o "), + ), + ).toBe(true); + expect(run.stdout).toContain( + `Railpack version ${RAILPACK_VERSION} installed`, + ); + }, + SPAWN_TEST_TIMEOUT_MS, + ); + it( + "fails the update when the RClone download fails", + () => { + const run = runComponentScript(["rclone"], { + env: { FAKE_CURL_FAILS_FOR: "downloads.rclone.org" }, + }); + + expect(run.status).not.toBe(0); + expect(run.stdout).not.toContain("RClone version"); + }, + SPAWN_TEST_TIMEOUT_MS, + ); + + it( + "fails the update when the Buildpacks download fails", + () => { + const run = runComponentScript(["buildpacks"], { + env: { FAKE_CURL_FAILS_FOR: "github.com" }, + }); + + expect(run.status).not.toBe(0); + expect(run.stdout).not.toContain("Buildpacks version"); + }, + SPAWN_TEST_TIMEOUT_MS, + ); + it( + "resets the Traefik restart policy during the Docker rollback", + () => { + const run = runComponentScript(["docker"], { + env: { FAKE_UPGRADE_KEEPS_OLD: "1" }, + }); + + expect(run.status).not.toBe(0); + expect(run.calls).toContain( + "docker update --restart always dokploy-traefik", + ); + }, + SPAWN_TEST_TIMEOUT_MS, + ); + it( + "checks the new Traefik once a second over the settle time, then once more", + () => { + const run = runComponentScript(["traefik"], { + env: { TRAEFIK_SETTLE_SECONDS: "3" }, + }); + + expect(run.status).toBe(0); + const checks = run.calls.filter((call) => + call.startsWith( + "docker inspect -f {{.State.Running}} {{.RestartCount}}", + ), + ); + expect(checks).toHaveLength(4); + }, + SPAWN_TEST_TIMEOUT_MS, + ); + + it( + "stops at the first check that finds the new Traefik not running", + () => { + const run = runComponentScript(["traefik"], { + env: { FAKE_NOT_RUNNING: "1", TRAEFIK_SETTLE_SECONDS: "10" }, + }); + + expect(run.status).not.toBe(0); + expect(run.stderr).toContain("while checking the new Traefik container"); + const checks = run.calls.filter((call) => + call.startsWith( + "docker inspect -f {{.State.Running}} {{.RestartCount}}", + ), + ); + expect(checks).toHaveLength(2); + }, + SPAWN_TEST_TIMEOUT_MS, + ); +}); + +describe("buildTraefikCreateCommand", () => { + 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", () => { + 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/__test__/server/server-setup.test.ts b/apps/dokploy/__test__/server/server-setup.test.ts index 6a23aa8f923..0a7d1c13dff 100644 --- a/apps/dokploy/__test__/server/server-setup.test.ts +++ b/apps/dokploy/__test__/server/server-setup.test.ts @@ -1,8 +1,19 @@ -import { execFileSync, execSync } from "node:child_process"; -import { chmodSync, mkdtempSync, writeFileSync } from "node:fs"; +import { execFileSync, execSync, spawnSync } from "node:child_process"; +import { + chmodSync, + mkdirSync, + mkdtempSync, + rmSync, + symlinkSync, + writeFileSync, +} from "node:fs"; import { tmpdir } from "node:os"; import path from "node:path"; import { defaultCommand, reportDockerVersion } from "@dokploy/server"; +import { + NIXPACKS_VERSION, + RAILPACK_VERSION, +} from "@dokploy/server/setup/component-versions"; import { describe, expect, it } from "vitest"; const resolveBin = (name: string) => @@ -96,3 +107,140 @@ describe("defaultCommand", () => { }, ); }); + +const BASH = resolveBin("bash"); +const SETUP_STEP_TOOLS = ["bash", "env", "mktemp", "rm", "tar", "uname"]; + +// The lines of one step of the setup script: from the echo that names it to the next step. +const setupStep = (script: string, marker: string) => { + const start = script.lastIndexOf("\n", script.indexOf(marker)) + 1; + const next = script.indexOf('\necho -e "', start); + return script.slice(start, next === -1 ? script.length : next + 1); +}; + +// Runs one step as the setup script runs it: under set -e, with a PATH that holds only +// the fake curl and the tools the step needs, so nixpacks, railpack and pack are absent. +const runSetupStep = (step: string, curlMode: "ok" | "fail") => { + const dir = mkdtempSync(path.join(tmpdir(), "dokploy-setup-step-")); + try { + const binDir = path.join(dir, "bin"); + mkdirSync(binDir); + for (const tool of SETUP_STEP_TOOLS) { + symlinkSync(resolveBin(tool), path.join(binDir, tool)); + } + const curl = path.join(dir, "curl"); + writeFileSync( + curl, + `#!/bin/sh +if [ "$FAKE_CURL_MODE" = fail ]; then exit 22; fi +out="" +while [ "$#" -gt 0 ]; do + if [ "$1" = "-o" ]; then out="$2"; fi + shift +done +printf '%s\\n' 'echo fake installer "$NIXPACKS_VERSION$RAILPACK_VERSION"' > "$out" +`, + ); + chmodSync(curl, 0o755); + const script = [ + "set -e", + 'command_exists() { command -v "$@" >/dev/null 2>&1; }', + 'SUDO_CMD=""', + "SYS_ARCH=x86_64", + step, + ].join("\n"); + return spawnSync(BASH, [], { + input: script, + encoding: "utf8", + env: { + ...process.env, + PATH: `${dir}:${binDir}`, + FAKE_CURL_MODE: curlMode, + }, + }); + } finally { + rmSync(dir, { recursive: true, force: true }); + } +}; + +describe("install scripts in the server setup", () => { + it.each([false, true])( + "downloads the Nixpacks and Railpack install scripts to files and checks them before running them (isBuildServer=%s)", + (isBuildServer) => { + const script = defaultCommand(isBuildServer); + + expect(script).toContain( + 'curl -fsSL https://nixpacks.com/install.sh -o "$nixpacks_installer"', + ); + expect(script).toContain('bash -n "$nixpacks_installer"'); + expect(script).toContain( + `env NIXPACKS_VERSION=${NIXPACKS_VERSION} bash "$nixpacks_installer"`, + ); + expect(script).toContain( + 'curl -fsSL https://railpack.com/install.sh -o "$railpack_installer"', + ); + expect(script).toContain('bash -n "$railpack_installer"'); + expect(script).toContain( + `env RAILPACK_VERSION=${RAILPACK_VERSION} bash "$railpack_installer"`, + ); + expect(script).not.toContain('bash -c "$(curl'); + }, + ); + + it("stops the setup when the Nixpacks install script cannot be downloaded", () => { + const result = runSetupStep( + setupStep(defaultCommand(true), 'Installing Nixpacks"'), + "fail", + ); + + expect(result.status).not.toBe(0); + expect(result.stderr).toContain( + "the Nixpacks install script could not be downloaded or checked", + ); + expect(result.stdout).not.toContain("Nixpacks version"); + }); + + it("stops the setup when the Railpack install script cannot be downloaded", () => { + const result = runSetupStep( + setupStep(defaultCommand(true), 'Installing Railpack"'), + "fail", + ); + + expect(result.status).not.toBe(0); + expect(result.stderr).toContain( + "the Railpack install script could not be downloaded or checked", + ); + expect(result.stdout).not.toContain("Railpack version"); + }); + + it("runs the downloaded Railpack install script with the pinned version", () => { + const result = runSetupStep( + setupStep(defaultCommand(true), 'Installing Railpack"'), + "ok", + ); + + expect(result.status).toBe(0); + expect(result.stdout).toContain(`fake installer ${RAILPACK_VERSION}`); + expect(result.stdout).toContain( + `Railpack version ${RAILPACK_VERSION} installed`, + ); + }); + + it("stops the setup when the RClone download fails", () => { + const result = runSetupStep( + setupStep(defaultCommand(true), 'Installing RClone. "'), + "fail", + ); + + expect(result.status).not.toBe(0); + }); + + it("stops the setup when the Buildpacks download fails", () => { + const result = runSetupStep( + setupStep(defaultCommand(true), 'Installing Buildpacks"'), + "fail", + ); + + expect(result.status).not.toBe(0); + }); +}); diff --git a/apps/dokploy/__test__/settings/agenthits-update-script.test.ts b/apps/dokploy/__test__/settings/agenthits-update-script.test.ts index 22551cf813b..e11c33e0d9d 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: { @@ -30,6 +33,7 @@ const writeFakeDocker = ( latestForkVersion?: string; serviceEnv: string[]; failPull?: boolean; + updateState?: string; }, ) => { const dockerPath = path.join(dir, "docker"); @@ -49,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 @@ -71,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 @@ -132,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); @@ -140,7 +167,10 @@ const runUpdateScript = ( ...process.env, AGENTHITS_SKIP_HOST_CHECK: "1", AGENTHITS_PULL_RETRY_DELAY: "0", + DOKPLOY_HEALTH_INTERVAL: "0", + DOKPLOY_HEALTH_TIMEOUT: "2", DOCKER_CALL_LOG: callLog, + DOCKER_INDEX_FILE: indexFile, PATH: `${tempDir}:${process.env.PATH}`, }, encoding: "utf8", @@ -156,131 +186,176 @@ 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( + "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/apps/dokploy/__test__/settings/dokploy-image-cleanup.test.ts b/apps/dokploy/__test__/settings/dokploy-image-cleanup.test.ts index 28bcc41de57..c68d12fe680 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 () => { @@ -348,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/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..321c30bc35a --- /dev/null +++ b/apps/dokploy/__test__/settings/install-agenthits-update.test.ts @@ -0,0 +1,1177 @@ +import { spawnSync } from "node:child_process"; +import { + chmodSync, + mkdirSync, + mkdtempSync, + readdirSync, + readFileSync, + rmSync, + statSync, + symlinkSync, + 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 powershellScript = path.join(repoRoot, "install-agenthits.ps1"); + +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) + case "$*" in + *Swarm*) echo active ;; + *) echo 29.8.2 ;; + esac + exit 0 + ;; + version) + cat "$DOCKER_ENGINE_FILE" + 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" ;; + *NetworkSettings*) echo "$FAKE_TRAEFIK_NETWORKS" ;; + *RestartCount*) + traefik_now="$(cat "$DOCKER_TRAEFIK_IMAGE_FILE" 2>/dev/null || echo "$FAKE_TRAEFIK_IMAGE")" + if [ "$FAKE_TRAEFIK_RUNNING" = "false" ] && [ "$traefik_now" != "$FAKE_TRAEFIK_IMAGE" ]; then echo "false 0"; else echo "true 0"; fi + ;; + *State.Running*) + traefik_now="$(cat "$DOCKER_TRAEFIK_IMAGE_FILE" 2>/dev/null || echo "$FAKE_TRAEFIK_IMAGE")" + if [ "$FAKE_TRAEFIK_RUNNING" = "false" ] && [ "$traefik_now" != "$FAKE_TRAEFIK_IMAGE" ]; then echo false; else echo true; fi + ;; + *State.Health.Status*) echo healthy ;; + esac + exit 0 + ;; + create) + for traefik_arg in "$@"; do traefik_image_arg="$traefik_arg"; done + printf '%s\\n' "$traefik_image_arg" > "$DOCKER_TRAEFIK_IMAGE_FILE" + exit 0 + ;; + network) + case "$2" in + inspect) + 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 + *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_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 + *--download-only*) + [ "$FAKE_PREDOWNLOAD_FAILS" = 1 ] && exit 1 + ;; + *--allow-downgrades*) + [ "$FAKE_ROLLBACK_FAILS" = 1 ] && exit 1 + printf '%s\\n' "$FAKE_PREVIOUS_ENGINE" > "$DOCKER_ENGINE_FILE" + ;; + *--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 +`; + +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"; + 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; + predownloadFails?: boolean; + hostCodename?: string; + repoCodename?: string; + settleSeconds?: string; +}; + +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, + }; + 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, ...args], { + encoding: "utf8", + env: { + ...process.env, + PATH: `${dir}:${process.env.PATH}`, + DOCKER_CALL_LOG: callLog, + DOCKER_INDEX_FILE: indexFile, + DOCKER_ENGINE_FILE: engineFile, + DOCKER_TRAEFIK_IMAGE_FILE: path.join(dir, "traefik-image"), + AGENTHITS_SKIP_HOST_CHECK: "1", + DOKPLOY_BACKUP_DIR: backupDir, + DOKPLOY_APT_SOURCES_DIR: sourcesDir, + DOKPLOY_OS_RELEASE: osRelease, + DOCKER_ENGINE_UPGRADE: scenario.upgradeEngine ? "1" : "0", + AGENTHITS_PULL_RETRY_DELAY: "0", + DOKPLOY_HEALTH_INTERVAL: "0", + DOKPLOY_HEALTH_TIMEOUT: "2", + DOKPLOY_DOCKER_VERIFY_TIMEOUT: "2", + DOKPLOY_TRAEFIK_SETTLE: scenario.settleSeconds ?? "0", + 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", + FAKE_PANEL_IMAGE: + 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", + FAKE_PREDOWNLOAD_FAILS: scenario.predownloadFails ? "1" : "0", + }, + }); + + const calls = readFileSync(callLog, "utf8").split("\n").filter(Boolean); + const backups = (() => { + try { + return readdirSync(backupDir); + } catch { + 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, backupEntries }; + } finally { + rmSync(dir, { force: true, recursive: true }); + } +}; + +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", + () => { + 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("create --name"))).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("create --name"))).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, backupModes } = 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(backupModes).toEqual([0o600]); + 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 traefikRemove = firstIndex(calls, "rm dokploy-traefik"); + const traefikCreate = firstIndex(calls, "create --name dokploy-traefik"); + 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", + ); + 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, + traefikRemove, + traefikCreate, + traefikStart, + postgres, + panel, + ]) { + expect(step).toBeGreaterThanOrEqual(0); + } + expect(lastPull).toBeLessThan(backup); + expect(backup).toBeLessThan(redis); + expect(redis).toBeLessThan(traefikStop); + expect(traefikStop).toBeLessThan(traefikRemove); + expect(traefikRemove).toBeLessThan(traefikCreate); + expect(traefikCreate).toBeLessThan(traefikStart); + expect(traefikStart).toBeLessThan(postgres); + expect(postgres).toBeLessThan(panel); + }, + SPAWN_TEST_TIMEOUT_MS, + ); + it( + "stops before any swap when the Postgres backup fails", + () => { + 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 image 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( + "Error: Traefik update failed while checking the new Traefik container", + ); + expect(result.stderr).toContain( + "The previous Traefik container is running again.", + ); + const recreated = calls.filter((call) => + call.startsWith("create --name dokploy-traefik"), + ); + expect(recreated).toHaveLength(2); + expect(recreated[1]?.endsWith("traefik:v3.6.25")).toBe(true); + expect(calls.some((call) => call.startsWith("service update"))).toBe( + false, + ); + }, + SPAWN_TEST_TIMEOUT_MS, + ); + it( + "reconnects every network of the old Traefik container before the new one starts", + () => { + 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( + "reports the failed step when a network cannot be connected and the old container cannot be restored", + () => { + const { result, calls } = runInstaller({ + traefikImage: "traefik:v3.6.25", + traefikNetworks: "app-a", + connectFails: "app-a", + }); + + expect(result.status).not.toBe(0); + expect(result.stderr).toContain( + "Error: Traefik update failed while creating the new Traefik container and connecting its networks.", + ); + expect(result.stderr).toContain("failed as well"); + expect( + calls.some( + (call) => + call.startsWith("create --name dokploy-traefik") && + call.endsWith("traefik:v3.6.25"), + ), + ).toBe(true); + expect(calls.some((call) => call.startsWith("service update"))).toBe( + false, + ); + }, + SPAWN_TEST_TIMEOUT_MS, + ); + it( + "refuses a Postgres major version change before anything runs", + () => { + 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, + ); +}); + +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, backupEntries } = runInstaller({ + upgradeEngine: true, + engineVersion: "28.3.0", + downloadFails: true, + }); + + expect(result.status).not.toBe(0); + expect( + backupEntries.some((entry) => + entry.name.startsWith("docker-rollback-"), + ), + ).toBe(false); + expect(result.stderr).toContain( + "Pre-download failed: current Docker packages for rollback. Nothing was changed.", + ); + 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, + ); + it( + "removes the saved packages when the new packages cannot be downloaded, then aborts", + () => { + const { result, calls, backupEntries } = runInstaller({ + upgradeEngine: true, + engineVersion: "28.3.0", + predownloadFails: true, + }); + + expect(result.status).not.toBe(0); + expect(result.stderr).toContain( + "Pre-download failed: the Docker packages. Nothing was changed.", + ); + expect( + backupEntries.some((entry) => + entry.name.startsWith("docker-rollback-"), + ), + ).toBe(false); + expect(calls.some((call) => call.startsWith("service update"))).toBe( + false, + ); + }, + SPAWN_TEST_TIMEOUT_MS, + ); +}); + +describe("install-agenthits.sh Traefik and host settings", () => { + 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, starts the new container last, and never renames", () => { + const installer = readFileSync(installerScript, "utf8"); + const swap = installer.slice(installer.indexOf("swap_dokploy_traefik() {")); + const order = [ + swap.indexOf("docker stop dokploy-traefik"), + swap.indexOf("docker rm dokploy-traefik"), + swap.indexOf('create_dokploy_traefik "$TRAEFIK_IMAGE" no'), + swap.indexOf("docker start dokploy-traefik >/dev/null"), + swap.indexOf("docker update --restart always dokploy-traefik"), + ]; + + expect(order.every((position) => position >= 0)).toBe(true); + expect(order).toEqual([...order].sort((a, b) => a - b)); + expect(installer).toContain( + 'docker network connect "$network" dokploy-traefik', + ); + expect(installer).not.toContain("docker rename"); + expect(installer).not.toContain("docker run -d"); + }); +}); + +describe("install-agenthits.sh Traefik restart policy", () => { + it("creates the new Traefik with --restart no and raises the policy only after the check", () => { + const { result, calls } = runInstaller({ traefikImage: "traefik:v3.6.25" }); + + expect(result.status).toBe(0); + const created = + calls.find((call) => call.startsWith("create --name dokploy-traefik")) ?? + ""; + expect(created).toContain("--restart no"); + expect(calls.some((call) => call.startsWith("update --restart no"))).toBe( + false, + ); + expect( + calls.indexOf("update --restart always dokploy-traefik"), + ).toBeGreaterThan(calls.indexOf("start dokploy-traefik")); + }); + it("restores --restart always on the Traefik it puts back", () => { + const { result, calls } = runInstaller({ + traefikImage: "traefik:v3.6.25", + traefikRunning: "false", + }); + + expect(result.status).not.toBe(0); + const restore = calls.indexOf("update --restart always dokploy-traefik"); + expect(restore).toBeGreaterThanOrEqual(0); + expect(restore).toBeLessThan(calls.lastIndexOf("start dokploy-traefik")); + }); +}); + +describe("install-agenthits launchers", () => { + it("passes the same settings through the macOS and WSL launchers", () => { + const installer = readFileSync(installerScript, "utf8"); + const powershell = readFileSync(powershellScript, "utf8"); + const macos = installer.slice( + installer.indexOf("PASSTHROUGH_VARS=("), + installer.indexOf("\n)", installer.indexOf("PASSTHROUGH_VARS=(")), + ); + const wsl = powershell.slice( + powershell.indexOf("$PassthroughVars = @("), + powershell.indexOf("\n)", powershell.indexOf("$PassthroughVars = @(")), + ); + const macosSettings = [...macos.matchAll(/^\s*([A-Z][A-Z0-9_]*)$/gm)] + .map((match) => match[1]) + .sort(); + const wslSettings = [...wsl.matchAll(/'([A-Z][A-Z0-9_]*)'/g)] + .map((match) => match[1]) + .sort(); + + expect(macosSettings).toContain("DOCKER_ENGINE_UPGRADE"); + expect(wslSettings).toEqual(macosSettings); + }); +}); + +describe("install-agenthits.sh toolchain downloads", () => { + const installer = readFileSync(installerScript, "utf8"); + const helperText = () => { + const start = installer.indexOf("run_install_script() {"); + return installer.slice(start, installer.indexOf("\n}\n", start) + 3); + }; + const FAKE_INSTALL_CURL = `#!/bin/sh +if [ "$FAKE_CURL_MODE" = fail ]; then exit 22; fi +out="" +while [ "$#" -gt 0 ]; do + if [ "$1" = "-o" ]; then out="$2"; fi + shift +done +printf '%s\\n' 'echo fake nixpacks "$NIXPACKS_VERSION"' > "$out" +`; + + const runHelper = (args: string[], curlMode: "ok" | "fail") => { + const dir = mkdtempSync(path.join(tmpdir(), "dokploy-install-script-")); + try { + const binDir = path.join(dir, "bin"); + mkdirSync(binDir); + for (const tool of ["bash", "env", "mktemp", "rm"]) { + symlinkSync(realPath(tool), path.join(binDir, tool)); + } + const curl = path.join(dir, "curl"); + writeFileSync(curl, FAKE_INSTALL_CURL); + chmodSync(curl, 0o755); + const script = [ + "set -euo pipefail", + helperText(), + `run_install_script ${args.join(" ")}`, + ].join("\n"); + return spawnSync(realPath("bash"), [], { + input: script, + encoding: "utf8", + env: { + ...process.env, + PATH: `${dir}:${binDir}`, + FAKE_CURL_MODE: curlMode, + }, + }); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }; + + it("does not pipe the Nixpacks and Railpack install scripts into bash", () => { + expect(installer).toContain( + 'run_install_script https://nixpacks.com/install.sh Nixpacks "NIXPACKS_VERSION=$NIXPACKS_VERSION"', + ); + expect(installer).toContain( + 'run_install_script https://railpack.com/install.sh Railpack "RAILPACK_VERSION=$RAILPACK_VERSION"', + ); + expect(installer).not.toContain('bash -c "$(curl'); + }); + + it("stops the install when the install script cannot be downloaded", () => { + const result = runHelper( + [ + "https://nixpacks.com/install.sh", + "Nixpacks", + "NIXPACKS_VERSION=29.4.0", + ], + "fail", + ); + + expect(result.status).not.toBe(0); + expect(result.stderr).toContain( + "the Nixpacks install script could not be downloaded or checked", + ); + }); + + it("runs the downloaded install script with the pinned version", () => { + const result = runHelper( + [ + "https://nixpacks.com/install.sh", + "Nixpacks", + "NIXPACKS_VERSION=29.4.0", + ], + "ok", + ); + + expect(result.status).toBe(0); + expect(result.stdout).toContain("fake nixpacks 29.4.0"); + }); +}); + +describe("install-agenthits.sh Docker rollback restart policy", () => { + it("resets Traefik's restart policy before the Docker rollback starts it", () => { + const installer = readFileSync(installerScript, "utf8"); + const rollback = installer.slice( + installer.indexOf("rollback_docker_engine() {"), + installer.indexOf("fail_docker_engine_upgrade() {"), + ); + + const reset = rollback.indexOf( + "docker update --restart always dokploy-traefik", + ); + expect(reset).toBeGreaterThanOrEqual(0); + expect(reset).toBeLessThan( + rollback.indexOf("docker start dokploy-traefik"), + ); + }); + + it("resets Traefik's restart policy during the Docker rollback", () => { + const { result, calls } = runInstaller({ + upgradeEngine: true, + engineVersion: "28.3.0", + upgradeKeepsOld: true, + }); + + expect(result.status).not.toBe(0); + expect(calls).toContain("update --restart always dokploy-traefik"); + }); +}); + +describe("install-agenthits.sh Docker verification window", () => { + it("checks Docker within DOKPLOY_DOCKER_VERIFY_TIMEOUT, 120 seconds by default", () => { + const installer = readFileSync(installerScript, "utf8"); + const rollback = installer.slice( + installer.indexOf("rollback_docker_engine() {"), + installer.indexOf("fail_docker_engine_upgrade() {"), + ); + const upgrade = installer.slice( + installer.indexOf("upgrade_docker_engine() {"), + installer.indexOf("# Order: Redis, Traefik, Postgres"), + ); + + expect(installer).toContain( + 'DOKPLOY_DOCKER_VERIFY_TIMEOUT="${DOKPLOY_DOCKER_VERIFY_TIMEOUT:-120}"', + ); + for (const section of [rollback, upgrade]) { + expect(section).toContain('wait_within "$DOKPLOY_DOCKER_VERIFY_TIMEOUT"'); + expect(section).not.toContain("DOKPLOY_HEALTH_TIMEOUT"); + } + }); + + it("passes DOKPLOY_DOCKER_VERIFY_TIMEOUT through the macOS and WSL launchers", () => { + const installer = readFileSync(installerScript, "utf8"); + const powershell = readFileSync(powershellScript, "utf8"); + const macos = installer.slice( + installer.indexOf("PASSTHROUGH_VARS=("), + installer.indexOf("\n)", installer.indexOf("PASSTHROUGH_VARS=(")), + ); + const wsl = powershell.slice( + powershell.indexOf("$PassthroughVars = @("), + powershell.indexOf("\n)", powershell.indexOf("$PassthroughVars = @(")), + ); + + expect(macos).toContain("DOKPLOY_DOCKER_VERIFY_TIMEOUT"); + expect(wsl).toContain("'DOKPLOY_DOCKER_VERIFY_TIMEOUT'"); + }); +}); + +describe("install-agenthits.sh Traefik settle check", () => { + it("polls the new Traefik once a second over the settle time, then checks it once more", () => { + const installer = readFileSync(installerScript, "utf8"); + + expect(installer).toContain("traefik_wait_settled() {"); + expect(installer).toContain("if ! traefik_wait_settled; then"); + expect(installer).not.toContain('sleep "$DOKPLOY_TRAEFIK_SETTLE"'); + }); + + it( + "checks the new Traefik once a second over the settle time, then once more", + () => { + const { result, calls } = runInstaller({ + traefikImage: "traefik:v3.6.25", + settleSeconds: "2", + }); + + expect(result.status).toBe(0); + const checks = calls.filter((call) => + call.startsWith( + "inspect --format {{.State.Running}} {{.RestartCount}}", + ), + ); + expect(checks).toHaveLength(3); + }, + SPAWN_TEST_TIMEOUT_MS, + ); + + it( + "stops at the first check that finds the new Traefik not running", + () => { + const { result, calls } = runInstaller({ + traefikImage: "traefik:v3.6.25", + traefikRunning: "false", + settleSeconds: "10", + }); + + expect(result.status).not.toBe(0); + expect(result.stderr).toContain( + "while checking the new Traefik container", + ); + const checks = calls.filter((call) => + call.startsWith( + "inspect --format {{.State.Running}} {{.RestartCount}}", + ), + ); + expect(checks).toHaveLength(2); + }, + SPAWN_TEST_TIMEOUT_MS, + ); +}); diff --git a/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 [upgradeDocker, setUpgradeDocker] = useState(false); + + 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, upgradeDocker }, + { + 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 ( + { + setIsOpen(open); + if (!open) { + setUpgradeDocker(false); + } + }} + > + + + + + + + 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("docker") && ( +
+ setUpgradeDocker(checked === true)} + /> + +
+ )} + + {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/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/apps/dokploy/server/api/routers/server.ts b/apps/dokploy/server/api/routers/server.ts index 4b5e8a95e02..fb15250aa51 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,71 @@ 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), + upgradeDocker: z.boolean().default(false), + }), + ) + .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); + }, + input.upgradeDocker, + ) + .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..986927067c8 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 в @@ -228,6 +228,80 @@ 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` в `DOKPLOY_BACKUP_DIR` (по умолчанию + `/var/backups/dokploy`). Файл остаётся на хосте, доступен только root (каталог 0700, файл + 0600) и не удаляется автоматически. Делается, только если Postgres будет заменён, до любой замены. +2. Redis: `docker service update` с остановкой старой задачи до запуска новой и + откатом при сбое. Ждёт ответа `PONG`. +3. Traefik: старый контейнер останавливается и удаляется (без переименования: + переименование запущенного контейнера, подключенного к нескольким сетям, + на Swarm-хосте не проходит). Перед этим installer запоминает образ и сети + старого контейнера на хосте (кроме `bridge`). Новый контейнер создается с + `--restart no`, подключается к `dokploy-network` и к тем же сетям и только + после этого запускается. В течение `DOKPLOY_TRAEFIK_SETTLE` секунд (10 по умолчанию) installer раз в секунду проверяет, что он запущен и без перезапусков. Как только это перестает выполняться, обновление откатывается, не дожидаясь конца окна. В конце проверка повторяется, и только после нее ему выставляется `--restart always`. Если на любом шаге после остановки + старого контейнера что-то не сходится, старый образ создается заново с теми + же сетями и запускается, а команда завершается ошибкой с названием шага. +4. Postgres: `docker service update` с остановкой старой задачи до запуска новой, + потому что две задачи не могут работать с одним volume. Ждёт `pg_isready`. +5. Панель: `update.sh`. Ждёт `healthy`-статуса контейнера; при откате swarm или + нездоровом статусе команда завершается с ошибкой. +6. Docker Engine: только при `DOCKER_ENGINE_UPGRADE=1` и если установленная + версия отличается от `DOCKER_VERSION`. Включайте переменную в окружении + команды `update`, например `DOCKER_ENGINE_UPGRADE=1 bash install-agenthits.sh update`; + она передаётся через launcher на macOS и в Windows (WSL). По умолчанию выключено. + Перед любыми изменениями installer сохраняет текущие пакеты Docker в + `DOKPLOY_BACKUP_DIR/docker-rollback-/` (каталог 0700, файлы 0600): + `docker-ce` и `docker-ce-cli` всегда, остальные пакеты, если их версия меняется. + Если сохранить пакеты не удалось, обновление останавливается с сообщением + `Pre-download failed: current Docker packages for rollback. Nothing was changed.` + Если источник Docker в `/etc/apt/sources.list.d` предназначен для другого + релиза, обновление тоже останавливается до изменений: installer не переписывает + источник сам. Новые пакеты скачиваются заранее, установка идёт в самом конце. + Перезапуск демона перезапускает все сервисы swarm на хосте: live-restore к + сервисам swarm не применяется. + После установки installer ждёт до `DOKPLOY_DOCKER_VERIFY_TIMEOUT` секунд (120 по умолчанию), пока + Docker Engine отвечает версией `DOCKER_VERSION`, swarm активен, `dokploy`, + `dokploy-postgres` и `dokploy-redis` готовы, а `dokploy-traefik` запущен. + Если установка пакетов не удалась или проверка не прошла, installer + автоматически устанавливает сохранённые пакеты обратно, ждёт демон, + запускает `dokploy-traefik`, проверяет старую версию и сервисы и завершает + команду с ошибкой с указанием каталога копии. Если откат тоже не удался, + выводятся команды для ручного отката (см. ниже). + +Серверы, которые панель обновляет по SSH (раздел Components), ждут 30 секунд, пока Docker отвечает закрепленной версией. Если за это время Docker не проходит проверку, Docker откатывается сразу после истечения окна, а `dokploy-traefik` запускается снова с `--restart always`. + +Смена major-версии Postgres (например, 17 → 18) этим обновлением не выполняется: +команда останавливается до любых изменений. Перенос данных между major-версиями +делается отдельно (dump и restore или `pg_upgrade`). + +Простой на каждом шаге равен времени остановки старой задачи (или контейнера) и +запуска новой: образы уже локальные, поэтому скачивание в простое не участвует. +Единственный шаг, который перезапускает демон Docker, — шаг 6. + +Ручной откат. Для сервисов swarm: `docker service update --image <предыдущий образ> <сервис>`, +предыдущий образ печатается в логе шага. Для Traefik: +`docker rm -f dokploy-traefik`, затем создайте контейнер из предыдущего образа (он печатается в логе шага) с теми же флагами и подключите его сети: `docker network connect <сеть> dokploy-traefik`. Сети и флаги до обновления видны в `docker inspect dokploy-traefik`. Запуск: `docker start dokploy-traefik`. + +Ручной откат Docker Engine, если автоматический не завершился. Каталог копии +имеет вид `DOKPLOY_BACKUP_DIR/docker-rollback-/`, например +`/var/backups/dokploy/docker-rollback-20261008T120000Z`; подставьте свой: + +```bash +apt-get install -y --allow-downgrades --no-download /var/backups/dokploy/docker-rollback-/*.deb +systemctl start docker +docker start dokploy-traefik +docker version --format '{{.Server.Version}}' +``` + +Последняя команда должна показать версию Engine, которая была до обновления. +`docker start dokploy-traefik` безопасен, даже если контейнер уже запущен. + В dashboard кнопка `Check for updates` для AgentHits fork сравнивает текущий Docker service image digest с `ghcr.io/agenthits/dokploy:agenthits-dev`. Если digest отличается, обновление через UI запускает `docker service update` diff --git a/docs/server-components-update.md b/docs/server-components-update.md new file mode 100644 index 00000000000..993a606b279 --- /dev/null +++ b/docs/server-components-update.md @@ -0,0 +1,56 @@ +# Версии компонентов: один источник, установка и обновление + +Все версии, которые ставит форк, лежат в одном месте: `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** на карточке сервера сравнивает установленные версии с закреплёнными и обновляет отличающиеся. Сначала скачивается всё, что нужно выбранным компонентам: образ Traefik, скрипт get.docker.com и (на хостах с apt) пакеты Docker. Если какая-либо загрузка не удалась, команда завершается с сообщением `Nothing was changed`, и ни один сервис не трогается: + +- 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) уже установленное не обновляет. Поэтому после смены версии нужна именно эта кнопка. + +## Как поменять версию + +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.40.1 (миграция `0207_railpack_default_0_40_1`). Существующие приложения сохраняют свою версию, миграция её не меняет. +- Версии Dokploy (`DOKPLOY_OFFICIAL_VERSION`, сейчас v0.29.8) и npm-зависимости в этот список не входят. +- Обновление Traefik и Docker проверено тестами с подменёнными командами docker, apt-get, apt-cache и curl. Полный прогон с приложениями на живом сервере не выполнялся. +- Предварительная загрузка пакетов Docker работает только на хостах с apt. Если в apt-репозиториях нет закреплённой версии Docker, обновление останавливается до любых изменений. diff --git a/docs/server-hardening.md b/docs/server-hardening.md new file mode 100644 index 00000000000..cce63f414de --- /dev/null +++ b/docs/server-hardening.md @@ -0,0 +1,125 @@ +# Усиление защиты серверов: 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.14`. Тест `apps/dokploy/__test__/server/component-versions-sync.test.ts` падает, если версия по умолчанию в панели расходится с установщиком. + +Флаги усиления принимают только `install` и `harden`. `install-agenthits.sh update` обновляет панель и её стек, но не меняет SSH, UFW и Fail2Ban. + +## Как запускать + +### 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 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. Панель целиком (машина OrbStack): + + ```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..28459c51718 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,10 @@ $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', 'DOCKER_ENGINE_UPGRADE', + 'DOKPLOY_BACKUP_DIR', 'DOKPLOY_HEALTH_TIMEOUT', 'DOKPLOY_HEALTH_INTERVAL', 'DOKPLOY_TRAEFIK_SETTLE', 'AGENTHITS_PULL_RETRY_DELAY', + 'DOKPLOY_DOCKER_VERIFY_TIMEOUT' ) # VM creator id of WSL in the Hyper-V firewall (documented by Microsoft). $WslVmCreatorId = '{40E0AC32-46A5-438A-A0B2-2B479E8F2E90}' diff --git a/install-agenthits.sh b/install-agenthits.sh index ca3b5d578b7..456486a8c98 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,39 @@ PASSTHROUGH_VARS=( DOCKER_SWARM_INIT_ARGS ADVERTISE_ADDR PUBLIC_IP + DOKPLOY_SERVER_HARDENING + HARDEN_SSH + HARDEN_UFW + HARDEN_FAIL2BAN + DOCKER_ENGINE_UPGRADE + DOKPLOY_BACKUP_DIR + DOKPLOY_HEALTH_TIMEOUT + DOKPLOY_DOCKER_VERIFY_TIMEOUT + DOKPLOY_HEALTH_INTERVAL + DOKPLOY_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}" +DOKPLOY_BACKUP_DIR="${DOKPLOY_BACKUP_DIR:-/var/backups/dokploy}" +DOKPLOY_HEALTH_TIMEOUT="${DOKPLOY_HEALTH_TIMEOUT:-240}" +DOKPLOY_DOCKER_VERIFY_TIMEOUT="${DOKPLOY_DOCKER_VERIFY_TIMEOUT:-120}" +DOKPLOY_HEALTH_INTERVAL="${DOKPLOY_HEALTH_INTERVAL:-3}" +DOKPLOY_TRAEFIK_SETTLE="${DOKPLOY_TRAEFIK_SETTLE:-10}" +AGENTHITS_PULL_RETRY_DELAY="${AGENTHITS_PULL_RETRY_DELAY:-10}" +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="" +DOCKER_ROLLBACK_DIR_CREATED=0 +TRAEFIK_EXTRA_NETWORKS="" +TRAEFIK_OLD_IMAGE="" +TRAEFIK_OLD_STOPPED=0 command_exists() { command -v "$@" >/dev/null 2>&1 @@ -68,7 +103,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%%.*}" @@ -241,11 +277,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 @@ -254,6 +286,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 @@ -334,6 +374,72 @@ install_docker_if_missing() { ensure_docker_running } +tool_version_matches() { + local expected="$1" + shift + command_exists "$1" && "$@" 2>/dev/null | grep -q -- "$expected" +} + +# A failed download must stop the install: piping the script into bash, or running it +# inside $(...), runs nothing when the download fails and still reports success. +run_install_script() { + local url="$1" + local label="$2" + local assignment="$3" + local script="" + script="$(mktemp)" + if ! curl -fsSL "$url" -o "$script" || [ ! -s "$script" ] || ! bash -n "$script"; then + rm -f "$script" + echo "Error: the $label install script could not be downloaded or checked; $label was not installed." >&2 + exit 1 + fi + if ! env "$assignment" bash "$script"; then + rm -f "$script" + echo "Error: the $label install script failed." >&2 + exit 1 + fi + rm -f "$script" +} + +install_toolchain() { + local arch="" pack_suffix="" + case "$(uname -m)" in + 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 + run_install_script https://nixpacks.com/install.sh Nixpacks "NIXPACKS_VERSION=$NIXPACKS_VERSION" + fi + if ! tool_version_matches "$RAILPACK_VERSION" railpack --version; then + run_install_script https://railpack.com/install.sh Railpack "RAILPACK_VERSION=$RAILPACK_VERSION" + fi + if ! tool_version_matches "$BUILDPACKS_VERSION" pack --version; then + curl -sSL "https://github.com/buildpacks/pack/releases/download/v${BUILDPACKS_VERSION}/pack-v${BUILDPACKS_VERSION}-linux${pack_suffix}.tgz" | tar -C /usr/local/bin/ --no-same-owner -xz pack + 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 +725,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 +743,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="" @@ -666,7 +923,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)" @@ -680,6 +937,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 +979,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" \ @@ -728,18 +991,9 @@ 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" + run_dokploy_traefik "$TRAEFIK_IMAGE" - docker network connect dokploy-network dokploy-traefik + apply_hardening local public_ip="${PUBLIC_IP:-${ADVERTISE_ADDR:-$(get_public_ip)}}" local formatted_addr @@ -752,35 +1006,602 @@ 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 +} + +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. +remove_docker_rollback_dir() { + if [ "$DOCKER_ROLLBACK_DIR_CREATED" = "1" ]; then + rm -rf "$DOCKER_ROLLBACK_DIR" + DOCKER_ROLLBACK_DIR_CREATED=0 + fi +} + +save_docker_rollback_packages() { + local package="" + local installed="" + 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 "$DOCKER_ROLLBACK_DIR"); then + echo "Pre-download failed: current Docker packages for rollback. Nothing was changed." >&2 + exit 1 + fi + DOCKER_ROLLBACK_DIR_CREATED=1 + for package in docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin docker-ce-rootless-extras; do + installed="$(docker_package_installed_version "$package")" + if [ -z "$installed" ]; then + 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 + remove_docker_rollback_dir + 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 + 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 + save_docker_rollback_packages + if ! download_with_retry apt-get install -y -qq --download-only "docker-ce=$DOCKER_PACKAGE_VERSION" "docker-ce-cli=$DOCKER_PACKAGE_VERSION" containerd.io docker-buildx-plugin docker-compose-plugin; then + remove_docker_rollback_dir + echo "Pre-download failed: the Docker packages. Nothing was changed." >&2 + exit 1 + fi +} + +# 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 +} - # 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" +# 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 + 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 +} + +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 +} + +panel_ready() { + local container="" + container="$(task_container dokploy)" + [ -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_within() { + local seconds="$1" + local check="$2" + shift 2 + local deadline=$(($(date +%s) + seconds)) + until "$check" "$@"; do + if [ "$(date +%s)" -ge "$deadline" ]; then + return 1 + fi + sleep "$DOKPLOY_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) + 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)" + 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 $DOKPLOY_HEALTH_TIMEOUT seconds. Roll back with: docker service update --image $previous_image $service" >&2 + exit 1 + fi + sleep "$DOKPLOY_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 + # The dump holds the data and the role password hashes, so it stays private. + 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" + echo "Error: the Postgres backup failed. Nothing was changed." >&2 + exit 1 + fi + echo "Postgres backup written to $file" +} + +# 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 +} + +# Created but not started, with the given restart policy, and connected to all its +# networks before it starts, so the time without a running Traefik stays short. +create_dokploy_traefik() { + local image="$1" + local restart="$2" + docker create \ + --name dokploy-traefik \ + --restart "$restart" \ + -v /etc/dokploy/traefik/traefik.yml:/etc/traefik/traefik.yml \ + -v /etc/dokploy/traefik/dynamic:/etc/dokploy/traefik/dynamic \ + -v /var/run/docker.sock:/var/run/docker.sock:ro \ + -p 80:80/tcp \ + -p 443:443/tcp \ + -p 443:443/udp \ + "$image" >/dev/null || return 1 + + docker network connect dokploy-network dokploy-traefik || return 1 + local network="" + for network in $TRAEFIK_EXTRA_NETWORKS; do + if docker network inspect "$network" >/dev/null 2>&1; then + docker network connect "$network" dokploy-traefik || return 1 + fi + done +} + +run_dokploy_traefik() { + create_dokploy_traefik "$1" always + docker start dokploy-traefik >/dev/null +} + +traefik_check_running() { + [ "$(docker inspect --format '{{.State.Running}} {{.RestartCount}}' dokploy-traefik 2>/dev/null)" = "true 0" ] +} + +# Names the step that failed. Once the old container is stopped, it is created again +# from its image with the same networks and started, before the script exits. +traefik_wait_settled() { + local second=0 + while [ "$second" -lt "$DOKPLOY_TRAEFIK_SETTLE" ]; do + sleep 1 + if ! traefik_check_running; then + return 1 + fi + second=$((second + 1)) + done + traefik_check_running +} + +traefik_restore() { + local failed_step="$1" + trap - HUP INT TERM + echo "Error: Traefik update failed while $failed_step." >&2 + if [ "$TRAEFIK_OLD_STOPPED" = "1" ]; then + echo "Restoring the previous Traefik container from $TRAEFIK_OLD_IMAGE." >&2 + docker rm -f dokploy-traefik >/dev/null 2>&1 || true + if create_dokploy_traefik "$TRAEFIK_OLD_IMAGE" no && docker update --restart always dokploy-traefik >/dev/null && docker start dokploy-traefik >/dev/null && traefik_check_running; then + echo "The previous Traefik container is running again." >&2 + else + echo "Error: restoring the previous Traefik container failed as well. Check with: docker ps -a --filter name=dokploy-traefik" >&2 + fi + 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 + + local existed=0 + if docker inspect dokploy-traefik >/dev/null 2>&1; then + existed=1 + fi + TRAEFIK_OLD_IMAGE="$current" + TRAEFIK_EXTRA_NETWORKS="$(traefik_extra_networks)" + TRAEFIK_OLD_STOPPED=0 + trap 'traefik_restore "being interrupted"' HUP INT TERM + docker rm -f dokploy-traefik-previous >/dev/null 2>&1 || true + if [ "$existed" = "1" ]; then + if ! docker stop dokploy-traefik >/dev/null; then + traefik_restore "stopping the running Traefik container" + fi + TRAEFIK_OLD_STOPPED=1 + if ! docker rm dokploy-traefik >/dev/null; then + traefik_restore "removing the stopped Traefik container" + fi + fi + if ! create_dokploy_traefik "$TRAEFIK_IMAGE" no; then + traefik_restore "creating the new Traefik container and connecting its networks" + fi + if ! docker start dokploy-traefik >/dev/null; then + traefik_restore "starting the new Traefik container" + fi + if ! traefik_wait_settled; then + traefik_restore "checking the new Traefik container (running, with no restarts)" + fi + if ! docker update --restart always dokploy-traefik >/dev/null; then + traefik_restore "setting the restart policy of the new Traefik container" + fi + trap - HUP INT TERM + echo "Traefik runs $TRAEFIK_IMAGE" +} + +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" } +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_within "$DOKPLOY_DOCKER_VERIFY_TIMEOUT" docker_daemon_running; then + return 1 + fi + if docker inspect dokploy-traefik >/dev/null 2>&1; then + docker update --restart always dokploy-traefik >/dev/null || true + if ! traefik_running; then + docker start dokploy-traefik >/dev/null || true + fi + fi + wait_within "$DOKPLOY_DOCKER_VERIFY_TIMEOUT" 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 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 + fail_docker_engine_upgrade "Error: installing the pre-downloaded Docker packages failed." + fi + if ! docker_daemon_running; then + start_docker_service || true + fi + if ! wait_within "$DOKPLOY_DOCKER_VERIFY_TIMEOUT" docker_stack_ready "$DOCKER_VERSION"; then + fail_docker_engine_upgrade "Error: the host was not healthy on Docker Engine $DOCKER_VERSION within $DOKPLOY_DOCKER_VERIFY_TIMEOUT seconds." + fi + echo "Docker Engine is $DOCKER_VERSION" +} + +# 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 + + 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 + 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 "Panel host update finished." +} + +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 +1611,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/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"), 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/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 `; }; 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..c4f01d65fee --- /dev/null +++ b/packages/server/src/setup/server-components.ts @@ -0,0 +1,668 @@ +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 { buildTraefikCreateWithImage, 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, + }; + }); + +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_SKIPPED=0 +DOCKER_SKIP_REASON="" +DOCKER_PACKAGE_VERSION="" +DOCKER_ROLLBACK_DIR="" +DOCKER_KEY_WRITTEN=0 +DOCKER_SOURCE_WRITTEN=0 +DOCKER_KEYDIR_CREATED=0 +DOCKER_ROLLBACK_DIR_CREATED=0 + +docker_skip() { + DOCKER_SKIPPED=1 + DOCKER_SKIP_REASON="$1" +} + +docker_installed_version() { + if [ "$(dpkg-query -W -f='\${db:Status-Status}' "$1" 2>/dev/null)" = "installed" ]; then + dpkg-query -W -f='\${Version}' "$1" + 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 + docker_skip "the Docker apt repository in $DOKPLOY_APT_SOURCES_DIR is for $repo_codename, but this host runs \${host_codename:-an unknown release}; fix that source file yourself and run the update again" + return 1 + fi + done + return 0 +} + +docker_pinned_candidate() { + apt-cache madison docker-ce | awk -F'|' '{ gsub(/ /, "", $2); print $2 }' | grep -m1 -E '^([0-9]+:)?${dockerVersionPattern}-' || true +} + +docker_undo_repo() { + if [ "$DOCKER_SOURCE_WRITTEN" = 1 ]; then + $SUDO_CMD rm -f "$DOKPLOY_APT_SOURCES_DIR/docker.list" || true + DOCKER_SOURCE_WRITTEN=0 + fi + if [ "$DOCKER_KEY_WRITTEN" = 1 ]; then + $SUDO_CMD rm -f "$DOKPLOY_DOCKER_KEYRING" || true + DOCKER_KEY_WRITTEN=0 + fi + if [ "$DOCKER_KEYDIR_CREATED" = 1 ]; then + $SUDO_CMD rmdir "$(dirname "$DOKPLOY_DOCKER_KEYRING")" 2>/dev/null || true + DOCKER_KEYDIR_CREATED=0 + fi +} + +docker_remove_rollback_dir() { + if [ "$DOCKER_ROLLBACK_DIR_CREATED" = 1 ]; then + $SUDO_CMD rm -rf "$DOCKER_ROLLBACK_DIR" || true + DOCKER_ROLLBACK_DIR_CREATED=0 + fi +} + +docker_add_apt_repository() { + local id="" codename="" arch="" key="" + if [ -n "$(docker_repo_codenames)" ] || [ -e "$DOKPLOY_APT_SOURCES_DIR/docker.list" ]; then + docker_skip "Docker ${PINNED_VERSIONS.docker} is not offered by the Docker apt repository configured on this host" + return 1 + fi + id="$( (. "$DOKPLOY_OS_RELEASE" && printf '%s' "\${ID:-}") 2>/dev/null || true)" + codename="$( (. "$DOKPLOY_OS_RELEASE" && printf '%s' "\${VERSION_CODENAME:-}") 2>/dev/null || true)" + case "$id" in + ubuntu | debian) ;; + *) + docker_skip "Docker ${PINNED_VERSIONS.docker} is not in the apt sources, and Docker's repository is only added automatically on ubuntu and debian (this host is '$id')" + return 1 + ;; + esac + if [ -z "$codename" ]; then + docker_skip "VERSION_CODENAME is not set in $DOKPLOY_OS_RELEASE, so Docker's repository cannot be added" + return 1 + fi + if [ ! -e "$DOKPLOY_DOCKER_KEYRING" ]; then + if [ ! -d "$(dirname "$DOKPLOY_DOCKER_KEYRING")" ]; then + DOCKER_KEYDIR_CREATED=1 + fi + if ! key="$(mktemp)"; then + docker_undo_repo + docker_skip "a temporary file for Docker's apt key could not be created" + return 1 + fi + if ! download_with_retry curl -fsSL "https://download.docker.com/linux/$id/gpg" -o "$key"; then + rm -f "$key" + docker_undo_repo + docker_skip "Docker's apt key could not be downloaded" + return 1 + fi + DOCKER_KEY_WRITTEN=1 + if ! $SUDO_CMD install -d -m 0755 "$(dirname "$DOKPLOY_DOCKER_KEYRING")" || ! $SUDO_CMD install -m 0644 "$key" "$DOKPLOY_DOCKER_KEYRING"; then + rm -f "$key" + docker_undo_repo + docker_skip "Docker's apt key could not be installed" + return 1 + fi + rm -f "$key" + fi + if ! arch="$(dpkg --print-architecture)"; then + docker_undo_repo + docker_skip "the architecture could not be read for Docker's apt source" + return 1 + fi + DOCKER_SOURCE_WRITTEN=1 + if ! printf 'deb [arch=%s signed-by=%s] https://download.docker.com/linux/%s %s stable\\n' "$arch" "$DOKPLOY_DOCKER_KEYRING" "$id" "$codename" | $SUDO_CMD tee "$DOKPLOY_APT_SOURCES_DIR/docker.list" >/dev/null; then + docker_undo_repo + docker_skip "Docker's apt source could not be written" + return 1 + fi + return 0 +} + +docker_save_rollback_packages() { + local package="" installed="" candidate="" + if [ -z "$(docker_installed_version docker-ce)" ]; then + docker_skip "docker-ce is not installed, so there is no Docker Engine to roll back to" + return 1 + fi + DOCKER_ROLLBACK_DIR="$DOKPLOY_BACKUP_DIR/docker-rollback-$(date -u +%Y%m%dT%H%M%SZ)" + if ! $SUDO_CMD mkdir -p "$DOKPLOY_BACKUP_DIR" || ! $SUDO_CMD chmod 700 "$DOKPLOY_BACKUP_DIR" || ! $SUDO_CMD sh -c 'umask 077 && mkdir "$1"' sh "$DOCKER_ROLLBACK_DIR"; then + docker_skip "the current Docker packages could not be saved for rollback" + return 1 + fi + DOCKER_ROLLBACK_DIR_CREATED=1 + for package in docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin docker-ce-rootless-extras; do + installed="$(docker_installed_version "$package")" + if [ -z "$installed" ]; then + continue + fi + case "$package" in + docker-ce | docker-ce-cli) ;; + *) + candidate="$(docker_candidate_version "$package")" + if [ "$candidate" = "$installed" ]; then + continue + fi + ;; + esac + if ! $SUDO_CMD sh -c 'umask 077 && cd "$1" && apt-get download "$2" >/dev/null' sh "$DOCKER_ROLLBACK_DIR" "$package=$installed"; then + docker_skip "the current Docker packages could not be saved for rollback" + return 1 + fi + done + return 0 +} + +docker_prepare_apt() { + if ! $SUDO_CMD apt-get update -qq; then + abort_before_change "Pre-download failed: the apt package lists could not be refreshed." + fi + DOCKER_PACKAGE_VERSION="$(docker_pinned_candidate)" + if [ -z "$DOCKER_PACKAGE_VERSION" ]; then + if ! docker_add_apt_repository; then + return 1 + fi + if ! $SUDO_CMD apt-get update -qq; then + docker_undo_repo + docker_skip "the apt package lists could not be refreshed after adding Docker's repository" + return 1 + fi + DOCKER_PACKAGE_VERSION="$(docker_pinned_candidate)" + if [ -z "$DOCKER_PACKAGE_VERSION" ]; then + docker_undo_repo + docker_skip "Docker ${PINNED_VERSIONS.docker} is not in the apt sources, even after adding Docker's repository" + return 1 + fi + fi + if ! docker_save_rollback_packages; then + docker_undo_repo + return 1 + fi + if ! download_with_retry $SUDO_CMD apt-get install -y -qq --download-only "docker-ce=$DOCKER_PACKAGE_VERSION" "docker-ce-cli=$DOCKER_PACKAGE_VERSION" containerd.io docker-buildx-plugin docker-compose-plugin; then + docker_undo_repo + docker_remove_rollback_dir + abort_before_change "Pre-download failed: the Docker packages." + fi + return 0 +} + +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 1 + 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 ]; then + $SUDO_CMD docker update --restart always dokploy-traefik >/dev/null 2>&1 || true + fi + if [ "$traefik_existed" = 1 ] && [ "$($SUDO_CMD docker inspect -f '{{.State.Running}}' dokploy-traefik 2>/dev/null)" != "true" ]; then + $SUDO_CMD docker start dokploy-traefik >/dev/null 2>&1 || true + fi + 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_skip "this host has no apt-get, so the Docker packages cannot be pre-downloaded or rolled back" +elif docker_check_repo_codename; then + docker_prepare_apt || true +fi +if [ "$DOCKER_SKIPPED" = 1 ]; then + docker_remove_rollback_dir +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. +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")) { + downloads.push(dockerPredownload()); + } + 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 +} +${components.includes("docker") ? dockerHelpers() : ""} +echo "Downloading update artifacts before anything is changed" +${downloads.join("\n")} +`; +}; + +const updateStepFor = (component: UpdatableComponent) => { + switch (component) { + case "docker": + return ` +if [ "$DOCKER_SKIPPED" = 1 ]; then + echo "Docker not updated: $DOCKER_SKIP_REASON. The other components continue." +else + echo "Updating Docker to ${PINNED_VERSIONS.docker}" + docker_update_from_apt + echo "Docker version ${PINNED_VERSIONS.docker} installed ✅" +fi +`; + case "rclone": + return ` +${rcloneInstallCommand()} +`; + case "traefik": + return ` +echo "Updating Traefik to ${TRAEFIK_VERSION}" +traefik_existed=0 +traefik_old_stopped=0 +traefik_old_image="" +traefik_extra_networks="" +if $SUDO_CMD docker inspect dokploy-traefik >/dev/null 2>&1; then + traefik_existed=1 + traefik_old_image="$($SUDO_CMD docker inspect -f '{{.Config.Image}}' dokploy-traefik 2>/dev/null || true)" + traefik_extra_networks="$($SUDO_CMD docker inspect -f '{{range $k,$v := .NetworkSettings.Networks}}{{$k}} {{end}}' dokploy-traefik 2>/dev/null || true)" +fi +traefik_create() { +${buildTraefikCreateWithImage('"$1"')} +} +traefik_connect() { + $SUDO_CMD docker network connect dokploy-network dokploy-traefik || return 1 + local traefik_network="" + for traefik_network in $1; do + case "$traefik_network" in + bridge | dokploy-network) continue ;; + esac + if $SUDO_CMD docker network inspect "$traefik_network" >/dev/null 2>&1; then + $SUDO_CMD docker network connect "$traefik_network" dokploy-traefik >/dev/null || return 1 + fi + done +} +traefik_check_running() { + [ "$($SUDO_CMD docker inspect -f '{{.State.Running}} {{.RestartCount}}' dokploy-traefik 2>/dev/null)" = "true 0" ] +} +traefik_wait_settled() { + local second=0 + while [ "$second" -lt "\${TRAEFIK_SETTLE_SECONDS:-10}" ]; do + sleep 1 + if ! traefik_check_running; then + return 1 + fi + second=$((second + 1)) + done + traefik_check_running +} +traefik_restore() { + local failed_step="$1" + trap - HUP INT TERM + echo "Error: Traefik update failed while $failed_step." >&2 + if [ "$traefik_old_stopped" = 1 ]; then + echo "Restoring the previous Traefik container from $traefik_old_image." >&2 + $SUDO_CMD docker rm -f dokploy-traefik >/dev/null 2>&1 || true + if traefik_create "$traefik_old_image" && traefik_connect "$traefik_extra_networks" && $SUDO_CMD docker update --restart always dokploy-traefik >/dev/null && $SUDO_CMD docker start dokploy-traefik >/dev/null && traefik_check_running; then + echo "The previous Traefik container is running again." >&2 + else + echo "Error: restoring the previous Traefik container failed as well. Check with: docker ps -a --filter name=dokploy-traefik" >&2 + fi + fi + exit 1 +} +trap 'traefik_restore "being interrupted"' HUP INT TERM +$SUDO_CMD docker rm -f dokploy-traefik-previous >/dev/null 2>&1 || true +if [ "$traefik_existed" = 1 ]; then + if ! $SUDO_CMD docker stop dokploy-traefik >/dev/null; then + traefik_restore "stopping the running Traefik container" + fi + traefik_old_stopped=1 + if ! $SUDO_CMD docker rm dokploy-traefik >/dev/null; then + traefik_restore "removing the stopped Traefik container" + fi +fi +if ! traefik_create "traefik:v${TRAEFIK_VERSION}"; then + traefik_restore "creating the new Traefik container" +fi +if ! traefik_connect "$traefik_extra_networks"; then + traefik_restore "connecting the new Traefik container to its networks" +fi +if ! $SUDO_CMD docker start dokploy-traefik >/dev/null; then + traefik_restore "starting the new Traefik container" +fi +if ! traefik_wait_settled; then + traefik_restore "checking the new Traefik container (running, with no restarts)" +fi +if ! $SUDO_CMD docker update --restart always dokploy-traefik >/dev/null; then + traefik_restore "setting the restart policy of the new Traefik container" +fi +trap - HUP INT TERM +echo "Traefik version ${TRAEFIK_VERSION} installed ✅" +`; + case "nixpacks": + return ` +echo "Updating Nixpacks to ${PINNED_VERSIONS.nixpacks}" +nixpacks_installer="$(mktemp)" +if ! curl -fsSL https://nixpacks.com/install.sh -o "$nixpacks_installer" || [ ! -s "$nixpacks_installer" ] || ! bash -n "$nixpacks_installer"; then + rm -f "$nixpacks_installer" + echo "Error: the Nixpacks install script could not be downloaded or checked; Nixpacks was not changed." >&2 + exit 1 +fi +if ! $SUDO_CMD env NIXPACKS_VERSION=${PINNED_VERSIONS.nixpacks} bash "$nixpacks_installer"; then + rm -f "$nixpacks_installer" + echo "Error: the Nixpacks install script failed." >&2 + exit 1 +fi +rm -f "$nixpacks_installer" +echo "Nixpacks version ${PINNED_VERSIONS.nixpacks} installed ✅" +`; + case "railpack": + return ` +echo "Updating Railpack to ${PINNED_VERSIONS.railpack}" +railpack_installer="$(mktemp)" +if ! curl -fsSL https://railpack.com/install.sh -o "$railpack_installer" || [ ! -s "$railpack_installer" ] || ! bash -n "$railpack_installer"; then + rm -f "$railpack_installer" + echo "Error: the Railpack install script could not be downloaded or checked; Railpack was not changed." >&2 + exit 1 +fi +if ! $SUDO_CMD env RAILPACK_VERSION=${PINNED_VERSIONS.railpack} bash "$railpack_installer"; then + rm -f "$railpack_installer" + echo "Error: the Railpack install script failed." >&2 + exit 1 +fi +rm -f "$railpack_installer" +echo "Railpack version ${PINNED_VERSIONS.railpack} installed ✅" +`; + case "buildpacks": + 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 +${predownloadGate(components)} +${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, + upgradeDocker = false, +) => { + const plan = filterDockerUpgrade(components, upgradeDocker); + if (plan.skippedDocker) { + onData?.(`${DOCKER_UPGRADE_SKIPPED_MESSAGE}\n`); + } + const { exitCode } = await runSshCommand( + serverId, + buildComponentUpdateScript(plan.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..32b8c8647a2 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 `; @@ -741,12 +832,47 @@ export const createTraefikInstance = () => { return command; }; +// A failed download must fail the setup. Piping curl into bash, or running it inside +// $(...), runs nothing when the download fails and still reports success. +const installScriptRun = ({ + label, + url, + name, + variable, + version, +}: { + label: string; + url: string; + name: string; + variable: string; + version: string; +}) => ` + ${name}_installer="$(mktemp)" + if ! curl -fsSL ${url} -o "$${name}_installer" || [ ! -s "$${name}_installer" ] || ! bash -n "$${name}_installer"; then + rm -f "$${name}_installer" + echo "Error: the ${label} install script could not be downloaded or checked; ${label} was not installed." >&2 + exit 1 + fi + if ! $SUDO_CMD env ${variable}=${version} bash "$${name}_installer"; then + rm -f "$${name}_installer" + echo "Error: the ${label} install script failed." >&2 + exit 1 + fi + rm -f "$${name}_installer" +`; + const installNixpacks = () => ` if command_exists nixpacks; then echo "Nixpacks already installed ✅" else - export NIXPACKS_VERSION=1.41.0 - $SUDO_CMD bash -c "$(curl -fsSL https://nixpacks.com/install.sh)" + export NIXPACKS_VERSION=${NIXPACKS_VERSION} +${installScriptRun({ + label: "Nixpacks", + url: "https://nixpacks.com/install.sh", + name: "nixpacks", + variable: "NIXPACKS_VERSION", + version: NIXPACKS_VERSION, +})} echo "Nixpacks version $NIXPACKS_VERSION installed ✅" fi `; @@ -755,8 +881,14 @@ 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} +${installScriptRun({ + label: "Railpack", + url: "https://railpack.com/install.sh", + name: "railpack", + variable: "RAILPACK_VERSION", + version: RAILPACK_VERSION, +})} echo "Railpack version $RAILPACK_VERSION installed ✅" fi `; @@ -786,8 +918,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..4bfe45c08a6 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,35 @@ 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; + +const traefikContainerOptions = ( + image: string, + restart: string, +) => `--name dokploy-traefik \ + --restart ${restart} \ + -v /etc/dokploy/traefik/traefik.yml:/etc/traefik/traefik.yml \ + -v /etc/dokploy/traefik/dynamic:/etc/dokploy/traefik/dynamic \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -p ${TRAEFIK_SSL_PORT}:${TRAEFIK_SSL_PORT} \ + -p ${TRAEFIK_PORT}:${TRAEFIK_PORT} \ + -p ${TRAEFIK_HTTP3_PORT}:${TRAEFIK_HTTP3_PORT}/udp \ + ${image}`; + +export const buildTraefikRunCommand = (version: string) => ` + $SUDO_CMD docker run -d ${traefikContainerOptions(`traefik:v${version}`, "always")} + $SUDO_CMD docker network connect dokploy-network dokploy-traefik +`; + +// Created with no restart policy and not started: networks are attached before the +// ports are taken, and the policy is raised only after the new container runs. +export const buildTraefikCreateWithImage = (image: string) => ` + $SUDO_CMD docker create ${traefikContainerOptions(image, "no")} +`; + +export const buildTraefikCreateCommand = (version: string) => + buildTraefikCreateWithImage(`traefik:v${version}`); export interface TraefikOptions { env?: string[]; diff --git a/update.sh b/update.sh index fa3e76d0d8d..610f7d98da1 100755 --- a/update.sh +++ b/update.sh @@ -7,6 +7,9 @@ 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:-}" +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 @@ -32,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 } @@ -206,6 +209,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) + DOKPLOY_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 Dokploy panel 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: the Dokploy panel was not healthy after ${DOKPLOY_HEALTH_TIMEOUT} seconds (swarm state: ${state:-unknown})." >&2 + exit 1 + fi + sleep "$DOKPLOY_HEALTH_INTERVAL" + done +} + update_agenthits_dokploy() { require_root_linux_host install_docker_if_missing @@ -280,7 +321,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" }