Back to Journal
DevOps

Complete Guide to Kubernetes Production Setup with Typescript

A comprehensive guide to implementing Kubernetes Production Setup using Typescript, covering architecture, code examples, and production-ready patterns.

Muneer Puthiya Purayil 15 min read

TypeScript has become a viable choice for Kubernetes-native applications through the Node.js runtime, particularly for teams already invested in the TypeScript ecosystem. This guide covers containerization, deployment patterns, and production concerns specific to running TypeScript services on Kubernetes.

Container Image Optimization

Node.js images tend toward bloat. A disciplined multi-stage build keeps production images lean:

dockerfile
1FROM node:22-alpine AS builder
2WORKDIR /app
3COPY package.json package-lock.json ./
4RUN npm ci
5COPY tsconfig.json ./
6COPY src/ src/
7RUN npm run build
8 
9FROM node:22-alpine
10RUN addgroup -g 1001 -S appuser && adduser -S appuser -u 1001
11WORKDIR /app
12COPY --from=builder /app/dist ./dist
13COPY --from=builder /app/node_modules ./node_modules
14COPY package.json ./
15USER appuser
16EXPOSE 8080
17CMD ["node", "--max-old-space-size=384", "dist/server.js"]
18 

For further optimization, prune development dependencies:

dockerfile
1FROM node:22-alpine AS deps
2WORKDIR /app
3COPY package.json package-lock.json ./
4RUN npm ci --omit=dev
5 
6FROM node:22-alpine AS builder
7WORKDIR /app
8COPY package.json package-lock.json ./
9RUN npm ci
10COPY tsconfig.json ./
11COPY src/ src/
12RUN npm run build
13 
14FROM node:22-alpine
15RUN addgroup -g 1001 -S appuser && adduser -S appuser -u 1001
16WORKDIR /app
17COPY --from=deps /app/node_modules ./node_modules
18COPY --from=builder /app/dist ./dist
19COPY package.json ./
20USER appuser
21EXPOSE 8080
22CMD ["node", "--max-old-space-size=384", "dist/server.js"]
23 

The separate deps stage installs only production dependencies, excluding TypeScript, testing frameworks, and build tools from the runtime image. This typically reduces node_modules size by 40-60%.

Application Architecture

HTTP Server with Graceful Shutdown

typescript
1import { createServer, IncomingMessage, ServerResponse } from "node:http";
2import { Pool } from "pg";
3 
4interface AppState {
5 db: Pool;
6 isShuttingDown: boolean;
7}
8 
9const state: AppState = {
10 db: new Pool({
11 connectionString: process.env.DATABASE_URL,
12 max: 20,
13 min: 5,
14 idleTimeoutMillis: 300_000,
15 connectionTimeoutMillis: 5_000,
16 }),
17 isShuttingDown: false,
18};
19 
20const server = createServer(async (req: IncomingMessage, res: ServerResponse) => {
21 if (req.url === "/healthz" && req.method === "GET") {
22 res.writeHead(200);
23 res.end("ok");
24 return;
25 }
26 
27 if (req.url === "/readyz" && req.method === "GET") {
28 if (state.isShuttingDown) {
29 res.writeHead(503);
30 res.end("shutting down");
31 return;
32 }
33 try {
34 await state.db.query("SELECT 1");
35 res.writeHead(200);
36 res.end("ready");
37 } catch {
38 res.writeHead(503);
39 res.end("not ready");
40 }
41 return;
42 }
43 
44 // Route handling...
45 res.writeHead(404);
46 res.end("not found");
47});
48 
49server.listen(8080, () => {
50 console.log(JSON.stringify({ level: "info", msg: "server started", port: 8080 }));
51});
52 
53async function shutdown(signal: string): Promise<void> {
54 console.log(JSON.stringify({ level: "info", msg: "shutdown initiated", signal }));
55 state.isShuttingDown = true;
56 
57 server.close(async () => {
58 await state.db.end();
59 console.log(JSON.stringify({ level: "info", msg: "server stopped" }));
60 process.exit(0);
61 });
62 
63 setTimeout(() => {
64 console.log(JSON.stringify({ level: "error", msg: "forced shutdown" }));
65 process.exit(1);
66 }, 25_000);
67}
68 
69process.on("SIGTERM", () => shutdown("SIGTERM"));
70process.on("SIGINT", () => shutdown("SIGINT"));
71 

Node.js's single-threaded event loop handles concurrent connections efficiently for I/O-bound workloads. A single Node.js process can handle thousands of concurrent connections because it never blocks on I/O operations.

Using Fastify for Production

