A production Dockerfile: builds, caching and deployment on Inquir

Build a Node.js and TypeScript image with separate stages, a lockfile, a non-root user and a repeatable deployment checklist for Inquir Compute.

A production Dockerfile: builds, caching and deployment on Inquir

An application runs on a laptop, but the cloud build cannot find a package. Or changing one handler triggers a full dependency installation. A Dockerfile often explains both problems: it packages accidental workstation state instead of describing a repeatable application build.

This guide uses Node.js and TypeScript. It assumes npm run build creates dist/server.js, the server listens on 0.0.0.0:3000, and /healthz returns a successful HTTP response. These are requirements of this example. Adapt the paths and port before running the commands against a different project.

Separate building from execution

The TypeScript compiler belongs in the build environment; the process running compiled JavaScript usually does not need it. Separate stages make the contents of the final image explicit:

FROM node:22-bookworm-slim AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY tsconfig.json ./
COPY src ./src
RUN npm run build

FROM node:22-bookworm-slim AS runtime
WORKDIR /app
ENV NODE_ENV=production
COPY package.json package-lock.json ./
RUN npm ci --omit=dev && npm cache clean --force
COPY --from=build --chown=node:node /app/dist ./dist
USER node
EXPOSE 3000
CMD ["node", "dist/server.js"]

If your build uses templates, Prisma, static assets or configuration outside src, copy those inputs explicitly as well. Inspect npm lifecycle scripts: a prepare script that invokes the compiler cannot run in the runtime stage without development dependencies. Fix the project structure rather than indiscriminately disabling install scripts, which native packages may require.

Both stages use the same base system. Native modules compiled in one environment may fail to load in another. For tighter control, pin a verified base-image digest and schedule deliberate updates. Pinning a digest does not automatically install later security fixes.

Understand what the cache can reuse

Dependency manifests are copied before source code. Editing a handler should not require reinstalling packages when the lockfile is unchanged and the builder retains its cache. A remote builder may start without that cache, so this is an optimization opportunity, not a promised build duration.

npm ci helps expose disagreement between the manifest and lockfile. Commit both files. Avoid copying local node_modules, which can contain binaries for a different operating system or architecture. See Docker’s build recommendations for the underlying build-stage and cache principles.

Keep the build context intentional

Place a .dockerignore beside the Dockerfile:

.git
node_modules
dist
coverage
.env
.env.*
*.log

Excluding dist is intentional because this image compiles its own output. A workflow packaging an existing artifact needs a different rule. Also inspect certificates, database dumps and directories containing user uploads. This list cannot anticipate every sensitive filename in your repository.

Do not put registry passwords or database credentials in ARG, ENV or COPY. Docker provides build secrets, but passing them depends on the build environment. This guide does not assume that Inquir automatically forwards local Docker build secrets. Configure a supported authentication path in your builder before adding private packages.

Run the actual image locally

docker build -t inquir-api-check .
docker run --rm -p 127.0.0.1:3000:3000 inquir-api-check

In another terminal, request http://127.0.0.1:3000/healthz and a real API route. Binding the published port to localhost is deliberate. Verify that startup does not need the TypeScript compiler, files from your laptop or write access to /app. The node user does not automatically own arbitrary directories.

Test the same entry point used by CMD. Running a development server inside a container is not equivalent to running the built artifact. If your application needs configuration, document which values are required at startup and which are safe defaults; keep real secrets out of the example image.

Deploy on Inquir Compute

After installing and authenticating the CLI, run these commands from the project directory:

inquir apps create ts-api --ingress public --port 3000 --health-path /healthz
inquir apps deploy ts-api --promote
inquir apps status ts-api
inquir apps logs ts-api --no-follow --tail 100

Run create once. Obtain the public address from status; the example name does not imply a predetermined domain. Configure production secrets through the application’s variable settings rather than baking them into its image.

Verify an actual API response at the assigned address as well as the health check. A successful build proves that an image was produced. It does not validate database connectivity, third-party credentials or the user’s workflow. If the image builds but the service is unavailable, follow the deployment troubleshooting guide.