Deployment
Deploying an Ecopages application depends on whether you are using only static features or if you've added API routes and server-side logic.
Production Build
Before deploying, create a production build. This processes your assets, pre-renders static pages, and writes public output to dist/client/ and private server output, when needed, to dist/server/.
pnpm run buildThis command will:
- Set
NODE_ENV=production. - Clean the
distoutput directory. - Generate static HTML for all files in
src/pages. - Write
robots.txt(and optionalsitemap.xmlwhen enabled — see Sitemap). - Process images and CSS.
- Bundle the server into
dist/server/when the app registers API or WebSocket handlers or has endpoint files (*.api.ts,.api.tsx,.api.jsor.api.jsx) in its pages directory. A static-only app has no server bundle. The build first checks each endpoint file's exports and fails, naming the file, on a broken one; see Endpoint Files.
Static Hosting
If your site does not use API routes or programmatic server logic, point your static host at dist/client/. The private server output is in the sibling dist/server/ directory and is never inside the static host's document root. ecopages start cannot run a static-only build because it has no server bundle; use ecopages preview to serve it locally.
- Vercel / Netlify: Connect your repository, set the build command to
pnpm run build, and set the output directory todist/client. - GitHub Pages: Use a GitHub Action to run
pnpm run buildand deploydist/client/.
Server Hosting (Node/Bun)
If your application uses API routes or dynamic server logic, you need a runtime environment compatible with the adapter you configured (Bun or Node).
1. Using pnpm run start
After building, you can start the production server. This needs a server bundle, so it works only for apps that register API or WebSocket handlers or have endpoint files:
pnpm run start2. What to deploy
ecopages build writes public output into dist/client/ and the server into dist/server/. The server code holds no absolute paths of the machine that built it: it loads packages from node_modules through bare imports or paths relative to dist/server/. You can build in one place and run in another, such as a CI runner and a container image with a different root. Copy these entries of your project and keep their relative positions:
dist/: the public files inclient/, plus the server bundle, its config, the route list, and emitted server modules inserver/.node_modules/: the install the build ran against.
Copy the node_modules the build used, installed on the same operating system and CPU architecture as the server, because packages with native bindings install files for one platform. Some imports point into the package manager's own folders, such as node_modules/.pnpm/<name>@<version>_<peers>/, so a fresh install only works when it produces the same folder names: same lockfile, same package manager version and same layout settings (for pnpm node-linker, virtual-store-dir and virtual-store-dir-max-length; for Bun the linker).
Start the server from that folder with node dist/server/app.mjs, or bun dist/server/app.mjs. Both, like ecopages start, check that dist/ is complete and load the routes, server modules and config compiled into dist/server/. The server does not compile Pages; when runtime asset setup copies an available source public directory, those files go into dist/client/.
Production import.meta.url, import.meta.dirname and import.meta.filename point at the emitted files in dist/server/. App code that reads a file next to its module must declare it in serverFiles so the build copies it into dist/server/ by basename, or move the file to public/ for the browser to fetch. Development still rewrites import.meta to source locations.
Yarn Plug'n'Play installs are not supported for a server you move after building: their packages live in .zip archives, so the build bundles them instead of importing them from node_modules.
In a monorepo, the packages live in the workspace root node_modules, outside your app folder. Build from a standalone copy of the app instead, for example the output of pnpm deploy. ecopages build warns with one line per server output file, such as dist/server/app.bundle.mjs, that imports packages from outside the app folder, naming the first five and counting the rest.
3. Docker Deployment
You can containerize the app with the official Node image. To build in one stage and run in another, copy the entries listed above. Both stages use the same base image, so native bindings match. Add node_modules and dist to .dockerignore, or COPY . . overwrites the image's install with the one from your machine:
FROM node:24-slim AS build
WORKDIR /build
RUN corepack enable
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
COPY . .
RUN pnpm run build
FROM node:24-slim
WORKDIR /srv/app
ENV NODE_ENV=production ECOPAGES_HOSTNAME=0.0.0.0
COPY --from=build /build/node_modules ./node_modules
COPY --from=build /build/dist ./dist
EXPOSE 3000
CMD ["node", "dist/server/app.mjs"]To run on Bun instead, use the official Bun image. That process runs on Bun through Node APIs.
FROM oven/bun:latest AS build
WORKDIR /build
COPY package.json ./
RUN bun install
COPY . .
RUN bun run build
FROM oven/bun:latest
WORKDIR /srv/app
ENV NODE_ENV=production ECOPAGES_HOSTNAME=0.0.0.0
COPY --from=build /build/node_modules ./node_modules
COPY --from=build /build/dist ./dist
EXPOSE 3000
CMD ["bun", "dist/server/app.mjs"]4. VPS / PaaS (Railway, Fly.io)
Deploy dist/ and node_modules/ as listed above. Point the platform at dist/server/app.mjs, or run ecopages start from that folder. Do not use app.ts as the production entry.
Environment Check
Before going live, check that these environment variables are set on your hosting provider:
| Variable | Recommended Value |
|---|---|
NODE_ENV | production |
ECOPAGES_BASE_URL | Your production domain (e.g., https://example.com) |
ECOPAGES_PORT | The port required by your host (e.g., 8080) |
ECOPAGES_HOSTNAME | 0.0.0.0 (for Docker/Cloud environments) |