diff --git a/.github/workflows/bun-migration-docker-check.yml b/.github/workflows/bun-migration-docker-check.yml index afadcce7aa..9aeaa11cfc 100644 --- a/.github/workflows/bun-migration-docker-check.yml +++ b/.github/workflows/bun-migration-docker-check.yml @@ -2,10 +2,16 @@ name: Bun Migration Docker Check on: workflow_dispatch: + inputs: + railpack-frontend: + description: "Railpack frontend for the sample app build, e.g. a version existing applications keep (empty: the image's Railpack version)" + type: string + default: "" pull_request: branches: [AgentHits-Dev] paths: - ".github/workflows/bun-migration-docker-check.yml" + - ".trivyignore.yaml" - "Dockerfile" - "Dockerfile.cloud" - "Dockerfile.server" @@ -58,6 +64,57 @@ jobs: test -f /app/packages/server/dist/index.js node --input-type=module -e "console.log(await import.meta.resolve(\"@dokploy/server\"))" ' + # The panel calls these at runtime; the image installs them without + # apt recommends and without the Docker engine. + docker run --rm --entrypoint sh dokploy-bun-check:main -ec ' + docker --version + docker buildx version + docker compose version + rclone version | head -n 1 + railpack --version + pack --version + nixpacks --version + git lfs version + ssh -V + for tool in ssh-keyscan pkill htpasswd rsync jq zip unzip ip tini; do command -v "$tool"; done + ' + + # Repeats the panel's Railpack path (railpack prepare, then buildx with the + # Railpack frontend on a docker-container builder) from inside the image, + # so a Railpack bump that cannot build a plain Node app fails here. + - name: Build a sample app with Railpack + env: + RAILPACK_FRONTEND: ${{ inputs.railpack-frontend }} + run: | + app="$RUNNER_TEMP/railpack-app" + mkdir -p "$app" + printf '%s\n' '{"name":"railpack-smoke","private":true,"scripts":{"start":"node index.js"}}' > "$app/package.json" + printf '%s\n' 'require("node:http").createServer((_, res) => res.end("railpack-ok")).listen(process.env.PORT || 3000);' > "$app/index.js" + frontend="${RAILPACK_FRONTEND:-$(docker run --rm --entrypoint railpack dokploy-bun-check:main --version | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -n 1)}" + echo "Railpack frontend: $frontend" + docker run --rm -v /var/run/docker.sock:/var/run/docker.sock -v "$app:/src" -e FRONTEND="$frontend" --entrypoint sh dokploy-bun-check:main -ec ' + railpack prepare /src --plan-out /src/railpack-plan.json --info-out /src/railpack-info.json + docker buildx create --name railpack-smoke --driver docker-container + docker buildx build --builder railpack-smoke --build-arg "BUILDKIT_SYNTAX=ghcr.io/railwayapp/railpack-frontend:v$FRONTEND" -f /src/railpack-plan.json --output type=docker,name=railpack-smoke /src + docker buildx rm railpack-smoke + ' + docker run -d --name railpack-smoke -e PORT=3000 -p 127.0.0.1:3999:3000 railpack-smoke + for _ in $(seq 1 30); do + if [ "$(curl -fs http://127.0.0.1:3999 || true)" = railpack-ok ]; then exit 0; fi + sleep 1 + done + docker logs railpack-smoke + exit 1 + + # Same scan as image-audit.yml, on the image this PR builds. + - name: Scan main image + env: + TRIVY_IMAGE: aquasec/trivy:0.76.1@sha256:790fd66d1697395d8693d1acaa1ea702406a20f109b6fc7bac8db623eca6c06c + run: | + docker run --rm -v /var/run/docker.sock:/var/run/docker.sock -v "$PWD:/src:ro" -w /src "$TRIVY_IMAGE" image \ + --image-src docker --scanners vuln --severity CRITICAL,HIGH \ + --ignore-unfixed --ignorefile .trivyignore.yaml --exit-code 1 \ + --no-progress dokploy-bun-check:main service-image-builds: runs-on: ubuntu-24.04 diff --git a/.github/workflows/image-audit.yml b/.github/workflows/image-audit.yml new file mode 100644 index 0000000000..9fe2bde227 --- /dev/null +++ b/.github/workflows/image-audit.yml @@ -0,0 +1,44 @@ +name: Image audit + +# bun audit sees only the npm dependencies. This scan reads the published panel +# image: its Debian packages, the Go binaries of the bundled tools (Docker CLI, +# rclone, Railpack, pack) and the npm packages inside it. It fails on critical +# and high findings that already have a fix; .trivyignore.yaml lists the ones +# still waiting for a tool release. GitHub notifies the person who last changed +# the cron line when a scheduled run fails. +on: + schedule: + - cron: "53 5 * * 1" + workflow_dispatch: + inputs: + image: + description: Image to scan + type: string + default: ghcr.io/agenthits/dokploy:agenthits-dev + +permissions: + contents: read + +env: + # Pinned by digest: in March 2026 Trivy releases, its images and the + # trivy-action tags were replaced with credential stealers (GHSA-69fq-xp46-6x23). + # bun-migration-docker-check.yml pins the same image. + TRIVY_IMAGE: aquasec/trivy:0.76.1@sha256:790fd66d1697395d8693d1acaa1ea702406a20f109b6fc7bac8db623eca6c06c + +jobs: + scan: + name: image audit + runs-on: ubuntu-24.04 + timeout-minutes: 20 + steps: + - uses: actions/checkout@v7 + with: + persist-credentials: false + - name: Scan image + env: + IMAGE: ${{ inputs.image || 'ghcr.io/agenthits/dokploy:agenthits-dev' }} + run: | + docker run --rm -v "$PWD:/src:ro" -w /src "$TRIVY_IMAGE" image \ + --image-src remote --scanners vuln --severity CRITICAL,HIGH \ + --ignore-unfixed --ignorefile .trivyignore.yaml --exit-code 1 \ + --no-progress "$IMAGE" diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index d7b2aaeb4d..0f22830e8d 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -68,7 +68,7 @@ jobs: - name: Install Railpack if: matrix.job == 'test' && runner.environment == 'github-hosted' run: | - export RAILPACK_VERSION=0.40.1 + export RAILPACK_VERSION=0.42.0 curl -sSL https://railpack.com/install.sh | bash echo "Railpack installed $RAILPACK_VERSION" diff --git a/.trivyignore.yaml b/.trivyignore.yaml new file mode 100644 index 0000000000..f38e7226d8 --- /dev/null +++ b/.trivyignore.yaml @@ -0,0 +1,21 @@ +# Fixable findings in bundled tools whose latest release still carries them. +# Each entry expires, so the image audit fails again and the tool gets checked +# for a new release. +vulnerabilities: + - id: CVE-2026-111943 + paths: + - usr/bin/rclone + - usr/local/bin/railpack + statement: rclone 1.75.2 and Railpack 0.42.0 are built with Go 1.26.9; the fix needs Go 1.26.10. + expired_at: 2026-11-27 + - id: CVE-2026-78669 + paths: + - usr/libexec/docker/cli-plugins/docker-buildx + - usr/libexec/docker/cli-plugins/docker-compose + statement: buildx 0.38.1 and compose 5.6.1, the latest releases, use golang.org/x/net below 0.60.0. + expired_at: 2026-11-27 + - id: CVE-2026-17106 + paths: + - usr/libexec/docker/cli-plugins/docker-buildx + statement: buildx 0.38.1, the latest release, uses moby/go-archive 0.2.0. + expired_at: 2026-11-27 diff --git a/CLAUDE.md b/CLAUDE.md index b6a2ac7598..87744d8a95 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -27,3 +27,4 @@ One GitHub release `agenthits-vX.Y.Z` covers a batch of work, not a single PR (o - The one-day age is `minimumReleaseAge` in `bunfig.toml` and `MIN_RELEASE_AGE` in the workflow; change both together. - A commit pushed to the branch of the open PR stops its daily rebuild until the PR is merged or closed. - `.github/workflows/dependency-audit.yml` runs `bun audit` every Monday for advisories published between updates. +- `bun audit` does not see the image. Trivy scans the published `agenthits-dev` image every Monday (`.github/workflows/image-audit.yml`) and the image a PR builds (`bun-migration-docker-check.yml`). Both fail on critical and high findings that have a fix in Debian packages, in the Go binaries of the bundled tools (Docker CLI, rclone, Railpack, pack) or in npm packages inside the image. `.trivyignore.yaml` holds findings whose latest tool release still carries them; each entry expires, so the audit fails again and the tool gets rechecked. Trivy is pinned by digest in both workflows; move them together. diff --git a/Dockerfile b/Dockerfile index 1ca88dde2f..52ea42ac3d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -25,10 +25,14 @@ COPY packages/server/package.json ./packages/server/package.json # build-stage install kept Bun's isolated store (node_modules/.bun, 1.6 GB) in # the image, and packages/server resolved its imports through that store, so # the panel loaded every shared dependency into memory twice. +# +# The @trpc packages list typescript as a required peer, so the production +# install also brings the TypeScript 7 compiler, a Go binary the panel never runs. FROM deps AS prod-deps RUN --mount=type=cache,id=bun,target=/root/.bun/install/cache \ bun install --production --frozen-lockfile --linker hoisted \ && rm -f node_modules/dokploy node_modules/@dokploy/api node_modules/@dokploy/schedules \ + && rm -rf node_modules/typescript node_modules/@typescript node_modules/.bin/tsc \ && mkdir -p packages/server/node_modules FROM deps AS build @@ -61,7 +65,18 @@ ENV NODE_ENV=production ENV DOKPLOY_OFFICIAL_VERSION=$DOKPLOY_OFFICIAL_VERSION ENV DOKPLOY_FORK_VERSION=$DOKPLOY_FORK_VERSION -RUN apt-get update && apt-get install -y tini curl unzip zip apache2-utils iproute2 rsync jq git-lfs && git lfs install && rm -rf /var/lib/apt/lists/* +# Recommends would add systemd, dbus and X11 libraries; the packages the panel +# calls are listed instead: openssh-client for git over SSH and ssh-keyscan, +# procps for pkill when a deployment is cancelled. +RUN apt-get update \ + && apt-get install -y --no-install-recommends tini curl unzip zip apache2-utils iproute2 rsync jq git-lfs openssh-client procps xz-utils netbase \ + && git lfs install \ + && rm -rf /var/lib/apt/lists/* + +# The panel runs on node and bun. npm, npx and node-gyp only serve the build +# stages, and npm ships its own copies of packages with open advisories. +RUN rm -rf /usr/local/lib/node_modules/npm /usr/local/lib/node_modules/node-gyp \ + /usr/local/bin/npm /usr/local/bin/npx /usr/local/bin/node-gyp # Copy only the necessary files COPY --from=build /usr/src/app/apps/dokploy/.next ./.next @@ -78,12 +93,21 @@ COPY --from=build /usr/src/app/packages/server/package.json ./packages/server/pa COPY --from=build /usr/src/app/packages/server/dist ./packages/server/dist -# Install docker -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 +# The panel drives the host's Docker daemon through the mounted socket, so it +# needs only the CLI and its buildx and compose plugins. get.docker.com would +# also install the engine, containerd and the rootless extras: unused binaries +# that scanners report. +ARG DOCKER_VERSION=29.9.2 +RUN . /etc/os-release \ + && install -m 0755 -d /etc/apt/keyrings \ + && curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc \ + && echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian ${VERSION_CODENAME} stable" > /etc/apt/sources.list.d/docker.list \ + && apt-get update \ + && apt-get install -y --no-install-recommends "docker-ce-cli=5:${DOCKER_VERSION}-1~debian.${VERSION_ID}~${VERSION_CODENAME}" docker-buildx-plugin docker-compose-plugin \ + && rm -rf /var/lib/apt/lists/* # Install rclone -ARG RCLONE_VERSION=1.75.1 +ARG RCLONE_VERSION=1.75.2 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 \ @@ -102,12 +126,12 @@ RUN curl -sSL https://nixpacks.com/install.sh -o install.sh \ && bun install -g tsx # Install Railpack -ARG RAILPACK_VERSION=0.40.1 +ARG RAILPACK_VERSION=0.42.0 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.40.9 /usr/local/bin/pack /usr/local/bin/pack +COPY --from=buildpacksio/pack:0.40.10 /usr/local/bin/pack /usr/local/bin/pack EXPOSE 3000 diff --git a/apps/dokploy/__test__/server/server-components.test.ts b/apps/dokploy/__test__/server/server-components.test.ts index f80885d8d7..af4794dbc2 100644 --- a/apps/dokploy/__test__/server/server-components.test.ts +++ b/apps/dokploy/__test__/server/server-components.test.ts @@ -517,7 +517,7 @@ case "$1" in 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" + echo " docker-ce | 5:${PINNED_VERSIONS.docker}-1~ubuntu.24.04~noble | https://download.docker.com/linux/ubuntu noble/stable amd64 Packages" ;; policy) case "$2" in @@ -726,7 +726,7 @@ const runComponentScript = ( CALL_LOG: callLog, FAKE_STATE: dir, FAKE_PREVIOUS_ENGINE: "28.3.0", - FAKE_TARGET_ENGINE: "29.8.2", + FAKE_TARGET_ENGINE: PINNED_VERSIONS.docker, FAKE_OLD_IMAGE: "traefik:v3.6.25", TMPDIR: dir, TRAEFIK_SETTLE_SECONDS: "0", @@ -763,7 +763,7 @@ const runComponentScript = ( 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"; +const DOCKER_NEW = `5:${PINNED_VERSIONS.docker}-1~ubuntu.24.04~noble`; const HTTP_CHALLENGE_CONFIG = "certificatesResolvers:\n letsencrypt:\n acme:\n email: admin@example.com\n httpChallenge:\n entryPoint: web\n"; const TLS_CHALLENGE_CONFIG = @@ -1413,7 +1413,7 @@ describe("buildComponentUpdateScript run order", { timeout: 60_000 }, () => { 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.", + `Docker not updated: Docker ${PINNED_VERSIONS.docker} is not offered by the Docker apt repository configured on this host.`, ); expect(run.stdout).toContain( `Traefik version ${TRAEFIK_VERSION} installed`, @@ -1499,7 +1499,7 @@ describe("buildComponentUpdateScript run order", { timeout: 60_000 }, () => { 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.", + `Docker not updated: Docker ${PINNED_VERSIONS.docker} 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`, diff --git a/apps/dokploy/components/dashboard/application/build/show.tsx b/apps/dokploy/components/dashboard/application/build/show.tsx index 6fb9031243..7f301990dc 100644 --- a/apps/dokploy/components/dashboard/application/build/show.tsx +++ b/apps/dokploy/components/dashboard/application/build/show.tsx @@ -31,6 +31,8 @@ import { api } from "@/utils/api"; // Railpack versions from https://github.com/railwayapp/railpack/releases export const RAILPACK_VERSIONS = [ + "0.42.0", + "0.41.0", "0.40.1", "0.39.0", "0.38.0", @@ -142,7 +144,7 @@ const mySchema = z.discriminatedUnion("buildType", [ }), z.object({ buildType: z.literal(BuildType.railpack), - railpackVersion: z.string().nullable().default("0.40.1"), + railpackVersion: z.string().nullable().default("0.42.0"), }), z.object({ buildType: z.literal(BuildType.static), @@ -283,7 +285,7 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => { : null, railpackVersion: data.buildType === BuildType.railpack - ? data.railpackVersion || "0.40.1" + ? data.railpackVersion || "0.42.0" : null, }) .then(async () => { @@ -545,7 +547,7 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => { size="sm" onClick={() => { setIsManualRailpackVersion(false); - field.onChange("0.40.1"); + field.onChange("0.42.0"); }} > Use predefined versions @@ -561,7 +563,7 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => { field.onChange(value); } }} - value={field.value ?? "0.40.1"} + value={field.value ?? "0.42.0"} > @@ -575,7 +577,7 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => { {RAILPACK_VERSIONS.map((version) => ( v{version} - {version === "0.40.1" && ( + {version === "0.42.0" && ( /dev/null 2>&1