@alxia/create 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -28,8 +28,8 @@ bun create @alxia my-site --template react-router
28
28
 
29
29
  | template | what it writes |
30
30
  | --- | --- |
31
- | `api` | an `@alxia/core` app with Zod: `POST /todos` validates its body, behind `requireKey`, a hook made with `defineHook` that answers 401 without an API key; a `bun test` spec calling it with `app.request()` and through `@alxia/client`, typed; `bun dev` restarting on change, `typecheck`, `build`, a strict `tsconfig.json`, a `Dockerfile` on `oven/bun:1`, `.dockerignore`, `.gitignore`, `.env.example` and a README |
32
- | `react-router` | React Router's official template, as `create-react-router` writes it, shipped in this package and copied, with [`@alxia/react-router`](https://www.npmjs.com/package/@alxia/react-router) added as its README says: `alxia()` in `vite.config.ts`'s plugins, `start` running `bun build/server/index.js`, a `bunfig.toml` starting React Router's CLI on Bun, and a `Dockerfile` on `oven/bun:1` in place of React Router's Node one. No server file: the default one serves the pages; `bunx alxia-react-router reveal` writes it out to customise |
31
+ | `api` | an `@alxia/core` app with Zod: `POST /todos` validates its body, behind `requireKey`, a hook made with `defineHook` that answers 401 without an API key; a `bun test` spec calling it with `app.request()` and through `@alxia/client`, typed; `bun dev` restarting on change, `typecheck`, `build`, a strict `tsconfig.json`, a `Dockerfile` running on `oven/bun:1-alpine`, `.dockerignore`, `.gitignore`, `.env.example` and a README |
32
+ | `react-router` | React Router's official template, as `create-react-router` writes it, shipped in this package and copied, with [`@alxia/react-router`](https://www.npmjs.com/package/@alxia/react-router) added as its README says: `alxia()` in `vite.config.ts`'s plugins, `start` running `bun build/server/index.js`, a `bunfig.toml` starting React Router's CLI on Bun, and a `Dockerfile` running on `oven/bun:1-alpine` in place of React Router's Node one. No server file: the default one serves the pages; `bunx alxia-react-router reveal` writes it out to customise |
33
33
 
