Deployment
px build compiles your app into a self-contained dist/ that runs with Bun and nothing else: no src/, no node_modules. Copy dist/ to your server, run it, and you're live.
Build
px buildA scaffolded app also wires a build script that runs any asset step (such as compiling your stylesheet) before px build, so bun run build is the one command for a full production build.
What build produces
dist/
├── client/ # static assets, served at /
│ ├── index.html # each page, prerendered to static HTML
│ ├── <route>/index.html # one prerendered page per route
│ ├── styles/global.css # src/styles/global.css, if the app has one
│ ├── styles.css # public/styles.css (Sass or Tailwind output), if any
│ ├── styles-<hash>.css # extracted component css`` styles, if any
│ └── <entry>-<hash>.js # the client bundle
└── server/
└── index.js # the self-contained production serverTwo halves: client/ holds static assets and a prerendered HTML page for each route, and server/index.js is a small standalone server that serves them, runs your guards, and answers your API routes.
Because pages are prerendered, a request for a page returns real HTML on the first byte, so crawlers and first paint get content, not an empty shell that waits for JavaScript.
Run
px servepx serve starts dist/server/index.js from your project directory, and every scaffolded project has a start script for it, so bun run start works too. On a host where only dist/ is present, run the file with Bun directly; it's the same server:
bun dist/server/index.jsIt runs from dist/ alone, with no source and no dependencies, serving the prerendered pages, static assets, and any API routes. The port resolves the same way in both: a --port= flag, then the PORT environment variable, then your config's server.port, then 2500.
Config is fixed at build time
build reads palinx.config.ts once and builds its values into the server: port, TLS, CORS, headers, and rate limits. The running server doesn't read the config file, so after changing it, or an environment variable it reads, run build again.
Set those variables on the machine that builds. If your config turns TLS on only when a certificate variable is set, build with that variable present, or you'll ship a server that speaks plain HTTP. The certificate and key files are read when the server starts, relative to the directory you start it from.
PALINX_CSRF_SECRET is the exception. The secret is never written into dist/; the server reads it from its environment every time it starts, and refuses to start if your config used a secret at build time but the variable is missing now. Set it on the host, or put it in a .env file in the directory you start the server from.
Errors in production
An API handler that throws gets a generic {"error": "Internal server error"} with status 500, and the full error goes to the server's log. Errors meant for the caller, with a 4xx status, keep their status and message. API endpoints covers the rules.
Errors that happen outside a request, such as a promise a handler started without awaiting that later rejects, or a throw inside a timer, don't stop the server. Each one is logged with its stack, on a line starting [palinx] unhandled promise rejection (the server keeps running) or [palinx] uncaught exception (the server keeps running), and the server keeps serving. When the same error repeats, the production server logs it in full once, then a single line such as [palinx] …and 499 more like it in the last 60s.
If your server runs under a supervisor that restarts it (systemd, a container platform), you may prefer it to exit instead. Set server.exitOnUncaughtException: true in palinx.config.ts and rebuild: after an uncaught exception the server logs it, stops accepting new connections, lets the ones already open finish their requests (for up to 5 seconds), and exits with status 1. Unhandled promise rejections are still only logged.
Deploy
The whole deploy is: build, copy dist/ to the host, run it.
px build
scp -r dist/ user@host:/srv/my-app/
# on the host:
bun /srv/my-app/dist/server/index.jsRun it behind your process manager and reverse proxy of choice. Nothing else from your project needs to ship. If the app has WebSocket routes, the proxy has to pass WebSocket connections through and forward the original Host header; see WebSockets.
Containers
The self-contained dist/ makes for a minimal image: copy dist/, run it with Bun.
FROM oven/bun:1.3
WORKDIR /app
COPY dist/ ./dist/
EXPOSE 2500
CMD ["bun", "dist/server/index.js"]Build your dist/ first (px build), then build the image against it. There's no node_modules layer and no source: the image is essentially Bun plus your dist/. The server honors PORT, so a platform that injects one needs no extra configuration.
The production server listens on every interface (0.0.0.0) by default, which a container needs to be reachable through its published port, so the image above works as is. If your config sets server.host to a loopback address such as 127.0.0.1, px build warns that the server will be unreachable from outside the container; override it when the container starts with PALINX_HOST=0.0.0.0. Going the other way, set PALINX_HOST=127.0.0.1 (or pass --host 127.0.0.1) when a reverse proxy on the same machine is the only thing that should reach the server.
px dev is different: it listens on 127.0.0.1 by default, which inside a container means nothing outside it can connect. Run it there with px dev --host. Another container that reaches it by its service name (say web in a Compose file) sends Host: web, which dev refuses with a 403 until you allow the name: px dev --host --allowed-hosts=web.
Persisting scheduler data
If your app uses scheduled tasks, the scheduler keeps its state in a small SQLite database. By default that database lives under your working directory (.palinx/scheduler.db), which on an ephemeral container filesystem is lost on every restart. Point it at a persistent volume with PALINX_DB_PATH:
PALINX_DB_PATH=/data/scheduler.db bun dist/server/index.jsMount /data as a persistent volume and scheduled-task state survives restarts and redeploys. This matters specifically for apps with a src/scheduled/ directory: the built server runs the scheduler just as px dev does, so a containerized deployment needs the durable path or it silently loses schedule state. An app with no scheduled tasks has no scheduler database and nothing to persist.
Next
That completes the framework tour: routing, rendering, data, access, scheduling, testing, and shipping. Head back to any guide from the sidebar, or start building.