Skip to content

Commit 003cc1b

Browse files
authored
feat: installer hardening flags and one source for pinned versions (#49)
feat: installer hardening flags and one source for pinned versions Installer hardening (opt-in, Linux only): - install-agenthits.sh accepts --harden, --ufw, --ssh-keys-only and --fail2ban, plus a harden mode for a host that is already installed. The same switches are read as HARDEN_SSH, HARDEN_UFW and HARDEN_FAIL2BAN, so the macOS (OrbStack machine) and Windows (WSL) launchers pass them on. - SSH keys-only writes the main sshd_config, because the Security tab reads that file and not drop-ins. It refuses to run without a valid root key and skips hosts that have no sshd. - UFW denies incoming traffic except SSH, 80, 443 (tcp, and udp for HTTP/3) and keeps forwarding enabled for Docker. Fail2Ban runs the sshd jail in aggressive mode from /etc/fail2ban/jail.local. UFW and Fail2Ban are Debian/Ubuntu only and are skipped elsewhere. Panel-driven server setup: - DOKPLOY_SERVER_HARDENING (ssh, ufw, fail2ban or all) applies the same hardening as the last step of Setup Server. It is off by default. The installer sets it from its flags. One source for component versions: - packages/server/src/setup/component-versions.ts (PINNED_VERSIONS) pins Node 24.21.0, Docker 29.8.2, Traefik 3.7.14, Postgres 18.6, Redis 8.10.2, Nixpacks 1.41.0, Railpack 0.40.1, pack 0.40.9 and RClone 1.75.1. - The installer defaults, remote server setup, both Dockerfiles, CI, and the application Railpack picker read these values. component-versions-sync fails when a file drifts from the object. - Remote setup installs the pinned RClone release archive. The Ubuntu 26.04 Docker repin to 29.4.2 is gone, because 29.8.2 is published for noble and resolute on amd64 and arm64. - Setup Server still installs only what is missing. A Components button on each server compares installed and pinned versions and updates Docker, Traefik, RClone, Nixpacks, Railpack and pack. Docker updates restart the daemon and Traefik is recreated; the dialog warns about both. - The panel installer installs Nixpacks, Railpack, pack and RClone on the host at the pinned versions. Fix: - Nixpacks and Railpack installed through sudo ignored the pinned version, because sudo resets the environment. The version is passed with env now. Docs: docs/server-hardening.md (flags, rollback, removing a server) and docs/server-components-update.md (pins, update behaviour, limits). Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com> --- docs: describe the panel machine generically in the hardening runbook --- feat(db): default Railpack version for new applications to 0.40.1 New applications now store Railpack 0.40.1 as their default version, matching the pinned version the installers and the remote setup use. Migration 0207 only changes the column default: existing applications keep the version they have stored. Checked on a throwaway Postgres 18.6 instance: a row inserted before the migration keeps 0.15.4, and a row inserted after it without the column gets 0.40.1. drizzle-kit reports no further schema changes. Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com> --- docs: update the hardening runbook to the current Traefik pin The runbook still named Traefik 3.7.5 and a test file that was replaced by component-versions-sync.test.ts. Its update example also set TRAEFIK_VERSION=3.7.5 on the panel, which would make new servers install the old Traefik instead of the pinned 3.7.14. The example now only adds DOKPLOY_SERVER_HARDENING; the installer sets TRAEFIK_VERSION from TRAEFIK_IMAGE on its own. Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com> --- test: give the subprocess settings tests a 30s timeout Two tests in the settings suite spawn bash or sh against fake docker and curl scripts. They pass locally in about 3s, but on the CI runner, which also runs real Docker builds in parallel, they exceeded vitest's 5s default (pr-check run 37777323548). Raise the timeout only for the tests that spawn processes. Most of the diff is biome reindenting the it() calls whose timeout argument moved to its own line. Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com> --- fix(update): roll back a failed panel self-update The panel self-update passed no failure action, so Swarm's default (pause) applied: when the new task failed, the panel stayed down with no return to the previous image. Both self-update commands now pass --update-failure-action rollback, and state --update-order stop-first explicitly. Stop-first is deliberate. The panel runs drizzle migrations when its container starts (apps/dokploy/migration.ts, run before server.ts), so an overlapping old and new task would migrate and serve the same schema at once. The image is pulled before the update, so the gap is only container start time. Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com> --- feat(update): pre-download components before the update changes anything The Components update removed the running Traefik container before the new image was local, and the Docker step restarted the daemon before anything else was fetched. Now one gate runs before every step: it pulls the pinned Traefik image, fetches the get.docker.com script and, on apt hosts, downloads the pinned Docker packages. Any failure aborts with "Nothing was changed" while every running service is still untouched. Traefik is swapped without removing the running container first. The old container is stopped and renamed, the new one is started and connected to the network, and the old one is removed only after the new one has been running for TRAEFIK_SETTLE_SECONDS (10 by default). If the new container fails to start or is not running, the old one is renamed back, started, and the script exits non-zero. ERR and HUP/INT/TERM traps cover a failed run or network connect and an SSH session that drops mid-swap. Docker keeps its daemon restart. The Docker documentation scopes live-restore to standalone containers and states that it does not apply to Swarm services, which are the workload on these hosts, so daemon.json is not changed. The "recreates the Traefik container" test asserted the remove-first step and now asserts the rename. New tests run the generated script against fake docker, apt-get, apt-cache, curl and sudo commands to check the order and the restore paths. Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com> --- feat(update): read the panel update outcome from swarm update.sh ran an attached docker service update. Attached mode has no timeout, and it reports a completed rollback as success, so a panel update that rolled back looked like a success. The update is now submitted with --detach, and the script waits until the update is registered (the service Version.Index has moved) before it reads UpdateStatus. It fails on a paused or rolled-back state, and when the new task is not healthy within AGENTHITS_HEALTH_TIMEOUT (240 seconds by default). The update-script tests fake the service index, the update state and the container health. A new case covers a rollback. Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com> --- feat(update): update the panel host stack in dependency order install-agenthits.sh update replaced only the panel image. It now runs the whole host stack, after one pre-download gate that pulls every pinned image (Postgres, Redis, Traefik, the panel) and fetches update.sh before anything is changed. Then, in order: 1. Postgres backup: pg_dumpall to AGENTHITS_BACKUP_DIR, kept on the host. It runs before any swap, so a failed backup changes nothing. 2. Redis: stop-first swarm update with rollback on failure; waits for PONG. 3. Traefik: the running container is stopped and renamed, the new one is started and joined to dokploy-network, and it must still be running after AGENTHITS_TRAEFIK_SETTLE seconds. If it is not, the old container is put back. The old container stays stopped as dokploy-traefik-previous with restart=no, so a daemon restart cannot start it next to the new one. 4. Postgres: stop-first swarm update, which the shared data volume requires; waits for pg_isready. 5. Panel: update.sh, which waits for health and rolls back on failure. 6. Docker Engine, only with DOCKER_ENGINE_UPGRADE=1: the packages are downloaded in the gate and installed with --no-download after the rest is healthy. The daemon restart is the one disruption that remains. Order: Redis and Traefik fail before the database changes. Postgres follows its backup and comes before the panel, because the new panel migrates the database when it starts. The panel is last, so its dependencies are on their pinned versions and healthy before its update runs. The fresh install now calls run_dokploy_traefik for its Traefik container with the same flags, so both paths share one definition. install-agenthits-update.test.ts runs the installer in update mode against fake docker, apt, curl, id and uname. It checks the order, that a failed pre-download or backup changes nothing, that a rolled-back Redis stops the update, and that a Traefik that does not start is restored. Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com> --- docs: describe the update order, rollback and backups agenthits-install.md now lists the operator update steps in order, the pre-download gate, the Postgres backup location, the Traefik keep-until- verified swap, the optional Docker Engine upgrade and the manual rollback commands. server-components-update.md states the pre-download gate, the Traefik restore behaviour, why live-restore is not used for swarm services, and that pre-downloading Docker packages needs apt. server-hardening.md notes that the update path leaves SSH, UFW and Fail2Ban unchanged. Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com> --- refactor(update): name the new operator settings after the panel The settings added for the operator update (backup directory, health timeout and interval, Traefik settle time) now use the DOKPLOY_ prefix the installer already uses for its own settings. Their messages no longer carry the fork name, and the default backup directory is /var/backups/dokploy. Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com> --- fix(update): keep the Postgres backup readable by root only pg_dumpall writes the data and the role password hashes. The backup was created with the default umask, so the file could be world-readable. The backup directory is now created 0700 and the dump runs under umask 077, so the file is 0600. The installer test checks the mode of the file it writes. Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com> --- fix(update): refuse a Postgres major version change before anything runs The Postgres swap keeps the existing volume mount, so a new major version does not read the old data directory. The PostgreSQL 18 image would start a new, empty cluster while the panel reconnects to it. The update now compares the running and pinned major versions before the pre-download gate and stops with "Nothing was changed" when they differ. The major version check reuses get_postgres_major_version, which now takes an optional image argument. Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com> --- fix(update): set the backup directory mode with an explicit chmod mkdir -p -m applies the mode only to the deepest directory, which shellcheck flags as SC2174. The directory is created with mkdir -p and then chmod 700, which is the same result without the ambiguity. Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com> --- fix(update): pin Docker in update.sh to the shared version update.sh installed Docker 28.5.0 on a host without Docker, while the installer and the component pins use 29.8.2. Read the version from DOCKER_VERSION with the same default as the installer, and extend the version sync test to cover update.sh. Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com> --- feat(update): roll back the panel host Docker engine upgrade The Docker Engine upgrade stays off unless DOCKER_ENGINE_UPGRADE=1 is set in the environment of the update command; the macOS and WSL launchers now pass it on. Before the Engine is touched, the installed packages the upgrade replaces are saved under DOKPLOY_BACKUP_DIR/docker-rollback-<UTC>/ (directory 0700, files 0600). After the upgrade the stack must report the pinned version with an active swarm and healthy panel, Postgres, Redis and Traefik; if not, the saved packages are installed back and the stack is checked again. When the rollback fails too, the commands to finish it by hand are printed. The Docker apt source is compared with VERSION_CODENAME before anything changes, and a mismatch stops the update without rewriting the source. Traefik is recreated with docker create, connected to dokploy-network and to every other network of the old container (bridge excluded), and only then started. /etc/dokploy is 755 instead of 777. Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com> --- feat(servers): install the pinned Docker from apt with rollback The component update installs the pinned Docker from the packages downloaded before any step runs, with apt-get install --no-download, instead of running get.docker.com a second time. get.docker.com is still fetched only on hosts without apt-get, where nothing can be pre-downloaded or rolled back. Before anything changes, the Docker apt source is compared with VERSION_CODENAME. When it names another release only the Docker step is skipped, with a message, and the other components still run. When no Docker source exists and apt does not offer the pinned version, Docker's repository is added for ubuntu and debian; an existing source is never rewritten. The packages that the upgrade replaces are saved first, as in the panel host installer. If the daemon does not report the pinned version, or Traefik is not running afterwards, the saved packages are installed back, Traefik is started and the step fails. Traefik is created with docker create, connected to dokploy-network and to every other network of the old container (read on the host; bridge excluded), and only then started. The old container is stopped just before the new one starts, so the time without a running Traefik stays short. The Docker step is opt-in: filterDockerUpgrade removes docker from the list unless upgradeDocker is set, and logs one line saying so. Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com> --- feat(servers): make the Docker Engine upgrade opt-in in the Components dialog The component update takes upgradeDocker (default false). The Components dialog shows an unchecked "Upgrade Docker Engine (restarts all containers on this server)" checkbox whenever Docker is among the outdated components, and sends its value with the update. Without it, Docker is skipped and the update says so in its log. Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com> --- docs: document Docker engine upgrade and rollback Explains that the panel host upgrade of the Docker Engine needs DOCKER_ENGINE_UPGRADE=1 in the environment of the update command, what is saved for rollback and where, the checks after the upgrade, and the commands to roll back by hand. Describes the new order in which Traefik is replaced. Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com> --- fix(update): set the Traefik restart policy before the swap stops the old container Before the replaced Traefik container is stopped, its restart policy is set to no, so a daemon restart cannot start it next to the new container. When the old container is put back, its restart policy is set to always before it is started. The panel host installer and the remote server update follow the same order. Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com> --- fix(servers): skip only the Docker step on Docker-specific pre-download failures A Docker-specific failure skips the Docker step with one message and lets the other components run: a Docker apt source for another release, no pinned candidate after the repository check, an unsupported distribution, a failure to save the rollback packages (including hosts without docker-ce), and a host without apt-get. Every other pre-download failure, such as the Traefik image, the apt lists or the Docker packages, still aborts the whole update before any change. get.docker.com is no longer run; a host without apt-get skips Docker instead. Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com> --- fix(update): pass the same settings through the WSL launcher as through macOS The WSL launcher forwarded fewer settings than the macOS launcher, so DOKPLOY_BACKUP_DIR, DOKPLOY_HEALTH_TIMEOUT, DOKPLOY_HEALTH_INTERVAL, DOKPLOY_TRAEFIK_SETTLE and AGENTHITS_PULL_RETRY_DELAY set on Windows never reached the installer. They are added to $PassthroughVars. The macOS list already had every setting. A test keeps the two lists equal. Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com> --- fix(servers): leave the host as it was when a later Docker step fails When a run adds a Docker apt repository and a later Docker step fails, the key and source file that this run wrote are removed. The steps covered are the key download, the candidate check after the update, the download of the rollback packages, and the download of the new packages. The first three skip only Docker, and the other components still run. A failed download of the new packages still aborts the update, now without the repository left behind. A key or source file that existed before the run is never removed, and an existing key is no longer overwritten. Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com> --- fix(update): remove the saved-packages directory when Docker is skipped or the update aborts The timestamped docker-rollback directory is created before the new packages are downloaded. When Docker is skipped, or the update aborts before any change, that directory is removed, but only if this run created it: it is made with a plain mkdir, so an existing directory is never adopted or removed. The apt package lists are left as they are, since they are a cache. Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com> --- fix(update): replace the Traefik swap that renamed the running container Renaming the running Traefik fails on swarm hosts where it is attached to several networks ("cannot create entry in table endpoint_table ... already exists"). The swap no longer renames anything, on the remote server path or in the panel host installer. The old container is stopped and removed with docker rm (no force). The new one is created with --restart no, connected to dokploy-network and to the captured networks (bridge excluded), and started. After the settle time it must report running with no restarts, and only then is its restart policy raised to always. A failure after the old container was stopped removes whatever carries the name, recreates the old image with the same flags and networks, starts it and checks it, then exits 1 naming the step that failed. A leftover dokploy-traefik-previous from earlier runs is removed first. Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com> --- fix(servers): fail the Nixpacks and Railpack updates when their install script does not download The steps ran bash -c "$(curl ...)". When the download failed, the command substitution came out empty, bash ran nothing, and the update reported success. Each install script is now downloaded to a file with curl -fsSL, checked for content and syntax, and only then run; any failure exits non-zero. These steps do not touch running services, so they need no pre-download. Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com> --- docs: describe the Traefik replacement without renaming The install guide still described the swap as renaming the running Traefik container and gave a manual rollback that used docker rename. It now describes stop, remove, create with --restart no, connect, start, settle check, and then --restart always, and gives a manual rollback that recreates the container from the previous image. Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com> --- fix(servers): fail the server setup when the Nixpacks or Railpack install script does not download The setup ran bash -c "$(curl -fsSL ...)". When the download failed, the command substitution came out empty, bash ran nothing, and the setup went on to report Nixpacks or Railpack as installed. Each install script is now downloaded to a file with curl -fsSL, checked for content and syntax, and only then run with the pinned version in its environment; any failure stops the setup with a non-zero exit. Tests check the generated setup script, and run the Nixpacks, Railpack, RClone and Buildpacks steps with a failing download and a working one. Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com> --- fix(update): fail the installer when the Nixpacks or Railpack install script does not download install_toolchain ran bash -c "$(curl -fsSL ...)" for Nixpacks and Railpack, so a failed download ran nothing and the install went on. Both scripts now go through one helper that downloads to a file, checks it, runs it with the pinned version in its environment, and exits non-zero on any failure. Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com> --- test(servers): check that failed Buildpacks and RClone downloads fail the update The Buildpacks step pipes curl into tar and the RClone step downloads with curl -fsSL before unzip. Both already exit non-zero when the download fails, because tar rejects empty and HTML input and set -e stops the script. These run tests keep it that way. Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com> --- fix(update): reset Traefik's restart policy before the Docker rollback starts it A Docker rollback starts dokploy-traefik again. If the Traefik swap had left its restart policy at "no", a daemon restart during the rollback would leave Traefik down. The rollback now sets --restart always before it starts Traefik, on the remote-server path and in the panel host installer. Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com> --- fix(update): verify Docker within its own window, 30 s on remote servers and 120 s on the panel host A failed Docker upgrade on a remote server waited 60 seconds before it rolled back. The wait is now 30 one-second checks, and the rollback starts as soon as they expire. On the panel host, the Docker checks use DOKPLOY_DOCKER_VERIFY_TIMEOUT (120 seconds by default) instead of DOKPLOY_HEALTH_TIMEOUT, so the health timeout of the panel update does not change how long Docker is given. The new variable passes through the macOS and WSL launchers, and the install guide describes both windows. Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com> --- fix(update): poll the new Traefik during the settle time and stop at the first failed check The swap waited the full settle time before checking the new Traefik container, so a container that exited early still kept the ports dark until the wait ended. Both paths now check it once a second over the settle time, with State.Running and RestartCount, and roll back as soon as it is not running. The final check after the settle time stays, and the restart policy is raised only after it. Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com> ---
1 parent d1e3bcd commit 003cc1b

36 files changed

Lines changed: 16637 additions & 231 deletions

‎.github/workflows/dokploy.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ jobs:
207207

208208
- uses: actions/setup-node@v7
209209
with:
210-
node-version: 24.4.0
210+
node-version: 24.21.0
211211
cache: pnpm
212212

213213
- name: Generate OpenAPI specification

‎.github/workflows/pull-request.yml‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ jobs:
3030
- uses: actions/setup-node@v7
3131
if: ${{ !cancelled() }}
3232
with:
33-
node-version: 24.4.0
33+
node-version: 24.21.0
3434
- run: bun install --frozen-lockfile
3535
if: ${{ !cancelled() }}
3636
- run: bun run server:build
@@ -54,7 +54,7 @@ jobs:
5454
bun-version: 1.4.2
5555
- uses: actions/setup-node@v7
5656
with:
57-
node-version: 24.4.0
57+
node-version: 24.21.0
5858

5959
- name: Install Nixpacks
6060
if: matrix.job == 'test' && runner.environment == 'github-hosted'
@@ -66,7 +66,7 @@ jobs:
6666
- name: Install Railpack
6767
if: matrix.job == 'test' && runner.environment == 'github-hosted'
6868
run: |
69-
export RAILPACK_VERSION=0.15.4
69+
export RAILPACK_VERSION=0.40.1
7070
curl -sSL https://railpack.com/install.sh | bash
7171
echo "Railpack installed $RAILPACK_VERSION"
7272

‎.github/workflows/sync-openapi-docs.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ jobs:
2929
bun-version: 1.4.2
3030
- uses: actions/setup-node@v7
3131
with:
32-
node-version: 24.4.0
32+
node-version: 24.21.0
3333

3434
- name: Install dependencies
3535
run: bun install --frozen-lockfile

‎Dockerfile‎

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# syntax=docker/dockerfile:1
2-
FROM node:24.4.0-slim AS base
2+
FROM node:24.21.0-slim AS base
33
ENV BUN_INSTALL="/root/.bun"
44
ENV PATH="$BUN_INSTALL/bin:$PATH"
55
RUN apt-get update \
@@ -79,7 +79,17 @@ COPY --from=build /usr/src/app/packages/server/dist ./packages/server/dist
7979

8080

8181
# Install docker
82-
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
82+
ARG DOCKER_VERSION=29.8.2
83+
RUN curl -fsSL https://get.docker.com -o get-docker.sh && sh get-docker.sh --version "$DOCKER_VERSION" && rm get-docker.sh
84+
85+
# Install rclone
86+
ARG RCLONE_VERSION=1.75.1
87+
RUN apt-get update \
88+
&& apt-get install -y --no-install-recommends unzip \
89+
&& curl -fsSL "https://downloads.rclone.org/v${RCLONE_VERSION}/rclone-v${RCLONE_VERSION}-linux-amd64.zip" -o /tmp/rclone.zip \
90+
&& unzip -q /tmp/rclone.zip -d /tmp \
91+
&& install -m 0755 "/tmp/rclone-v${RCLONE_VERSION}-linux-amd64/rclone" /usr/bin/rclone \
92+
&& rm -rf /tmp/rclone.zip "/tmp/rclone-v${RCLONE_VERSION}-linux-amd64" /var/lib/apt/lists/*
8393

8494
# Install Nixpacks and tsx
8595
# | VERBOSE=1 VERSION=1.21.0 bash
@@ -92,12 +102,12 @@ RUN curl -sSL https://nixpacks.com/install.sh -o install.sh \
92102
&& bun install -g tsx
93103

94104
# Install Railpack
95-
ARG RAILPACK_VERSION=0.15.4
105+
ARG RAILPACK_VERSION=0.40.1
96106
RUN curl -sSL https://railpack.com/install.sh \
97107
| RAILPACK_VERSION="$RAILPACK_VERSION" bash -s -- --yes --bin-dir /usr/local/bin
98108

99109
# Install buildpacks
100-
COPY --from=buildpacksio/pack:0.39.1 /usr/local/bin/pack /usr/local/bin/pack
110+
COPY --from=buildpacksio/pack:0.40.9 /usr/local/bin/pack /usr/local/bin/pack
101111

102112
EXPOSE 3000
103113

‎Dockerfile.cloud‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# syntax=docker/dockerfile:1
2-
FROM node:24.4.0-slim AS base
2+
FROM node:24.21.0-slim AS base
33
ENV BUN_INSTALL="/root/.bun"
44
ENV PATH="$BUN_INSTALL/bin:$PATH"
55
RUN apt-get update \

‎Dockerfile.schedule‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# syntax=docker/dockerfile:1
2-
FROM node:24.4.0-slim AS base
2+
FROM node:24.21.0-slim AS base
33
ENV BUN_INSTALL="/root/.bun"
44
ENV PATH="$BUN_INSTALL/bin:$PATH"
55
RUN apt-get update \

‎Dockerfile.server‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# syntax=docker/dockerfile:1
2-
FROM node:24.4.0-slim AS base
2+
FROM node:24.21.0-slim AS base
33
ENV BUN_INSTALL="/root/.bun"
44
ENV PATH="$BUN_INSTALL/bin:$PATH"
55
RUN apt-get update \
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
import { readFileSync } from "node:fs";
2+
import path from "node:path";
3+
import { PINNED_VERSIONS } from "@dokploy/server/setup/component-versions";
4+
import { TRAEFIK_VERSION } from "@dokploy/server/setup/traefik-setup";
5+
import { describe, expect, it } from "vitest";
6+
7+
const repoRoot = path.resolve(process.cwd(), "../..");
8+
const read = (file: string) => readFileSync(path.join(repoRoot, file), "utf8");
9+
10+
const pinIn = (text: string, pattern: RegExp) => {
11+
const match = text.match(pattern);
12+
if (!match?.[1]) {
13+
throw new Error(`pin not found: ${pattern}`);
14+
}
15+
return match[1];
16+
};
17+
18+
describe("install-agenthits.sh defaults", () => {
19+
const installer = read("install-agenthits.sh");
20+
21+
it("pins Docker", () => {
22+
expect(
23+
pinIn(installer, /DOCKER_VERSION="\$\{DOCKER_VERSION:-([^}]+)\}"/),
24+
).toBe(PINNED_VERSIONS.docker);
25+
});
26+
it("pins Traefik", () => {
27+
expect(
28+
pinIn(installer, /TRAEFIK_IMAGE="\$\{TRAEFIK_IMAGE:-traefik:v([^}]+)\}"/),
29+
).toBe(PINNED_VERSIONS.traefik);
30+
});
31+
it("pins Postgres", () => {
32+
expect(
33+
pinIn(
34+
installer,
35+
/POSTGRES_IMAGE="\$\{POSTGRES_IMAGE:-postgres:([^}]+)\}"/,
36+
),
37+
).toBe(PINNED_VERSIONS.postgres);
38+
});
39+
it("pins Redis", () => {
40+
expect(
41+
pinIn(installer, /REDIS_IMAGE="\$\{REDIS_IMAGE:-redis:([^}]+)\}"/),
42+
).toBe(PINNED_VERSIONS.redis);
43+
});
44+
it("pins Nixpacks", () => {
45+
expect(
46+
pinIn(installer, /NIXPACKS_VERSION="\$\{NIXPACKS_VERSION:-([^}]+)\}"/),
47+
).toBe(PINNED_VERSIONS.nixpacks);
48+
});
49+
it("pins Railpack", () => {
50+
expect(
51+
pinIn(installer, /RAILPACK_VERSION="\$\{RAILPACK_VERSION:-([^}]+)\}"/),
52+
).toBe(PINNED_VERSIONS.railpack);
53+
});
54+
it("pins Buildpacks", () => {
55+
expect(
56+
pinIn(
57+
installer,
58+
/BUILDPACKS_VERSION="\$\{BUILDPACKS_VERSION:-([^}]+)\}"/,
59+
),
60+
).toBe(PINNED_VERSIONS.buildpacks);
61+
});
62+
it("pins RClone", () => {
63+
expect(
64+
pinIn(installer, /RCLONE_VERSION="\$\{RCLONE_VERSION:-([^}]+)\}"/),
65+
).toBe(PINNED_VERSIONS.rclone);
66+
});
67+
});
68+
69+
describe("update.sh defaults", () => {
70+
const updateScript = read("update.sh");
71+
72+
it("pins Docker", () => {
73+
expect(
74+
pinIn(updateScript, /DOCKER_VERSION="\$\{DOCKER_VERSION:-([^}]+)\}"/),
75+
).toBe(PINNED_VERSIONS.docker);
76+
});
77+
});
78+
79+
describe("Dockerfile", () => {
80+
const dockerfile = read("Dockerfile");
81+
82+
it("pins Docker", () => {
83+
expect(pinIn(dockerfile, /ARG DOCKER_VERSION=(\S+)/)).toBe(
84+
PINNED_VERSIONS.docker,
85+
);
86+
});
87+
it("pins RClone", () => {
88+
expect(pinIn(dockerfile, /ARG RCLONE_VERSION=(\S+)/)).toBe(
89+
PINNED_VERSIONS.rclone,
90+
);
91+
});
92+
it("pins Nixpacks", () => {
93+
expect(pinIn(dockerfile, /ARG NIXPACKS_VERSION=(\S+)/)).toBe(
94+
PINNED_VERSIONS.nixpacks,
95+
);
96+
});
97+
it("pins Railpack", () => {
98+
expect(pinIn(dockerfile, /ARG RAILPACK_VERSION=(\S+)/)).toBe(
99+
PINNED_VERSIONS.railpack,
100+
);
101+
});
102+
it("pins pack", () => {
103+
expect(pinIn(dockerfile, /buildpacksio\/pack:(\S+) /)).toBe(
104+
PINNED_VERSIONS.buildpacks,
105+
);
106+
});
107+
});
108+
109+
describe("CI workflow", () => {
110+
const ci = read(".github/workflows/pull-request.yml");
111+
112+
it("pins Nixpacks", () => {
113+
expect(pinIn(ci, /export NIXPACKS_VERSION=(\S+)/)).toBe(
114+
PINNED_VERSIONS.nixpacks,
115+
);
116+
});
117+
it("pins Railpack", () => {
118+
expect(pinIn(ci, /export RAILPACK_VERSION=(\S+)/)).toBe(
119+
PINNED_VERSIONS.railpack,
120+
);
121+
});
122+
});
123+
124+
describe("application Railpack picker", () => {
125+
const picker = read(
126+
"apps/dokploy/components/dashboard/application/build/show.tsx",
127+
);
128+
129+
it("defaults new applications to the pinned Railpack", () => {
130+
expect(
131+
pinIn(
132+
picker,
133+
/railpackVersion: z\.string\(\)\.nullable\(\)\.default\("([^"]+)"\)/,
134+
),
135+
).toBe(PINNED_VERSIONS.railpack);
136+
});
137+
it("offers the pinned Railpack in the version list", () => {
138+
expect(picker).toContain(`\t"${PINNED_VERSIONS.railpack}",`);
139+
});
140+
});
141+
142+
describe("panel Traefik default", () => {
143+
it("matches the pin", () => {
144+
expect(TRAEFIK_VERSION).toBe(PINNED_VERSIONS.traefik);
145+
});
146+
});
147+
148+
describe("Node.js image and CI version", () => {
149+
const dockerfiles = [
150+
"Dockerfile",
151+
"Dockerfile.cloud",
152+
"Dockerfile.schedule",
153+
"Dockerfile.server",
154+
];
155+
156+
it.each(dockerfiles)("%s builds on the pinned Node image", (file) => {
157+
expect(pinIn(read(file), /FROM node:(\S+)-slim AS base/)).toBe(
158+
PINNED_VERSIONS.node,
159+
);
160+
});
161+
162+
it.each([
163+
".github/workflows/dokploy.yml",
164+
".github/workflows/pull-request.yml",
165+
".github/workflows/sync-openapi-docs.yml",
166+
])("%s runs on the pinned Node version", (file) => {
167+
const versions = [...read(file).matchAll(/node-version: (\S+)/g)].map(
168+
(match) => match[1],
169+
);
170+
expect(versions.length).toBeGreaterThan(0);
171+
expect(versions.every((version) => version === PINNED_VERSIONS.node)).toBe(
172+
true,
173+
);
174+
});
175+
});

0 commit comments

Comments
 (0)