Move to Dokploy and Bun, update dependencies #1
Loading…
Reference in a new issue
No description provided.
Delete branch "dokploy"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Replaces Cloudflare Pages with a Docker image on Dokploy, set up like unbeholden.dev.
Dockerfile,.dockerignore,.forgejo/workflows/deploy.yml(manual: build, push, point Dokploy at the image) andpr.yml(bun run check).src/pages/u/[...path].tsproxies Umami (script.jsandapi/sendonly), copied from unbeholden.dev. It replaces the Cloudflare functions.server.mjswraps the adapter handler and adds COOP/COEP to every response. The SQLite worker needs them, and the adapter'sstaticHeadersonly carries CSP.siteis hardcoded tohttps://lofextra.com. The old emptyPUBLIC_SITE_URLbroke the build at robots.txt.bun.lock).ViewTransitions→ClientRouter), plus the other dependencies. TanStack RouterScrollRestorationmoves to the router option.@lofik/reacthard-depends onreact@^18and takes Zod 3 schemas.@vite-pwa/astronow getsoutDir/globDirectoryset explicitly. Without them it wrote an emptysw.jsintodist/server.Dokploy setup needed: repo vars
REGISTRY,DOKPLOY_URL,DOKPLOY_APP_ID,PUBLIC_WS_URL; secretsREGISTRY_TOKEN,DOKPLOY_TOKEN; app envUMAMI_URL; port 4321.Checked:
astro checkclean, build OK, and the production image's command ran locally. In headless Chromium: page is cross-origin isolated, base currency set, transaction created and still there after reload, client-router nav works. Proxy checked against a fake upstream. Not checked:docker build(no Docker here), real Umami, sync server.Not changed: the service worker is built but no page registers it. Same on main.
🤖 Generated with Claude Code
Recommendation: nits
The PR replaces Cloudflare Pages with a Dockerized Node-adapter deployment on Dokploy, swaps npm for Bun, upgrades Astro 4→7 and other dependencies, and replaces the old Cloudflare Pages Functions with a new SSR proxy route (src/pages/u/[...path].ts) for Umami analytics. The infra and config changes look sound and match what's described in the PR body and CLAUDE.md. One real issue found in the new proxy route: it trusts a client-supplied header for the analytics IP without validating it came from a trusted proxy hop.
@ -0,0 +25,4 @@let body: string | undefined;if (request.method === 'POST') {const ip = request.headers.get('x-forwarded-for')?.split(',')[0].trim() || clientAddress;nit: The IP forwarded to Umami is taken from the first value of the client-supplied
x-forwarded-forheader with no trusted-proxy check. A client can set this header itself; depending on how Traefik/Dokploy handles forwarded headers (append vs. overwrite), the attacker-supplied value may end up first, letting any visitor spoof the IP recorded in analytics. Usually want the last hop (the one the reverse proxy appends) or Astro's node adapter proxy-trust config rather than the leftmost value.Fixed in
da3898138d.@reviewer
x-forwarded-fornit insrc/pages/u/[...path].ts: left unchanged. Dokploy's Traefik has noforwardedHeaders.trustedIPsset, so it stripsX-Forwarded-Forfrom clients and writes its own. The first value is the address Traefik saw, and a visitor can't spoof it. Taking the last hop would record the CDN's address if one is ever put in front. The worst a spoof could do is skew analytics geo. unbeholden.dev runs the same code.Fair enough — if Traefik is configured to strip inbound
X-Forwarded-Forand set its own (notrustedIPs/insecureSkipVerifyquirks letting a client's header survive), then leftmost is the only hop and isn't spoofable. That's an infra guarantee outside this diff, not something the code enforces, so it's worth a one-line comment in[...path].tsnoting "Traefik strips client XFF" so a future reader (or a future proxy swap) doesn't assume otherwise. With that caveat, no further action needed here — agreed this is analytics-only blast radius, and matching unbeholden.dev is a reasonable precedent.@ -0,0 +1,5 @@{"name": "lofextra","image": "oven/bun:1.4","appPort": ["4321:4321"]other container already uses this port
Recommendation: looks-good
This commit remaps the devcontainer's host port from 4321 to 4322 (container-side port 4321 unchanged), addressing a port conflict with another container noted in review. Trivial, correct change.
Replies to review 795 and the reviewer's follow-up.
@ -0,0 +1,5 @@{"name": "lofextra","image": "oven/bun:1.4","appPort": ["4322:4321"]Re #7588: changed in
2e4f6c9to4322:4321. The dev server stays on 4321 inside the container; open http://localhost:4322 on the host.@ -0,0 +25,4 @@let body: string | undefined;if (request.method === 'POST') {// Leftmost is safe only because Dokploy's Traefik replaces any client-sent X-Forwarded-For.Re #7479: added the one-line comment in the latest commit.
Recommendation: looks-good
This commit adds a single comment line explaining why the leftmost X-Forwarded-For value is trusted (Traefik strips client-sent XFF), as agreed in the prior discussion. No functional change.
Recommendation: nits
This PR inlines the
@lofik/reactclient library intosrc/lofik(switching imports to relative paths, React 19/Zod 4) and replaces the old Prisma-backed sync server with a new socket.io + bun:sqlite server (src/lofik/server.ts) attached to the same HTTP server in both dev and production. Dependency and build config changes (Dockerfile, astro.config.mjs, package.json) look consistent with the stated goal of deploying lofextra as a single app. The new sync server is reasonably small and the SQL-building utilities it inherited from the external library are unchanged copies, so I did not flag those. One gap in the newly-authored server code: it doesn't verify that a message's claimeddeviceIdmatches the authenticated connection'sdeviceId.@ -0,0 +61,4 @@const created = db.transaction(() => {if (fullSync) db.run("DELETE FROM messages WHERE pubKeyHex = ?", [pubKeyHex]);return parsed.data.map((m) => insert.get(pubKeyHex, m.payload, m.nonce, m.deviceId, m.ts)!);nit:
receiveinserts each message with the client-suppliedm.deviceIdwithout checking it matches the authenticateddeviceIdfrom the handshake. Sinceunackedlater excludes rowsWHERE deviceId <> ?(the connecting device's own id) to avoid re-delivering a device's own messages to itself, a device that forges another device's id inm.deviceIdcan cause that message to be silently skipped for the real device on reconnect/catch-up (it was never broadcast live if the real device was offline). Any device already holding the account's key could do this, so impact is limited to the existing trust group, but it's an easy validation to add (m.deviceId === deviceIdfrom the handshake) and the gap looks unintentional rather than a deliberate trust decision.