34
34
  The heart of the `api` project, its route and hook (the whole file, with
35
35
  its imports and schemas, is in the [guide](https://github.com/softistx/alxia/blob/develop/packages/create/docs/guide.md#the-api-template)):
@@ -53,13 +53,17 @@ export const app = alxia()
53
53
 
54
54
  ## Docker
55
55
 
56
- Both projects build into an image as they are written, on `oven/bun:1`,
57
- the image running the app as the non-root `bun` user:
56
+ Both projects build into an image as they are written: built on
57
+ `oven/bun:1`, run on `oven/bun:1-alpine` as the non-root `bun` user, an
58
+ image of about 130 MB. Each `Dockerfile`
59
+ builds in a stage of its own, and the image holds the build output alone,
60
+ no `node_modules`:
58
61
 
59
- - `api`: the production dependencies, then `src/server.ts` run as it is.
60
- Bun runs TypeScript, so there is no build stage.
61
- - `react-router`: the production dependencies, then `bun run build`, then
62
- `bun build/server/index.js`.
62
+ - `api`: `bun run build` bundles `src/server.ts` and its dependencies into
63
+ `dist/server.js`; the image holds `dist/` and runs `bun --no-install dist/server.js`.
64
+ - `react-router`: `bun run build`, every dependency bundled into
65
+ `build/server/index.js` by `@alxia/react-router`'s plugin; the image
66
+ holds `build/` and runs `bun --no-install build/server/index.js`.
63
67
 
64
68
  ```sh
65
69
  cd my-api
@@ -76,7 +80,11 @@ docker run -p 3000:3000 my-site
76
80
  Commit the `bun.lock` that `bun install` wrote: the image installs from it
77
81
  with `--frozen-lockfile`. The
78
82
  [guide](https://github.com/softistx/alxia/blob/develop/packages/create/docs/guide.md#docker)
79
- has the stages.
83
+ has the stages, and
84
+ [troubleshooting](https://github.com/softistx/alxia/blob/develop/packages/create/docs/troubleshooting.md#error-cannot-find-package--from-appdistserverjs)
85
+ what to do for a dependency that cannot be bundled, and for a
86
+ [native addon built for glibc alone](https://github.com/softistx/alxia/blob/develop/packages/create/docs/troubleshooting.md#error--is-linked-against-glibc-dt_needed-libmso6-but-this-bun-build-uses-musl),
87
+ which does not load on Alpine.
80
88
 
81
89
  ## Options
82
90
 
package/docs/guide.md CHANGED
@@ -67,10 +67,10 @@ my-api/
67
67
  ├── src/
68
68
  │ ├── app.ts the app, and its type
69
69
  │ ├── app.spec.ts bun test: app.request() and @alxia/client
70
- │ └── server.ts app.listen(PORT)
70
+ │ └── server.ts app.listen(PORT), stopped on SIGTERM
71
71
  ├── package.json
72
72
  ├── tsconfig.json
73
- ├── Dockerfile the production dependencies and src/, on oven/bun:1
73
+ ├── Dockerfile bun run build, then dist/ alone, on oven/bun:1-alpine
74
74
  ├── .dockerignore
75
75
  ├── .env.example PORT and API_KEY, for a .env Bun loads
76
76
  ├── .gitignore
@@ -139,12 +139,20 @@ The scripts:
139
139
  | `bun dev` | `bun --watch src/server.ts`: restarted on every change, on `PORT` or 3000 |
140
140
  | `bun test` | the spec |
141
141
  | `bun run typecheck` | `tsc --noEmit` |
142
- | `bun run build` | `bun build src/server.ts --target=bun --outdir=dist`: one file, its dependencies bundled |
143
- | `bun start` | `bun src/server.ts`: Bun runs the TypeScript as it is, no build first |
142
+ | `bun run build` | `bun build src/server.ts --target=bun --outdir=dist --minify --sourcemap=linked`: one file, its dependencies bundled |
143
+ | `bun start` | `bun dist/server.js`: the build, after `bun run build` |
144
144
 
145
- `build` is for a host that has Bun and no `node_modules`: `dist/server.js`
146
- holds the dependencies, and runs as `bun dist/server.js`. `start`, `bun dev`
147
- and the image need no build.
145
+ `start` runs what `build` wrote, as production and the image do:
146
+
147
+ ```sh
148
+ bun run build && bun start
149
+ ```
150
+
151
+ `dist/server.js` holds every dependency, so it runs on a host that has Bun
152
+ and no `node_modules`. It is minified, and `dist/server.js.map` beside it
153
+ is linked from it: Bun reads the map, so a stack trace names the lines of
154
+ `src/`. `bun dev` and `bun test` run the TypeScript as it is, with no
155
+ build.
148
156
 
149
157
  Bun loads `.env` on every command. `.env.example` names the two variables
150
158
  the app reads, `PORT` (3000 by default) and `API_KEY` (`dev-key` by
@@ -222,19 +230,56 @@ goes from there.
222
230
 
223
231
  ## Docker
224
232
 
225
- Each project's `Dockerfile` runs it on Bun, on the official `oven/bun:1`
226
- image, as the image's non-root `bun` user. The installs are
233
+ Each project's `Dockerfile` builds it on Bun's official `oven/bun:1`
234
+ image, Debian's, and runs it on `oven/bun:1-alpine`, as that image's
235
+ non-root `bun` user (uid 1000). The installs are
227
236
  `--frozen-lockfile`, from the `bun.lock` the command's `bun install`
228
237
  wrote: commit it.
229
238
 
239
+ Each one builds in a stage of its own, and the image holds the build
240
+ output alone: no `node_modules`, no sources. The dependencies are inside
241
+ the bundle, so the image is the base image and a few hundred kilobytes to
242
+ a few megabytes: about 130 MB, where the same build on `oven/bun:1` is
243
+ about 345 MB.
244
+
245
+ The build stages stay on Debian: the tools a build runs, Vite's,
246
+ Tailwind's and any package's install script, are tried on glibc first,
247
+ and Alpine saves nothing there, since the stage is not shipped. What is
248
+ shipped is JavaScript, which Bun runs the same on musl. A native addon is
249
+ the exception: one built for glibc alone cannot load on Alpine, and the
250
+ final stage goes back to `oven/bun:1`
251
+ ([troubleshooting](troubleshooting.md#error--is-linked-against-glibc-dt_needed-libmso6-but-this-bun-build-uses-musl)). A dependency that cannot be bundled is kept external and
252
+ copied in: see
253
+ [troubleshooting](troubleshooting.md#error-cannot-find-package--from-appdistserverjs).
254
+
230
255
  ### `api`
231
256
 
232
- Two stages: the production dependencies, installed with
233
- `bun install --frozen-lockfile --production`, then an image with them,
234
- `package.json` and `src/`, running `bun src/server.ts`, `start`'s command,
235
- written out so that Bun is the container's process. There is no build
236
- stage: Bun runs the TypeScript as it is, so a build would only add a
237
- second, full install and a bundle the image does not need.
257
+ Two stages: every dependency, installed with
258
+ `bun install --frozen-lockfile`, then `bun run build`, which writes
259
+ `dist/server.js` and its source map; then an image with `dist/` alone,
260
+ running `bun --no-install dist/server.js`, `start`'s command with
261
+ Bun's `--no-install` (not create-alxia's option of the same name), so that a package missing from the bundle fails at
262
+ startup rather than being fetched from npm, written out so that Bun
263
+ is the container's process. `src/server.ts` stops the app on `SIGTERM`:
264
+ as process 1, Bun would otherwise ignore it, and `docker stop` would wait.
265
+
266
+ ```dockerfile
267
+ FROM oven/bun:1 AS build
268
+ WORKDIR /app
269
+ COPY package.json bun.lock* bunfig.toml* ./
270
+ RUN bun install --frozen-lockfile
271
+ COPY . .
272
+ RUN bun run build
273
+
274
+ FROM oven/bun:1-alpine
275
+ WORKDIR /app
276
+ ENV NODE_ENV=production
277
+ COPY --from=build /app/dist ./dist
278
+ USER bun
279
+ EXPOSE 3000
280
+ CMD ["bun", "--no-install", "dist/server.js"]
281
+ ```
282
+
238
283
  `.dockerignore` keeps `node_modules`, `dist`, `.env`, the README and the
239
284
  specs out of the context.
240
285
 
@@ -249,11 +294,15 @@ default is for development.
249
294
 
250
295
  ### `react-router`
251
296
 
252
- Three stages: the production dependencies, then every dependency and
253
- `bun run build`, then an image with `build/` and the production
254
- `node_modules` alone, running `bun build/server/index.js`, `start`'s
255
- command. `.dockerignore` keeps `node_modules`, `build` and
256
- `.react-router` out of the context.
297
+ Two stages: every dependency and `bun run build`, then an image with
298
+ `build/` alone, running `bun --no-install build/server/index.js`,
299
+ `start`'s command with `--no-install`.
300
+ `@alxia/react-router`'s plugin bundles every package into
301
+ `build/server/index.js` under `react-router build`, so `build/` needs no
302
+ `node_modules`
303
+ ([Self-contained](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/guide.md#self-contained)).
304
+ `.dockerignore` keeps `node_modules`, `build` and `.react-router` out of
305
+ the context.
257
306
 
258
307
  ```sh
259
308
  cd my-site
package/docs/roadmap.md CHANGED
@@ -30,12 +30,26 @@ Nothing scheduled yet.
30
30
 
31
31
  ### Next release
32
32
 
33
+ - **The images run on Alpine.** Every `Dockerfile`'s final stage is
34
+ `oven/bun:1-alpine`, the build stages staying on `oven/bun:1`: each
35
+ image is about 130 MB, where it was about 345 MB. The `api` project
36
+ stops on `SIGTERM`, so `docker stop` no longer waits.
37
+
38
+ ### 0.1.3
39
+
40
+ - **Every `Dockerfile` builds, and the image holds the build alone.** The
41
+ `api` template's builds `dist/server.js`, bundled, minified and source
42
+ mapped, and runs it with no `node_modules` and no `src/`; `start` runs
43
+ `dist/server.js` after `bun run build`. The `react-router` template's
44
+ copies `build/` alone, which `@alxia/react-router`'s plugin now bundles
45
+ whole. Each image is about 50 MB (api) and 150 MB (react-router)
46
+ smaller.
47
+
48
+ ### 0.1.2
49
+
33
50
  - **The `api` template is files, copied, with a `Dockerfile`.** It ships
34
51
  under `templates/api/` and is copied as `react-router`'s is. New in it:
35
- a `Dockerfile` on `oven/bun:1` that installs the production
36
- dependencies and runs `src/server.ts` as the non-root `bun` user,
37
- `.dockerignore`, `.env.example`, and `start` running the source with no
38
- build first.
52
+ a `Dockerfile` on `oven/bun:1`, `.dockerignore` and `.env.example`.
39
53
 
40
54
  ### 0.1.1
41
55
 
@@ -36,6 +36,9 @@ nothing — the symptom.
36
36
  **After**
37
37
 
38
38
  - [`error: lockfile had changes, but lockfile is frozen`](#error-lockfile-had-changes-but-lockfile-is-frozen)
39
+ - [`error: Module not found "dist/server.js"`](#error-module-not-found-distserverjs)
40
+ - [`error: Cannot find package '…' from '/app/dist/server.js'`](#error-cannot-find-package--from-appdistserverjs)
41
+ - [`error: … is linked against glibc (DT_NEEDED libm.so.6), but this Bun build uses musl.`](#error--is-linked-against-glibc-dt_needed-libmso6-but-this-bun-build-uses-musl)
39
42
  - [The project's `@alxia/*` are older than npm's latest](#the-projects-alxia-are-older-than-npms-latest)
40
43
 
41
44
  ## Before it runs
@@ -242,8 +245,7 @@ project is complete; only `node_modules` is missing.
242
245
  ### `error: lockfile had changes, but lockfile is frozen`
243
246
 
244
247
  **When:** `docker build` in a project stops at
245
- `RUN bun install --frozen-lockfile` (`react-router`) or
246
- `RUN bun install --frozen-lockfile --production` (`api`).
248
+ `RUN bun install --frozen-lockfile`.
247
249
 
248
250
  **Why:** the project's `Dockerfile` installs exactly what `bun.lock`
249
251
  records, and `package.json` now asks for something it does not: a
@@ -254,6 +256,105 @@ behind in a clone where `package.json` moved on without it.
254
256
  traps of the `react-router` image, a write refused to the `bun` user among them, are in
255
257
  [`@alxia/react-router`'s troubleshooting](https://github.com/softistx/alxia/blob/develop/packages/react-router/docs/troubleshooting.md#eacces-permission-denied-open-app).
256
258
 
259
+ ### `error: Module not found "dist/server.js"`
260
+
261
+ **When:** `bun start` in an `api` project that was never built, or whose
262
+ `dist/` was deleted.
263
+
264
+ **Why:** `start` runs the build, `bun dist/server.js`, as the image does;
265
+ it no longer runs `src/server.ts`. `bun dev` runs the sources.
266
+
267
+ **Fix:** build first:
268
+
269
+ ```sh
270
+ bun run build && bun start
271
+ ```
272
+
273
+ ### `error: Cannot find package '…' from '/app/dist/server.js'`
274
+
275
+ The container stops at startup; in the `react-router` image the path is
276
+ `/app/build/server/index.js`. For a package loaded with `require`, Bun
277
+ writes `Cannot find module '…'`. Or the server starts and a request fails
278
+ with `ENOENT: no such file or directory`.
279
+
280
+ **When:** the image holds the build alone, `dist/` or `build/`, as the
281
+ `Dockerfile` copies it, and a dependency is not inside the bundle: one
282
+ the build was told to leave external, or one that cannot be bundled, a
283
+ native addon (a `.node` file) or a package that reads files of its own
284
+ folder at runtime.
285
+
286
+ The image runs `bun --no-install`, Bun's flag, not create-alxia's
287
+ option of the same name: without the flag, Bun finds no
288
+ `node_modules` and fetches the missing package from npm at startup, at
289
+ whatever version npm has, instead of failing.
290
+
291
+ **Why:** the image has no `node_modules`. `bun run build` bundles every
292
+ dependency into one file, and only what it leaves out must be installed
293
+ beside it.
294
+
295
+ **Fix:** mark the package external, and give the image the production
296
+ dependencies. In `api`, in the `build` script:
297
+
298
+ ```json
299
+ "build": "bun build src/server.ts --target=bun --outdir=dist --minify --sourcemap=linked --external sharp"
300
+ ```
301
+
302
+ In `react-router`, in `vite.config.ts`:
303
+
304
+ ```ts
305
+ export default defineConfig({
306
+ ssr: { external: ['sharp'] },
307
+ plugins: [tailwindcss(), reactRouter(), alxia()],
308
+ });
309
+ ```
310
+
311
+ Then in the `Dockerfile`, a stage for the production dependencies, copied
312
+ beside the build:
313
+
314
+ ```dockerfile
315
+ # Before the build stage.
316
+ FROM oven/bun:1 AS production-dependencies
317
+ WORKDIR /app
318
+ COPY package.json bun.lock* bunfig.toml* ./
319
+ RUN bun install --frozen-lockfile --production
320
+
321
+ # In the final stage, before USER bun.
322
+ COPY --from=production-dependencies /app/node_modules ./node_modules
323
+ ```
324
+
325
+ `grep '^import' dist/server.js` (or `build/server/index.js`) lists what
326
+ the bundle still imports: Node's and Bun's modules, and the packages left
327
+ external.
328
+
329
+ ### `error: … is linked against glibc (DT_NEEDED libm.so.6), but this Bun build uses musl.`
330
+
331
+ The container stops at startup, or at the first request that loads the
332
+ package, with the path of a `.node` file in `node_modules` and
333
+ `code: "ERR_DLOPEN_FAILED"`. The library named after `DT_NEEDED` is
334
+ whichever one the addon links first: `libc.so.6`, `libstdc++.so.6` or
335
+ another; the fix is the same.
336
+
337
+ **When:** a package is kept external and installed beside the build, as
338
+ the previous entry says, and it loads a native addon built for glibc
339
+ alone.
340
+
341
+ **Why:** the final stage is `oven/bun:1-alpine`, whose C library is
342
+ musl. The bundle is JavaScript, which runs the same there, but a `.node`
343
+ file is compiled against one C library, and one compiled for glibc
344
+ cannot load on musl, even with `gcompat`. A package that publishes a
345
+ musl build too, as `sharp` does, works: `bun install`, `--production` too,
346
+ puts both variants in `node_modules`, and the package picks musl's.
347
+
348
+ **Fix:** run the final stage on Debian's image, `oven/bun:1`, which
349
+ holds glibc. The build stages stay as they are:
350
+
351
+ ```dockerfile
352
+ # The final stage.
353
+ FROM oven/bun:1
354
+ ```
355
+
356
+ The image is about 200 MB larger.
357
+
257
358
  ### The project's `@alxia/*` are older than npm's latest
258
359
 
259
360
  **Symptom:** a fresh project declares `@alxia/core` at `^0.3.4` while npm
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alxia/create",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Start an alxia app: `bun create @alxia` writes an API with Zod, or React Router's official template served by alxia",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -41,7 +41,7 @@
41
41
  "devDependencies": {
42
42
  "@alxia/client": "^0.2.1",
43
43
  "@alxia/core": "^0.3.1",
44
- "@alxia/react-router": "^0.3.0",
44
+ "@alxia/react-router": "^0.4.0",
45
45
  "@types/bun": "^1.4.2",
46
46
  "zod": "^4.6.5"
47
47
  }
@@ -1,22 +1,26 @@
1
- # Bun runs the app's TypeScript as it is: no build stage, the image holds
2
- # src/ and the production dependencies. Pinned to Bun 1, the major alxia
3
- # supports.
1
+ # Bun bundles the app and its dependencies into dist/server.js, and the
2
+ # image holds dist/ alone: no node_modules, no src/. Pinned to Bun 1, the
3
+ # major alxia supports.
4
4
 
5
- # The production dependencies alone, for the final image.
6
- FROM oven/bun:1 AS production-dependencies
5
+ # Every dependency, then bun run build: dist/server.js and its source map.
6
+ FROM oven/bun:1 AS build
7
7
  WORKDIR /app
8
8
  COPY package.json bun.lock* bunfig.toml* ./
9
- RUN bun install --frozen-lockfile --production
9
+ RUN bun install --frozen-lockfile
10
+ COPY . .
11
+ RUN bun run build
10
12
 
11
- # The app and its production dependencies, run as Bun's own non-root user
12
- # with the start script's command, written out so that Bun is the
13
- # container's process. The server listens on PORT (3000); set API_KEY.
14
- FROM oven/bun:1
13
+ # The bundle, run as Bun's own non-root user with the start script's
14
+ # command, written out so that Bun is the container's process, and
15
+ # --no-install: with no node_modules, Bun would otherwise fetch a package
16
+ # the build left out from the registry at startup, where this fails. The
17
+ # server listens on PORT (3000); set API_KEY. On Alpine, about 200 MB
18
+ # lighter: the bundle is JavaScript, which runs the same on musl. A native
19
+ # addon built for glibc alone needs oven/bun:1 here.
20
+ FROM oven/bun:1-alpine
15
21
  WORKDIR /app
16
22
  ENV NODE_ENV=production
17
- COPY package.json ./
18
- COPY --from=production-dependencies /app/node_modules ./node_modules
19
- COPY src ./src
23
+ COPY --from=build /app/dist ./dist
20
24
  USER bun
21
25
  EXPOSE 3000
22
- CMD ["bun", "src/server.ts"]
26
+ CMD ["bun", "--no-install", "dist/server.js"]
@@ -39,21 +39,37 @@ bun run typecheck
39
39
 
40
40
  ## Build
41
41
 
42
- Bun runs the TypeScript as it is: `bun start` serves `src/server.ts`, with
43
- no build step. `bun run build` bundles the server and its dependencies into
44
- one file, for a host with Bun and no `node_modules`:
42
+ `bun run build` bundles the server and its dependencies into one file,
43
+ `dist/server.js`, minified, with its source map beside it, and `bun start`
44
+ runs it: what production and the image run.
45
45
 
46
46
  ```sh
47
- bun run build # dist/server.js
48
- bun dist/server.js
47
+ bun run build # dist/server.js and dist/server.js.map
48
+ bun start # bun dist/server.js
49
49
  ```
50
50
 
51
+ `bun start` before any build fails with `Module not found
52
+ "dist/server.js"`: build first. `dist/` needs Bun and nothing else, no
53
+ `node_modules`. Bun reads the
54
+ source map, so a stack trace names the lines of `src/`. `bun dev` and
55
+ `bun test` run the TypeScript as it is, with no build.
56
+
57
+ A dependency that cannot be bundled, a native addon, is left out with
58
+ `--external <name>` in the `build` script, and must then be installed
59
+ beside `dist/`.
60
+
51
61
  ## Docker
52
62
 
53
- The `Dockerfile` installs the production dependencies on `oven/bun:1` and
54
- runs `src/server.ts` as the image's non-root `bun` user. It installs with
55
- `--frozen-lockfile`, from the `bun.lock` that `bun install` wrote: commit
56
- it.
63
+ The `Dockerfile` builds in a stage of its own, on `oven/bun:1`, and runs
64
+ on `oven/bun:1-alpine`. The build stage
65
+ installs every dependency with `--frozen-lockfile`, from the `bun.lock`
66
+ that `bun install` wrote (commit it), and runs `bun run build`. The image
67
+ holds `dist/` alone, no `node_modules` and no `src/`, and runs
68
+ `bun --no-install dist/server.js` as its non-root `bun` user: a package
69
+ missing from the bundle fails at startup instead of being fetched from
70
+ npm. `src/server.ts` stops the app on `SIGTERM`, so `docker stop` is
71
+ immediate. A native addon built for glibc alone does not load on
72
+ Alpine: put the final stage back on `oven/bun:1`.
57
73
 
58
74
  ```sh
59
75
  docker build -t my-api .
@@ -4,8 +4,8 @@
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "dev": "bun --watch src/server.ts",
7
- "build": "bun build src/server.ts --target=bun --outdir=dist",
8
- "start": "bun src/server.ts",
7
+ "build": "bun build src/server.ts --target=bun --outdir=dist --minify --sourcemap=linked",
8
+ "start": "bun dist/server.js",
9
9
  "test": "bun test",
10
10
  "typecheck": "tsc --noEmit"
11
11
  },
@@ -1,4 +1,18 @@
1
1
  import { app } from './app';
2
2
 
3
+ // Stop as the platform asks. In a container Bun is process 1, which a
4
+ // signal with no handler does not stop: `docker stop` would wait.
5
+ for (const signal of ['SIGINT', 'SIGTERM'] as const) {
6
+ process.once(signal, () => {
7
+ void app.stop().then(
8
+ () => process.exit(0),
9
+ (error: unknown) => {
10
+ console.error(error);
11
+ process.exit(1);
12
+ },
13
+ );
14
+ });
15
+ }
16
+
3
17
  const server = app.listen(Number(Bun.env['PORT'] ?? 3000));
4
18
  console.log(`listening on ${server.url}`);
@@ -1,11 +1,7 @@
1
- # Bun builds the app and runs it: build/server/index.js is alxia's server,
2
- # which runs on Bun. Pinned to Bun 1, the major alxia supports.
3
-
4
- # The production dependencies alone, for the final image.
5
- FROM oven/bun:1 AS production-dependencies
6
- WORKDIR /app
7
- COPY package.json bun.lock* bunfig.toml* ./
8
- RUN bun install --frozen-lockfile --production
1
+ # Bun builds the app, and the image holds build/ alone: alxia's plugin
2
+ # bundles every dependency into build/server/index.js, alxia's server, which
3
+ # runs on Bun with no node_modules. Pinned to Bun 1, the major alxia
4
+ # supports.
9
5
 
10
6
  # Every dependency, then react-router build: build/client and
11
7
  # build/server/index.js.
@@ -16,14 +12,17 @@ RUN bun install --frozen-lockfile
16
12
  COPY . .
17
13
  RUN bun run build
18
14
 
19
- # The build and the production dependencies, run as Bun's own non-root
20
- # user. The server listens on PORT (3000) and HOST (0.0.0.0).
21
- FROM oven/bun:1
15
+ # The build, run as Bun's own non-root user with the start script's
16
+ # command, and --no-install: with no node_modules, Bun would otherwise
17
+ # fetch a package the build left out from the registry at startup,
18
+ # where this fails.
19
+ # The server listens on PORT (3000) and HOST (0.0.0.0). On Alpine, about
20
+ # 200 MB lighter: build/ is JavaScript, which runs the same on musl. A
21
+ # native addon built for glibc alone needs oven/bun:1 here.
22
+ FROM oven/bun:1-alpine
22
23
  WORKDIR /app
23
24
  ENV NODE_ENV=production
24
- COPY package.json ./
25
- COPY --from=production-dependencies /app/node_modules ./node_modules
26
25
  COPY --from=build /app/build ./build
27
26
  USER bun
28
27
  EXPOSE 3000
29
- CMD ["bun", "build/server/index.js"]
28
+ CMD ["bun", "--no-install", "build/server/index.js"]
@@ -66,7 +66,7 @@ The containerized application can be deployed to any platform that supports Dock
66
66
 
67
67
  ### DIY Deployment
68
68
 
69
- The build is production-ready: `bun run start` runs `build/server/index.js` on Bun.
69
+ The build is production-ready and self-contained: `bun run start` runs `build/server/index.js` on Bun, with every dependency bundled into it, so `build/` needs no `node_modules`.
70
70
 
71
71
  Make sure to deploy the output of `bun run build`
72
72