Skip to content

feat: add docker support - #268

Merged
Aymericr merged 2 commits into
pascalorg:mainfrom
EvgeniGenchev:feat/docker-support
Aug 4, 2026
Merged

feat: add docker support#268
Aymericr merged 2 commits into
pascalorg:mainfrom
EvgeniGenchev:feat/docker-support

Conversation

@EvgeniGenchev

@EvgeniGenchev EvgeniGenchev commented Apr 21, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Adds a Dockerfile using oven/bun:1.3.0-alpine that installs dependencies, builds all workspace packages via Turbo, and serves the Next.js app
  • Adds a docker-compose.yml for a one-command start (docker compose up --build)
  • Adds .dockerignore to exclude node_modules, build artifacts, and local env files from the build context

Usage

docker compose up --build

The editor will be available at http://localhost:3000.

Optional env vars (e.g. NEXT_PUBLIC_GOOGLE_MAPS_API_KEY) can be passed under the environment key in docker-compose.yml — the editor works without them.

Test plan

  • docker compose up --build completes without errors
  • Editor loads at http://localhost:3000
  • Create/edit a building, confirm 3D viewport works

Note

Low Risk
Packaging and ops-only changes; no application auth or business logic modified, though build/runtime assumptions (Bun version, port 3000, data volume) affect how the editor is deployed.

Overview
Adds containerized deployment for the Next.js editor so it can be built and run with docker compose up --build.

The Dockerfile uses oven/bun:1.3.14-alpine, installs deps with bun install --frozen-lockfile, builds only the editor via Turbo, and runs as the bun user from apps/editor. It installs Alpine nodejs so next build uses a real Node binary (the Bun image’s node shim breaks Next 16). Scene SQLite data is directed to PASCAL_DATA_DIR=/data with a declared volume and permissions for the runtime user.

.dockerignore keeps .git, node_modules, Next/Turbo outputs, env files, and logs out of the build context. docker-compose.yml maps host port 3000 to container 3000 (documented constraint for /scenes and NEXT_PUBLIC_APP_URL build-time inlining), sets production env and pascal-data volume on /data, and uses restart: unless-stopped.

Reviewed by Cursor Bugbot for commit b9d7266. Bugbot is set up for automated code reviews on this repo. Configure here.

@karsumit94

Copy link
Copy Markdown

I came across this repo from an LinkedIn post and was wondering to to run it locally. While I was struggling to find a Dockerfile I was not able to find it. Thought of adding it but then looked at your PR!

@rlenferink

Copy link
Copy Markdown

I was also looking for a containerized version of this software and stumbled upon this pull request. It was indeed as simple as running docker compose up --build.

This pull request actually offers two things:

  • Building a container image using the Dockerfile and preventing it to be bloated by using a proper .dockerignore
  • Providing an easy mechanism to run it with the docker-compose.yml

Already having a container image that builds automatically with each commit and is automatically pushed as release with each official release is a huge win already. Where possibly an example of the docker-compose.yml can also be added to the README.md.

@Aymericr

Copy link
Copy Markdown
Contributor

Keeping this open as the Docker track, but the current 11-line image is a development proof rather than a production container. A current-main revision should verify the GitHub package dependency installs on Alpine, use a multi-stage build with a non-root runtime, copy only runtime output/dependencies, add a healthcheck, and document required build/runtime env.

Please include the exact docker build and run commands plus an /api/health result. That evidence will also let us decide whether Next standalone output is worth enabling.

@Aymericr Aymericr left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this, and sorry for the long silence — it's been open since April and that's on us. I built and ran it end to end, and the core is solid: docker build --no-cache succeeds on both arm64 and amd64 (5/5 turbo tasks), the container boots next start on :3000, /api/health returns {"status":"ok","app":"editor"}, / renders with the real compiled Tailwind bundle, and docker compose config validates. Every functional claim in your body reproduces.

It also settled two things I'd asked about back in July, both in your favour: the github:pascalorg/plugin-trees dependency does install cleanly on Alpine, and the gnu-pinned native optionalDependencies do not break musl — bun installs both variants and musl resolves at runtime. Good to have that on the record.

One item I'd call a genuine bug rather than polish, and it's the reason I can't merge as-is:

Saved scenes are lost on docker compose down. The editor persists to SQLite, and the default path resolution ends at $HOME/.pascal/data/pascal.db (packages/mcp/src/storage/sqlite-scene-store.ts:107). Inside this container that's /root/.pascal/data/pascal.db, and docker-compose.yml declares no volume — so recreating the container silently discards every project. That's the worst outcome for exactly the "one command to try it" audience this PR is for.

