DEV Community

Ahmad Karawash
Ahmad Karawash

Posted on

Amoxi: From laptop to production

Run on every core, recover from crashes, configure without leaking secrets, ship a Docker image, and the checklist to go through before real users arrive.

The app from the previous articles runs nicely under npm run dev. Production asks different questions. What happens when a process crashes? Where do the API keys go? How does a load balancer know the app is ready? This article answers them in order.

Build once, start anywhere

npm run build
Enter fullscreen mode Exit fullscreen mode
npm start
Enter fullscreen mode Exit fullscreen mode

npm run build runs amoxi build (wiring, validators, browser bundles, compressed assets) and then the TypeScript compiler. npm start runs amoxi start, which switches to the production profile and starts one worker per CPU core:

{"level":"info","msg":"[cluster] started 16 worker(s) for dist/src/main.js"}
{"level":"info","msg":"Startup completed in 131ms (framework: 36.81ms, 3 beans, 0 eager, 4 routes, profile production). Server Running: http://localhost:8080"}
Enter fullscreen mode Exit fullscreen mode

Logs are now one JSON object per line, ready for any log service. Each request line carries a requestId, the status and the duration. Use --workers 2 to choose the number yourself, for example in a container with two CPUs.

Production also changes some defaults: cookies are Secure over HTTPS, HSTS is on, error responses hide details, and the OpenAPI document and management endpoints are not served.

Crashes are not outages

Kill one worker hard and watch the log:

{"msg":"[cluster] worker 2004 exited (SIGKILL); starting a replacement"}
{"msg":"Startup completed in 98ms ... profile production"}
Enter fullscreen mode Exit fullscreen mode

The other workers kept serving while the replacement started, about 100 ms later. If workers crash in a loop (more than 10 a minute), restarts slow down and a warning is logged, so a bad deploy is visible without taking the site down. If workers fail while starting, for example because of a missing setting, amoxi start stops with the error instead of looping.

One more production difference: if the port is already taken, the app stops instead of quietly moving to another port, because a load balancer expects the configured one.

Port 8080 is already in use by another program. Stop it, or choose another port with SERVER_PORT=<port> (or server.port in application.json).
Enter fullscreen mode Exit fullscreen mode

Configuration and secrets

Settings come from five places. Later ones win:

  1. application.json: defaults, committed to git.
  2. application-production.json: production overrides, committed. The starter uses it for JSON logs.
  3. .env files: local values, git-ignored. .env.example documents them and is committed.
  4. Environment variables: what your hosting platform sets.
  5. --key=value arguments.

Any key can be set as an environment variable: capitals, dots as underscores, dashes left out. server.port is SERVER_PORT and server.max-body-size is SERVER_MAXBODYSIZE.

The rule for secrets is simple: API keys, database URLs, MCP tokens and the session secret go in environment variables or .env files, never in application*.json. The starter's .gitignore and .dockerignore already exclude .env files.

The settings most apps need in production:

Variable Why
AMOXI_SESSION_SECRET Keeps people signed in across deploys (32+ random characters, the same on every server)
APP_URL Your public address, for absolute links in llms.txt and AI views
ANTHROPIC_API_KEY or OPENAI_API_KEY A real model for your agents
DATABASE_URL When agent runs are stored in PostgreSQL
SERVER_TRUSTPROXY true (or the number of proxies) only behind a proxy you control

To generate a secret, in bash or PowerShell:

node -e 'console.log(require(`crypto`).randomBytes(32).toString(`hex`))'
Enter fullscreen mode Exit fullscreen mode

Why does the session secret matter? Sessions are encrypted with a key that amoxi build creates once per checkout. A CI job or a Docker build is a fresh checkout, so without a fixed secret every deploy signs everyone out. Because that key ends up in the build output, keep build output and images private, as you would with a Next.js build.

Docker

Every app created by create-amoxi-app already has a Dockerfile. It builds in one stage and runs in a second, small one:

FROM node:22-alpine AS build
WORKDIR /app
COPY . .
RUN npm ci && npm run build && npm prune --omit=dev

FROM node:22-alpine
ENV NODE_ENV=production
WORKDIR /app
COPY --from=build --chown=node:node /app/package.json /app/application*.json ./
COPY --from=build --chown=node:node /app/node_modules ./node_modules
COPY --from=build --chown=node:node /app/dist ./dist
COPY --from=build --chown=node:node /app/public ./public
RUN mkdir -p /app/data && chown node:node /app/data
USER node
EXPOSE 8080
HEALTHCHECK --interval=10s --timeout=3s --start-period=5s \
  CMD wget -qO- http://127.0.0.1:8080/amoxi/health/ready || exit 1
CMD ["node", "node_modules/amoxi/bin/amoxi.js", "start", "dist/src/main.js", "--workers", "auto"]
Enter fullscreen mode Exit fullscreen mode

