diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..8c50d5d --- /dev/null +++ b/.dockerignore @@ -0,0 +1,14 @@ +# .git stays IN the context on purpose: app/sitemap.ts reads each page's +# lastmod from `git log` at build time. +node_modules +apps/*/node_modules +packages/*/node_modules +apps/*/.next +apps/desktop/out +apps/desktop/dist-electron +packages/*/dist +apps/cli/dist +release +.shots +.env* +**/.env* diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1772d51..4c9a8ae 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -49,6 +49,57 @@ jobs: - name: Desktop starts run: pnpm smoke:desktop + # diskpush.com runs on dev2 as Next's standalone server under Bun (see + # .nixpacks/Dockerfile). Build it the way the image does (Node + pnpm, web + # only), then boot it under Bun and read the pages that come off disk at + # request time or go through the crawl-gateway middleware. + web-bun: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + # app/sitemap.ts dates each page from `git log`. + fetch-depth: 0 + + - uses: pnpm/action-setup@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 24 + cache: pnpm + + - uses: oven-sh/setup-bun@v2 + with: + bun-version: 1.4.0 + + - run: pnpm install --frozen-lockfile --prod=false --filter @diskpush/web... + - run: pnpm --filter @diskpush/web build + + - name: Boot the standalone server under Bun + run: | + s=apps/web/.next/standalone + cp -r apps/web/public "$s/apps/web/public" + cp -r apps/web/.next/static "$s/apps/web/.next/static" + test -f "$s/docs/install.md" && test -f "$s/scripts/install.sh" + (cd "$s" && PORT=3999 HOSTNAME=127.0.0.1 bun apps/web/server.js > /tmp/web.log 2>&1 &) + for i in $(seq 1 30); do + curl -sf -o /dev/null http://127.0.0.1:3999/ && break + sleep 1 + done + for p in / /docs /docs/install /docs/cli /security /privacy /install.sh /llms.txt /robots.txt /sitemap.xml /.well-known/security.txt /manifest.json; do + code=$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:3999$p") + echo "$p $code" + [ "$code" = 200 ] || { cat /tmp/web.log; exit 1; } + done + # An unknown slug is read from docs/ at request time: 404, not 500. + code=$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:3999/docs/no-such-doc) + echo "/docs/no-such-doc $code"; [ "$code" = 404 ] + grep -q 'SITE=' <(curl -s http://127.0.0.1:3999/install.sh) + # The crawl gateway in middleware answers AI training crawlers 402. + code=$(curl -s -o /dev/null -w '%{http_code}' -A GPTBot http://127.0.0.1:3999/) + echo "GPTBot $code"; [ "$code" = 402 ] + cat /tmp/web.log + integration: runs-on: ubuntu-latest steps: diff --git a/.nixpacks/Dockerfile b/.nixpacks/Dockerfile index 844793d..9e4076b 100644 --- a/.nixpacks/Dockerfile +++ b/.nixpacks/Dockerfile @@ -1,35 +1,35 @@ -FROM ghcr.io/railwayapp/nixpacks:ubuntu-1745885067 - -ENTRYPOINT ["/bin/bash", "-l", "-c"] -WORKDIR /app/ - - -COPY .nixpacks/nixpkgs-23f9169c4ccce521379e602cc82ed873a1f1b52b.nix .nixpacks/nixpkgs-23f9169c4ccce521379e602cc82ed873a1f1b52b.nix -RUN nix-env -if .nixpacks/nixpkgs-23f9169c4ccce521379e602cc82ed873a1f1b52b.nix && nix-collect-garbage -d - - -ARG CI NIXPACKS_METADATA NODE_ENV NPM_CONFIG_PRODUCTION -ENV CI=$CI NIXPACKS_METADATA=$NIXPACKS_METADATA NODE_ENV=$NODE_ENV NPM_CONFIG_PRODUCTION=$NPM_CONFIG_PRODUCTION - -# setup phase -# noop - -# install phase -ENV NIXPACKS_PATH=/app/node_modules/.bin:$NIXPACKS_PATH -COPY . /app/. -RUN --mount=type=cache,id=8nFwAs26cBU-/root/local/share/pnpm/store/v3,target=/root/.local/share/pnpm/store/v3 npm install -g corepack@0.36.0 && corepack enable -RUN --mount=type=cache,id=8nFwAs26cBU-/root/local/share/pnpm/store/v3,target=/root/.local/share/pnpm/store/v3 pnpm install --frozen-lockfile --prod=false --filter @diskpush/web... - -# build phase -COPY . /app/. -RUN --mount=type=cache,id=8nFwAs26cBU-apps/desktop/next/cache,target=/app/apps/desktop/.next/cache --mount=type=cache,id=8nFwAs26cBU-apps/web/next/cache,target=/app/apps/web/.next/cache --mount=type=cache,id=8nFwAs26cBU-node_modules/cache,target=/app/node_modules/.cache pnpm --filter @diskpush/web build - - -RUN printf '\nPATH=/app/node_modules/.bin:$PATH' >> /root/.profile - - -# start -COPY . /app - -CMD ["pnpm --filter @diskpush/web start"] - +# syntax=docker/dockerfile:1 +# +# diskpush.com on dev2. Hand-written: do NOT regenerate with nixpacks. +# dev2's compose (cli-tools dev2-site) builds this exact path, so it stays here. +# +# The build is unchanged from the nixpacks image: Node 24 + pnpm, installing +# only @diskpush/web and its deps from pnpm-lock.yaml. The monorepo stays on +# pnpm because the CLI and desktop releases are built and published with it. +# Only the runtime moves: Next's standalone server runs under Bun, with no +# Node in the final image. + +FROM node:24-slim AS build +RUN apt-get update && apt-get install -y --no-install-recommends git ca-certificates \ + && rm -rf /var/lib/apt/lists/* \ + && npm install -g pnpm@11.18.0 +WORKDIR /app +COPY . . +ENV NEXT_TELEMETRY_DISABLED=1 CI=true +RUN --mount=type=cache,id=diskpush-pnpm-store,target=/root/.local/share/pnpm/store \ + pnpm install --frozen-lockfile --prod=false --filter @diskpush/web... +RUN pnpm --filter @diskpush/web build + +FROM oven/bun:1.4.0-slim AS runtime +WORKDIR /app +ENV NODE_ENV=production NEXT_TELEMETRY_DISABLED=1 PORT=3000 +# Docker sets HOSTNAME to the container id, and standalone binds to it literally. +ENV HOSTNAME=0.0.0.0 +COPY --from=build --chown=bun:bun /app/apps/web/.next/standalone ./ +COPY --from=build --chown=bun:bun /app/apps/web/.next/static ./apps/web/.next/static +COPY --from=build --chown=bun:bun /app/apps/web/public ./apps/web/public +USER bun +EXPOSE 3000 +HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \ + CMD bun -e "fetch('http://127.0.0.1:'+(process.env.PORT||3000)+'/').then(r=>process.exit(r.status<500?0:1)).catch(()=>process.exit(1))" +CMD ["bun", "apps/web/server.js"] diff --git a/.nixpacks/build.sh b/.nixpacks/build.sh deleted file mode 100644 index 8bd92fa..0000000 --- a/.nixpacks/build.sh +++ /dev/null @@ -1 +0,0 @@ -docker build . -f ./.nixpacks/Dockerfile -t e28a68b1-8df8-4e57-a812-4038e72cd5be --build-arg CI=true --build-arg NIXPACKS_METADATA=node --build-arg NODE_ENV=production --build-arg NPM_CONFIG_PRODUCTION=false \ No newline at end of file diff --git a/.nixpacks/nixpkgs-23f9169c4ccce521379e602cc82ed873a1f1b52b.nix b/.nixpacks/nixpkgs-23f9169c4ccce521379e602cc82ed873a1f1b52b.nix deleted file mode 100644 index 66aafca..0000000 --- a/.nixpacks/nixpkgs-23f9169c4ccce521379e602cc82ed873a1f1b52b.nix +++ /dev/null @@ -1,21 +0,0 @@ -{ }: - -let pkgs = import (fetchTarball "https://github.com/NixOS/nixpkgs/archive/23f9169c4ccce521379e602cc82ed873a1f1b52b.tar.gz") { overlays = [ (import (builtins.fetchTarball "https://github.com/railwayapp/nix-npm-overlay/archive/main.tar.gz")) ]; }; -in with pkgs; - let - APPEND_LIBRARY_PATH = "${lib.makeLibraryPath [ gcc-unwrapped ] }"; - myLibraries = writeText "libraries" '' - export LD_LIBRARY_PATH="${APPEND_LIBRARY_PATH}:$LD_LIBRARY_PATH" - - ''; - in - buildEnv { - name = "23f9169c4ccce521379e602cc82ed873a1f1b52b-env"; - paths = [ - (runCommand "23f9169c4ccce521379e602cc82ed873a1f1b52b-env" { } '' - mkdir -p $out/etc/profile.d - cp ${myLibraries} $out/etc/profile.d/23f9169c4ccce521379e602cc82ed873a1f1b52b-env.sh - '') - nodejs_24 pnpm-9_x - ]; - } diff --git a/apps/web/next.config.mjs b/apps/web/next.config.mjs index f2ca04b..3135ae0 100644 --- a/apps/web/next.config.mjs +++ b/apps/web/next.config.mjs @@ -37,6 +37,12 @@ const securityHeaders = [ const nextConfig = { reactStrictMode: true, + // The dev2 image runs .next/standalone under Bun (see .nixpacks/Dockerfile). + // The tracing root is the repository root, so the server lands at + // .next/standalone/apps/web/server.js and the docs/ and scripts/ includes + // below land at the same ../../ path the pages read them from. + output: 'standalone', + outputFileTracingRoot: new URL('../../', import.meta.url).pathname, // The docs pages read Markdown from the repository at build time, so the // site and the repo can never disagree about what DiskPush does. outputFileTracingIncludes: { diff --git a/nixpacks.toml b/nixpacks.toml deleted file mode 100644 index f292df3..0000000 --- a/nixpacks.toml +++ /dev/null @@ -1,18 +0,0 @@ -# How the diskpush.com web app is built and started when the builder is nixpacks -# (dev2, and anywhere else that is not Railway). Railway held the same three -# commands in its RAILPACK_INSTALL_CMD / RAILPACK_BUILD_CMD / RAILPACK_START_CMD -# service variables, which no other builder can see. The root package.json has -# no start script on purpose: the web app is one workspace of several. -[phases.install] -cmds = [ - # corepack 0.24.1 (nixpacks' default) cannot launch pnpm 10+ on Node 24: - # ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING. 0.36 can. - "npm install -g corepack@0.36.0 && corepack enable", - "pnpm install --frozen-lockfile --prod=false --filter @diskpush/web...", -] - -[phases.build] -cmds = ["pnpm --filter @diskpush/web build"] - -[start] -cmd = "pnpm --filter @diskpush/web start"