Repository navigation
feat: installer hardening flags and one source for pinned versions - #49
Merged
Merged
Conversation
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
…s 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>
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>
… 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>
…ad 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>
…gh 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>
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>
…ed 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>
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>
…ll 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>
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>
…tall 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>
… 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>
… 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>
…k 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>
…ers 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>
…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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What is this PR about?
Two related changes for the installer and for the server setup that the panel runs.
DOKPLOY_SERVER_HARDENINGsetting for servers that the panel sets up. Everything is off by default.PINNED_VERSIONS). The panel installer, the remote server setup, the Dockerfiles, the CI workflow and the application Railpack picker read the same values, and a test fails when they drift. The pins move to current stable releases: 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. A Components button on each server updates the installed components to these versions.Why: a freshly installed panel and servers added later should get identical versions, and an already set up server should be updatable from the panel without SSH.
Reviewer notes
railpackVersionstored in their row; the database default (0.15.4) is unchanged and needs a migration to change. New applications get 0.40.1 in the form. Please check a real Railpack build before rollout.UsePAM no(required by the Security tab) disables pam_limits and pam_systemd for SSH sessions. Login by key was checked.tscis clean for packages/server and apps/dokploy, Biome is clean, and the full apps/dokploy run passes except one file that needs Docker in swarm mode (application.real.test.ts), which is unrelated to this change. If TypeScript reports errors in@better-auth/scimorbetter-authlocally, node_modules is stale:bun install --frozen-lockfilefixes it; the lockfile is unchanged.canaryitem does not apply.Checklist
Issues related (if applicable)
None.
Screenshots (if applicable)
Not applicable.
🤖 Generated with Claude Code