The fix is small, because the store already supports an override (sqlite-scene-store.ts:93-94):

services:
  editor:
    environment:
      NODE_ENV: production
      PASCAL_DATA_DIR: /data
    volumes:
      - pascal-data:/data
volumes:
  pascal-data:

Using PASCAL_DATA_DIR rather than mounting over /root also keeps the path stable if the runtime user changes — which brings me to the rest, in priority order:

  1. Volume + PASCAL_DATA_DIR, as above. This is the blocker.
  2. Don't run as root. The oven/bun base already ships a bun user (uid 1000), so USER bun is enough — and it composes with (1), since the data dir is then explicit rather than $HOME-derived.
  3. Multi-stage. Single-stage means the 2.33 GB image carries ~1.2 GB of dev dependencies (biome, turbo, typescript, tsgo) into the runtime layer. A multi-stage build with Next's output: 'standalone' came out at 598 MB from this same tree and served identically — 4× smaller. This answers the standalone question I raised in July: worth doing. Note apps/editor/next.config.ts doesn't set output today, so that's part of the change.
  4. Drop RUN touch apps/editor/.env.local (line 6). It was a valid workaround at your base commit, when the build script read -e ./.env.local, but main has since moved to dotenv -e ../../.env.local (apps/editor/package.json:8) — so that file isn't the one read, and dotenv-cli tolerates a missing one regardless. I removed it and the build still completed 5/5.

Two corrections while I'm here, neither blocking:

  • The body says optional env vars "can be passed under the environment key". For NEXT_PUBLIC_* that isn't true — Next inlines those at build time. I ran the image with -e NEXT_PUBLIC_APP_URL=http://runtime-injected.test and the value appears nowhere in the served HTML or client chunks. Those need ARG + --build-arg. Worth fixing in the body so nobody debugs it later.
  • NEXT_PUBLIC_GOOGLE_MAPS_API_KEY is a stale variable — it only exists in SETUP.md:26 and .env.example:4 and is referenced by zero lines of apps/ or packages/. Not your fault; I'll clean that up separately.

And one thing that isn't a defect but compose should guard against: /scenes returns 500 when the published port differs from 3000, because apps/editor/app/scenes/page.tsx:8-18 derives its self-fetch base from the Host header. Your 3000:3000 mapping avoids it, but setting NEXT_PUBLIC_APP_URL in compose would keep remapped ports and reverse proxies working.

Last thing: the branch is now ~3.5 months behind, and CI landed after you opened this, so it has never run here — 0 check-runs on 5dfca40. Please rebase onto main when you pick this up; bun run check, bun run check-types and bun run test are all gating now.

I'd like to land this — it's a real gap and two people in the thread have asked for it. If you'd rather not do the multi-stage part, I'll take (1), (2) and (4) alone and do the size work myself in a follow-up; say the word. If you've moved on entirely, tell me and I'll carry it over the line with credit to you.

EvgeniGenchev and others added 2 commits August 4, 2026 18:07
Three problems kept the image from being usable:

- Saved scenes went to $HOME/.pascal/data inside the container layer, so
  `docker compose down` silently discarded every project. Set
  PASCAL_DATA_DIR and back it with a named volume.
- The container ran as root. The base image already ships a `bun` user.
- `next build` runs under `node`, and oven/bun's `node` is a shim that
  re-execs bun. Next 16's build crashes it on both arm64 and amd64 —
  segfault on bun 1.3.14, a turbopack CommonJS error on 1.3.0. Install
  real nodejs. CI never hit this because runners have their own node.

Also pins the base image to the bun version in `packageManager` and drops
the `touch apps/editor/.env.local` workaround, which no longer matches the
path the build script reads.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Aymericr
Aymericr force-pushed the feat/docker-support branch from 5dfca40 to b9d7266 Compare August 4, 2026 22:28
@Aymericr

Aymericr commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

I've carried this over the line, as offered above — pushed to your branch (maintainerCanModify was on), so your commit keeps your authorship and mine sits on top. Rebased onto main too, so CI runs on it for the first time.

While verifying, I found a fourth problem that neither of us had spotted, and it's the one that would have bitten every user: the image doesn't build against current main at all.

next build runs under node, and oven/bun's node is a shim that re-execs bun:

$ docker run --rm oven/bun:1.3.14-alpine which node
/usr/local/bun-node-fallback-bin/node