(Simplified: the real file also detects pnpm, Yarn and Bun lockfiles.) The app runs as the unprivileged node user, data/ is writable for agent runs, and Docker itself checks readiness. Build and run it:

docker build -t my-app .
Enter fullscreen mode Exit fullscreen mode
docker run -p 8080:8080 -e AMOXI_SESSION_SECRET=your-secret my-app
Enter fullscreen mode Exit fullscreen mode

If your agents use the default file store, mount a volume on /app/data so runs and conversations survive a new container. With several containers, use PostgreSQL instead (article 6).

Health checks and graceful shutdown

Two endpoints are always there:

GET /amoxi/health/live   -> {"status":"UP"}  (the process is alive)
GET /amoxi/health/ready  -> {"status":"UP"}  (send traffic here)
Enter fullscreen mode Exit fullscreen mode

Readiness answers 503 while the app is starting or shutting down. On SIGTERM, which is what Docker, Kubernetes and most platforms send, the app turns readiness to 503, finishes the requests in flight (up to server.shutdown-timeout, 10 seconds by default), runs your @PreDestroy hooks and exits. On Kubernetes, also set server.shutdown-delay=5000 so the load balancer notices before connections close.

Build in CI, not on the server

A good pipeline:

npm ci
Enter fullscreen mode Exit fullscreen mode
npm run build
Enter fullscreen mode Exit fullscreen mode
npm test
Enter fullscreen mode Exit fullscreen mode

Then ship dist/, public/, node_modules/ and the application*.json files, or the Docker image. Do not commit dist/, public/_amoxi/ or src/generated/; the starter's .gitignore already excludes them. Because every wiring mistake from article 2 fails amoxi build, a green pipeline means the app will start.

One detail for rolling deploys: browser bundles have content-hashed names and are cached for a year. Keep the previous build's files available for a while during a rolling deploy, so pages that are still open never ask for a bundle that is gone. Next.js calls this version skew, and the same care applies to any framework that hashes its assets.

The checklist

Before the first real user:

  • TLS: put a reverse proxy or load balancer in front for HTTPS and HTTP/2, as with any Node.js server.
  • Proxy headers: set server.trust-proxy only when a proxy you control sets X-Forwarded-For. Otherwise clients could fake their address.
  • Health checks: point the load balancer at /amoxi/health/ready.
  • Secrets: keep AMOXI_SESSION_SECRET, API keys and tokens in the environment, never in git.
  • Public URL: set APP_URL.
  • Caching: use @Cached only for content that is the same for every visitor (article 4).
  • CSP: if you add analytics, fonts or other third-party scripts, extend server.csp instead of turning it off.
  • Limits: review the 1 MB body limit, upload limits and the 30-second request timeout for your workload.
  • Agents: decide which tools outside clients may call (expose: false), and give each MCP or A2A caller its own token (article 6).
  • Logs: forward X-Request-Id from your proxy so one id follows a request across services. Error pages show it to users, so a support request can be matched to the log line.
  • Upgrades: after upgrading amoxi, run amoxi build again; the generated code must match the runtime.

Evaluating Amoxi for your team

If you are deciding whether to adopt Amoxi, you do not have to take these articles at their word. Clone the repository and run its verification suite:

npm ci
Enter fullscreen mode Exit fullscreen mode
npm run verify
Enter fullscreen mode Exit fullscreen mode

It builds everything, runs every test, installs the packed npm packages into fresh projects, creates apps from every template, starts the examples as production servers and attacks them (path traversal, CSRF, tampered sessions, oversized bodies and more), puts them under load, kills workers in the middle of requests and agent runs, and drives them in a real browser. It writes a report to verify-report/report.md and works the same on Windows, macOS and Linux. --quick runs the fast stages in a few minutes.

Amoxi is still before 1.0. It follows Semantic Versioning, and until 1.0 breaking changes can arrive in minor releases, always listed in the changelog.

That is the series

Over seven articles you built:

  1. A notes app with pages, an API and tests.
  2. Services wired at build time, with mistakes caught before deploy.
  3. Pages with streaming and islands that cost a few hundred bytes.
  4. Validation from types, sessions and the default protections.
  5. An AI support agent with tools, approvals and generative UI.
  6. Durable runs, and agents opened to other agents over MCP and A2A.
  7. A production deployment.

The documentation covers the rest: WebSockets, file uploads, internationalisation, interceptors, static export and the full configuration reference. The repository's examples show complete apps in banking, healthcare, retail and other sectors, all with fictional data.

If something in these articles did not work for you, open an issue on GitHub. That is the fastest way to make the next reader's path smoother.

Amoxi is open source under the Apache License 2.0. Code: github.com/karawash-projects/amoxi. Documentation: karawash-projects.github.io/amoxi.

Top comments (0)