Skip to content

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.

bash
npx cell build                                # preset node: .cell/output
npx cell build --preset cloudflare
npx cell deploy --preset fc --dry-run         # the plan; nothing is uploaded

The 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 ​

PresetOutputRun locallyDeploy
node (default)Node.js HTTP server, server/index.mjscell previewCopy .cell/output anywhere Node.js 18+, Bun or Deno runs
cliA command-line program on Cell DI, no HTTP servercell previewDistribute the file or an executable
cloudflareWorker + static assets + wrangler.jsoncell preview (workerd through Wrangler)cell deploy --preset cloudflare
fcAlibaba Cloud Function Compute 3.0, custom runtimecell previewcell deploy --preset fc
scfTencent Cloud SCF web functioncell previewcell deploy --preset scf
lambdaAWS Lambda with a function URLcell previewcell deploy --preset lambda
vercelVercel Build Output API (.vercel/output)cell previewcell deploy --preset vercel

The output ​

text
.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 read

The 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.

OptionResult
--singleThe 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:3000 by default. In a container set CELL_SERVER__HOST=0.0.0.0; when the platform gives the port in PORT, set CELL_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 check lists settings that are unsafe in production (CELL_SECURITY_* warnings: the development cookie key, a short jwt.secret...). A production start (NODE_ENV=production, which cell build writes into the server bundle) logs the same warnings; set securityChecks: fail (or CELL_SECURITY_CHECKS=fail) to refuse to start instead, off to 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. 4xx HttpError messages are sent. web.errors.exposeMessages overrides this.

A container image ​

dockerfile
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.

PresetCredentials (environment)Platform options in cell.config.ts
cloudflareCLOUDFLARE_API_TOKEN (and CLOUDFLARE_ACCOUNT_ID), or npx wrangler loginwrangler.jsonc in the project root: name, vars, bindings, routes
fcALIBABA_CLOUD_ACCESS_KEY_ID, ALIBABA_CLOUD_ACCESS_KEY_SECRETfc: { name, region, memory, cpu, timeout, concurrency, env }
scfTENCENTCLOUD_SECRET_ID, TENCENTCLOUD_SECRET_KEYscf: { name, region, namespace, memory, timeout, env }
lambdaAWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, or AWS_PROFILElambda: { name, region, architecture, memory, timeout, role, env }
vercelVERCEL_TOKEN, or npx vercel loginvercel: { project, scope, environment, memory, maxDuration, regions }
ts
// 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:

ts
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.