typescript
1import Fastify from "fastify";
2import { Pool } from "pg";
3import metricsPlugin from "fastify-metrics";
4 
5const pool = new Pool({
6 connectionString: process.env.DATABASE_URL,
7 max: 20,
8 min: 5,
9});
10 
11const app = Fastify({
12 logger: {
13 level: "info",
14 transport: undefined, // JSON output by default in production
15 },
16 trustProxy: true,
17});
18 
19await app.register(metricsPlugin, {
20 endpoint: "/metrics",
21 defaultMetrics: { enabled: true },
22 routeMetrics: { enabled: true },
23});
24 
25app.get("/healthz", async () => ({ status: "ok" }));
26 
27app.get("/readyz", async (request, reply) => {
28 try {
29 await pool.query("SELECT 1");
30 return { status: "ready" };
31 } catch {
32 reply.code(503);
33 return { status: "not ready" };
34 }
35});
36 
37interface OrderParams {
38 id: string;
39}
40 
41interface CreateOrderBody {
42 productId: string;
43 quantity: number;
44}
45 
46app.get<{ Params: OrderParams }>("/api/v1/orders/:id", async (request) => {
47 const { id } = request.params;
48 const result = await pool.query("SELECT * FROM orders WHERE id = $1", [id]);
49 return result.rows[0];
50});
51 
52app.post<{ Body: CreateOrderBody }>("/api/v1/orders", async (request, reply) => {
53 const { productId, quantity } = request.body;
54 const result = await pool.query(
55 "INSERT INTO orders (product_id, quantity, status) VALUES ($1, $2, $3) RETURNING *",
56 [productId, quantity, "pending"]
57 );
58 reply.code(201);
59 return result.rows[0];
60});
61 
62try {
63 await app.listen({ port: 8080, host: "0.0.0.0" });
64} catch (err) {
65 app.log.error(err);
66 process.exit(1);
67}
68 

Fastify is 2-3x faster than Express for JSON serialization workloads and includes schema validation, structured logging, and a plugin system. The fastify-metrics plugin exposes Prometheus-compatible metrics automatically.

Kubernetes Deployment

yaml
1apiVersion: apps/v1
2kind: Deployment
3metadata:
4 name: api-service
5spec:
6 replicas: 3
7 selector:
8 matchLabels:
9 app: api-service
10 template:
11 metadata:
12 labels:
13 app: api-service
14 annotations:
15 prometheus.io/scrape: "true"
16 prometheus.io/port: "8080"
17 prometheus.io/path: "/metrics"
18 spec:
19 terminationGracePeriodSeconds: 30
20 containers:
21 - name: api
22 image: registry.example.com/api-service:v1.5.0
23 ports:
24 - containerPort: 8080
25 name: http
26 env:
27 - name: NODE_ENV
28 value: "production"
29 - name: DATABASE_URL
30 valueFrom:
31 secretKeyRef:
32 name: api-secrets
33 key: database-url
34 - name: NODE_OPTIONS
35 value: "--max-old-space-size=384"
36 resources:
37 requests:
38 cpu: 250m
39 memory: 256Mi
40 limits:
41 memory: 512Mi
42 readinessProbe:
43 httpGet:
44 path: /readyz
45 port: http
46 initialDelaySeconds: 3
47 periodSeconds: 10
48 livenessProbe:
49 httpGet:
50 path: /healthz
51 port: http
52 initialDelaySeconds: 5
53 periodSeconds: 15
54 securityContext:
55 allowPrivilegeEscalation: false
56 readOnlyRootFilesystem: true
57 runAsNonRoot: true
58 runAsUser: 1001
59 capabilities:
60 drop: ["ALL"]
61 volumeMounts:
62 - name: tmp
63 mountPath: /tmp
64 volumes:
65 - name: tmp
66 emptyDir: {}
67 

Memory Configuration

--max-old-space-size=384 sets the V8 heap limit to 384MB inside a 512Mi container. The remaining 128MB covers V8 overhead (new space, code space, external allocations) and the Node.js runtime itself. Without this flag, V8 defaults to a heap limit based on the host machine's memory, not the container's limit, which leads to OOM kills.

Formula: max-old-space-size = container_memory_limit * 0.75

Need a second opinion on your DevOps pipelines architecture?

I run free 30-minute strategy calls for engineering teams tackling this exact problem.

Book a Free Call

Scaling with Node.js Cluster Mode

Node.js is single-threaded, so a single process only uses one CPU core. For multi-core utilization, use the cluster module or separate pod replicas:

typescript
1import cluster from "node:cluster";
2import { cpus } from "node:os";
3 
4const numWorkers = parseInt(process.env.WEB_CONCURRENCY || "0") || cpus().length;
5 
6if (cluster.isPrimary) {
7 console.log(JSON.stringify({ level: "info", msg: "primary started", workers: numWorkers }));
8 
9 for (let i = 0; i < numWorkers; i++) {
10 cluster.fork();
11 }
12 
13 cluster.on("exit", (worker, code) => {
14 console.log(JSON.stringify({
15 level: "warn",
16 msg: "worker died",
17 pid: worker.process.pid,
18 code,
19 }));
20 cluster.fork(); // Replace dead workers
21 });
22} else {
23 // Start the Fastify server in each worker
24 startServer();
25}
26 

In Kubernetes, you have two options for horizontal scaling:

  1. Single-process pods with more replicas. Simpler, and Kubernetes manages the distribution. Each pod runs one Node.js process.
  2. Cluster mode within pods with fewer replicas. More memory-efficient because workers share the V8 code cache. Each pod runs N Node.js processes.

For most services, option 1 is simpler and sufficient. Option 2 helps when you need to reduce the number of pods (e.g., to reduce database connection count) while maintaining throughput.

Background Job Processing with BullMQ

typescript
1import { Queue, Worker, QueueEvents } from "bullmq";
2import IORedis from "ioredis";
3 
4const connection = new IORedis(process.env.REDIS_URL, {
5 maxRetriesPerRequest: null,
6 enableReadyCheck: false,
7});
8 
9const emailQueue = new Queue("email", { connection });
10 
11const worker = new Worker(
12 "email",
13 async (job) => {
14 const { to, subject, template, data } = job.data;
15 await sendEmail({ to, subject, template, data });
16 },
17 {
18 connection,
19 concurrency: 10,
20 limiter: {
21 max: 100,
22 duration: 60_000, // 100 emails per minute
23 },
24 }
25);
26 
27worker.on("completed", (job) => {
28 console.log(JSON.stringify({ level: "info", msg: "job completed", jobId: job.id }));
29});
30 
31worker.on("failed", (job, err) => {
32 console.log(JSON.stringify({
33 level: "error",
34 msg: "job failed",
35 jobId: job?.id,
36 error: err.message,
37 }));
38});
39 
40// Graceful shutdown
41process.on("SIGTERM", async () => {
42 await worker.close();
43 await connection.quit();
44 process.exit(0);
45});
46 

Deploy workers as a separate Kubernetes Deployment from the API:

yaml
1apiVersion: apps/v1
2kind: Deployment
3metadata:
4 name: email-worker
5spec:
6 replicas: 2
7 selector:
8 matchLabels:
9 app: email-worker
10 template:
11 spec:
12 containers:
13 - name: worker
14 image: registry.example.com/api-service:v1.5.0
15 command: ["node", "dist/workers/email.js"]
16 resources:
17 requests:
18 cpu: 250m
19 memory: 256Mi
20 limits:
21 memory: 512Mi
22 

Anti-Patterns to Avoid

Using npm start as the container command. npm adds a process wrapper that doesn't forward SIGTERM correctly. Always use node dist/server.js directly.

Not setting NODE_ENV=production. Express (and many npm packages) run in development mode by default, which enables verbose logging, disables response caching, and includes stack traces in error responses.

Ignoring event loop blocking. A synchronous operation (JSON.parse on a 100MB payload, RegExp backtracking, synchronous file I/O) blocks the entire Node.js event loop. Use --diagnostic-report-on-signal and send SIGUSR2 to generate a diagnostic report when requests are slow.

Running npm install at container startup. All dependencies must be baked into the image. Container startup should be deterministic and fast — executing npm install adds 10-60 seconds of unpredictable startup time and fails when the npm registry is unreachable.

Using latest or lts Node.js base images. Pin to a specific major version (node:22-alpine) to prevent surprise runtime changes during image rebuilds.

Conclusion

TypeScript on Kubernetes works well for I/O-bound services where the team's existing TypeScript expertise outweighs Node.js's single-threaded limitation. The key operational requirements are explicit V8 heap sizing via --max-old-space-size, proper SIGTERM handling for graceful shutdown, and production-only dependency installation for lean images.

For compute-bound workloads, Node.js is not the optimal choice — Go or Rust provide better per-core performance. But for API services, webhook handlers, queue consumers, and BFF (Backend for Frontend) layers, TypeScript on Kubernetes delivers productive development with acceptable operational characteristics.

FAQ

Need expert help?

Building with CI/CD pipelines?

I help teams ship production-grade systems. From architecture review to hands-on builds.

Muneer Puthiya Purayil

SaaS Architect & AI Systems Engineer. 10+ years shipping production infrastructure across fintech, automotive, e-commerce, and healthcare.

Engage

Start a
Conversation.

For teams building at scale: SaaS platforms, agentic AI systems, and enterprise mobile infrastructure. Scope and fit are evaluated before any engagement begins.

Limited availability · Q3 / Q4 2026