Next 16's build crashes it. On the version I'd bumped to:

Args: "node" "/app/node_modules/.bin/next" "build"
panic: Segmentation fault at address 0x13CB0

and on your original 1.3.0, a different failure with the same cause:

Failed to load external module next/dist/compiled/next-server/app-page-turbo.runtime.prod.js:
TypeError: Expected CommonJS module to have a function wrapper
Error: Failed to collect page data for /

I confirmed it's the runtime and not the platform by reproducing on both arm64 and amd64, then fixing it with one line — apk add --no-cache nodejs takes the build from Failed: editor#build to Tasks: 5 successful, 5 total. Worth knowing why CI didn't catch this: GitHub runners have a real node on PATH, so bun run build there never touches the shim. The container is the only place the shim is the only node.

Not your fault — Next moved under the branch during the four months this sat open. But it does mean the docker build result in your body no longer reproduces, which is exactly the kind of drift a stale branch accumulates.

What's in my commit

  1. PASCAL_DATA_DIR=/data + a named volume — the blocker. Verified with a scene that survives container destruction:
    POST /api/scenes          → id bb5c6e9f6352
    docker compose down       → Container wt268-editor-1 Removed
    docker compose up -d
    GET  /api/scenes?limit=10 → bb5c6e9f6352, same createdAt
    
    And the counterfactual, because a persistence fix that isn't load-bearing is worth nothing: with PASCAL_DATA_DIR unset, the DB lands at /home/bun/.pascal/data/pascal.db — inside the container layer, gone on recreate. /scenes also renders the saved scene, so the API and the page agree.
  2. USER bun — no longer root. uid=1000(bun) gid=1000(bun). /data is chowned so the store can create the DB on first write (it mkdirs its own parent, so nothing else is needed).
  3. Base image pinned to 1.3.14, matching packageManager and what CI installs. A skew there is how --frozen-lockfile fails in the image but not locally.
  4. Dropped RUN touch apps/editor/.env.local — as noted, it targets a path the build script no longer reads.

What I left out, and why

The multi-stage / output: 'standalone' work isn't here. It needs output: 'standalone' in apps/editor/next.config.ts, and that file is shared with a Vercel deployment whose vercel.json declares "outputDirectory": ".next" — flipping output changes what that build produces. There's no build-time-only switch (__NEXT_PRIVATE_STANDALONE_CONFIG is the standalone server reading serialized config, not a way to opt in), and monorepo tracing would additionally need outputFileTracingRoot. So it's a real change to a shared deploy path, not a Dockerfile change, and I'd rather not smuggle it in under a docker PR. The image is 5.33 GB as a result. I'll do that separately.

I also dropped a NEXT_PUBLIC_APP_URL line I'd initially added to compose — I'd suggested it above for remapped ports. It doesn't work, and I should have caught that before suggesting it, since it's the same build-time inlining I flagged in your body:

port 8099 + NEXT_PUBLIC_APP_URL=http://localhost:8099  → /scenes http=500
port 8098, no var                                      → /scenes http=500

Identical, because the value is frozen at next build. Making it work needs ARG + --build-arg and an entry in turbo.json's build.env (it isn't there, so turbo strips it regardless). Also out of scope here; compose keeps 3000:3000 with a comment explaining why the port can't move.

Verified

docker build 5/5 turbo tasks, arm64 and amd64
docker compose config valid
/api/health {"status":"ok","app":"editor"}
runs as uid=1000(bun)
scene survives down/up yes, same id and createdAt
without the fix DB in container layer, lost
/scenes 200, lists the saved scene
bun run check clean, 1600 files

Thanks for opening this and for answering the Alpine and musl questions back in July — both landed in your favour, and the base image choice is yours, unchanged. Merging once CI is green. Sorry it took us three months to meet you halfway.

Aymericr added a commit that referenced this pull request Aug 4, 2026
Documented in SETUP.md and .env.example, referenced by zero lines of
apps/ or packages/ — there is no geocoder in this app, so the address
search it promises does not exist. Noticed while reviewing #268.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
@Aymericr
Aymericr merged commit 78b11a4 into pascalorg:main Aug 4, 2026
2 checks passed
Aymericr added a commit that referenced this pull request Aug 4, 2026
Docker support landed in #268 with no mention in SETUP.md, so the only way
to find it was to notice the compose file. Records the port constraint too,
since remapping it 500s the /scenes page for a non-obvious reason.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants