Building and deploying
Your code does not change between platforms: a preset, chosen at build time, decides the shape of the output and how it is deployed.
npx cell build # preset node: .cell/output
npx cell build --preset cloudflare
npx cell deploy --preset fc --dry-run # the plan; nothing is uploadedThe preset comes from --preset, else preset in cell.config.ts, else the CELL_PRESET variable, else the CI platform (cloudflare in Workers Builds, vercel on Vercel), else node. npx cell info shows which one applies and why.
Presets
| Preset | Output | Run locally | Deploy |
|---|---|---|---|
node (default) | Node.js HTTP server, server/index.mjs | cell preview | Copy .cell/output anywhere Node.js 18+, Bun or Deno runs |
cli | A command-line program on Cell DI, no HTTP server | cell preview | Distribute the file or an executable |
cloudflare | Worker + static assets + wrangler.json | cell preview (workerd through Wrangler) | cell deploy --preset cloudflare |
fc | Alibaba Cloud Function Compute 3.0, custom runtime | cell preview | cell deploy --preset fc |
scf | Tencent Cloud SCF web function | cell preview | cell deploy --preset scf |
lambda | AWS Lambda with a function URL | cell preview | cell deploy --preset lambda |
vercel | Vercel Build Output API (.vercel/output) | cell preview | cell deploy --preset vercel |
The output
.cell/output/
├── server/index.mjs # the backend: one ES module with every dependency inlined
├── public/ # the frontend build, when there is one
└── manifest.json # preset, format, targets: what cell preview and cell deploy readThe output does not need the project's node_modules. Packages that cannot be bundled (native modules, build.external in cell.config.ts) are copied into server/node_modules.
| Option | Result |
|---|---|
--single | The frontend is embedded in server/index.mjs: one file runs the whole application |
--compile [bun|node|deno] | An executable .cell/output/app that needs no Node.js on the host |
-m <mode> | Also reads cell.<mode>.yml (production is always active in builds) |
--sourcemap, --format cjs, --out-dir <dir> | Source maps, CommonJS output, another directory |
Configuration in production
- Backend configuration is evaluated when the server starts. The same build runs everywhere; give each environment its values through environment variables, either referenced in
cell.yml("${env.DATABASE_URL}") or as overrides (CELL_SERVER__PORT=8080). See configuration. - Never mark a secret public. Configuration is server-only unless a schema marks it public or it is under
app.public; frontend values are evaluated at build time and shipped to browsers. - The server listens on
127.0.0.1:3000by default. In a container setCELL_SERVER__HOST=0.0.0.0; when the platform gives the port inPORT, setCELL_SERVER__PORT='${env.PORT}'. - Set your own
web.cookies.keys(signing key of cookies and sessions), e.g.CELL_WEB__COOKIES__KEYS='["..."]'. - Security checks.
cell checklists settings that are unsafe in production (CELL_SECURITY_*warnings: the development cookie key, a shortjwt.secret...). A production start (NODE_ENV=production, whichcell buildwrites into the server bundle) logs the same warnings; setsecurityChecks: fail(orCELL_SECURITY_CHECKS=fail) to refuse to start instead,offto skip them. - Error messages. In production a server error answers with its status text only (
Internal Server Error); its message and stack go to the log. 4xxHttpErrormessages are sent.web.errors.exposeMessagesoverrides this.
A container image
FROM node:22-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npx cell build
FROM node:22-alpine
ENV NODE_ENV=production
ENV CELL_SERVER__HOST=0.0.0.0
WORKDIR /app
COPY --from=build --chown=node:node /app/.cell/output ./
USER node
EXPOSE 3000
CMD ["node", "server/index.mjs"]cell deploy
cell deploy builds with the preset, makes a plan (the resources it will create or update), then applies it. --dry-run stops after the plan and works without credentials; --skip-build deploys the existing output.
| Preset | Credentials (environment) | Platform options in cell.config.ts |
|---|---|---|
cloudflare | CLOUDFLARE_API_TOKEN (and CLOUDFLARE_ACCOUNT_ID), or npx wrangler login | wrangler.jsonc in the project root: name, vars, bindings, routes |
fc | ALIBABA_CLOUD_ACCESS_KEY_ID, ALIBABA_CLOUD_ACCESS_KEY_SECRET | fc: { name, region, memory, cpu, timeout, concurrency, env } |
scf | TENCENTCLOUD_SECRET_ID, TENCENTCLOUD_SECRET_KEY | scf: { name, region, namespace, memory, timeout, env } |
lambda | AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, or AWS_PROFILE | lambda: { name, region, architecture, memory, timeout, role, env } |
vercel | VERCEL_TOKEN, or npx vercel login | vercel: { project, scope, environment, memory, maxDuration, regions } |
// cell.config.ts
import { defineConfig } from '@celljs/cli';
export default defineConfig({
preset: 'fc',
fc: { region: 'cn-shanghai', memory: 512, env: { DATABASE_URL: process.env.DATABASE_URL ?? '' } },
});The function platforms (fc, scf, lambda) call the cloud APIs directly: no vendor SDK or CLI is installed. Wrangler and the Vercel CLI are used from the project when installed, otherwise through npx. Every failure has a code: CELL_DEPLOY_CREDENTIALS_MISSING, CELL_DEPLOY_UNAUTHORIZED, CELL_DEPLOY_PACKAGE_TOO_LARGE, ...
Platform details (custom domains, runtimes, package size limits, what each deployer creates) are in the CLI reference.
Hooks
Shell commands on toolchain events, for steps such as code generation:
export default defineConfig({
hooks: {
'build:before': 'prisma generate', // a failure cancels the build
'output:done': 'cp extra/* "$CELL_OUT_DIR/server/"',
},
});Extensions that need code (Vite plugins, extra output files, checks, MCP tools) are toolchain modules; see the CLI reference.