Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 20
node-version: 24
cache: npm

- name: Install dependencies
Expand Down
45 changes: 0 additions & 45 deletions .github/workflows/deploy-stage.yml

This file was deleted.

10 changes: 10 additions & 0 deletions .husky/install.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
if (
process.env.NODE_ENV === 'production' ||
process.env.CI === 'true' ||
process.env.HUSKY === '0'
) {
process.exit(0);
}

const husky = (await import('husky')).default;
husky();
24 changes: 15 additions & 9 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,20 +1,26 @@
# Use Node20 Slim Image
FROM harbor.internal.codebuckets.in/node20/slim
# Node 24 LTS, at least 24.22.1 (fixes CVE-2026-111497 and the rest of the October 2026 Node.js security release)
FROM node:24.22.1-slim

# Set the working directory inside the container
WORKDIR /usr/app

# Copy the rest of the application code to the working directory
COPY . .
# Production install: no dev dependencies, and no git hooks (husky isn't installed; see .husky/install.mjs)
ENV NODE_ENV=production
ENV HUSKY=0

# Set Registry
RUN npm set registry https://npm.internal.codebuckets.in
RUN npm config set registry https://npm.internal.codebuckets.in

# Dependencies first, so they stay cached while only the code changes
COPY package*.json ./
COPY .husky/install.mjs ./.husky/install.mjs
RUN npm ci --omit=dev

# Install dependencies for production
RUN npm install --production
# Copy the rest of the application code to the working directory
COPY . .

# Expose any necessary ports (if your application listens on any)
EXPOSE 3000
# The app listens on PORT, 8080 by default (bin/www)
EXPOSE 8080

# Command to start the application
CMD ["npm", "run", "start"]
20 changes: 8 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ npm run plan:new -- --slug=my-task --title="My Task"

### Prerequisites

- Node.js 16+ (Node 20+ recommended)
- Node.js 24+ (the Docker image uses 24.22.1, which has the October 2026 security fixes)
- MySQL 5.7+
- Redis (for production rate limiting)

Expand Down Expand Up @@ -292,12 +292,9 @@ Multi-core support using Node.js cluster module for improved performance and sca
# Development (with auto-reload)
npm run start:dev

# Production (Node 20+)
# Production (Node 24+)
npm start

# Production (Node 16)
npm run start:16

# Code generation
npm run plop

Expand Down Expand Up @@ -472,19 +469,18 @@ module.exports = router;

```bash
docker build -t node-template .
docker run -p 3000:3000 --env-file .env.production node-template
docker run -p 8080:8080 --env-file .env.production node-template
```

### Docker Compose

```bash
docker-compose up -d
```
Deployments run on Kubernetes (through Engineering Fabric); there is no Docker Compose deployment. The app listens on
`PORT` (8080 by default) and forks one worker per CPU of the container's limit (`WEB_CONCURRENCY` overrides it; one
worker when the container has no limit), never one per CPU of the host.

**Production Setup:**

- Uses internal registry: `harbor.internal.codebuckets.in`
- Base image: `node:24.22.1-slim`
- NPM registry: `https://npm.internal.codebuckets.in`
- Environment variables come from the deployment (Kubernetes secrets); Azure Key Vault (`@codebucket/azure-env`) is no longer used

---

Expand Down
7 changes: 3 additions & 4 deletions bin/www
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ loadEnvironment(process.env.NODE_ENV);
* Module dependencies.
*/
const cluster = require("cluster");
const cpu = require("os").cpus().length;
const { workerCount } = require("../config/cpu-count");
const debug = require("debug")("sidebrain:server");
const http = require("http");
const { startOpenTelemetry, shutdownOpenTelemetry } = require("../instrumentation/opentelemetry");
Expand All @@ -27,11 +27,10 @@ async function runMaster() {
await initMaster();
console.log("Master-only services initialized");

// WEB_CONCURRENCY, else the container's CPU limit (never the host's CPU count); one worker in stage and development
const workers =
process.env.WEB_CONCURRENCY ??
(process.env.NODE_ENV === "stage" || process.env.NODE_ENV === "development"
? 1
: Math.max(1, cpu));
(process.env.NODE_ENV === "stage" || process.env.NODE_ENV === "development" ? 1 : workerCount());

for (let i = 0; i < Number(workers); i++) {
console.log("Forking worker", i + 1, "of", Number(workers));
Expand Down
55 changes: 55 additions & 0 deletions config/cpu-count.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
const fs = require("fs");
const os = require("os");

// cgroup v2 and v1 files holding the container's CPU limit (quota per period)
const CGROUP_V2_CPU_MAX = "/sys/fs/cgroup/cpu.max";
const CGROUP_V1_QUOTA = "/sys/fs/cgroup/cpu/cpu.cfs_quota_us";
const CGROUP_V1_PERIOD = "/sys/fs/cgroup/cpu/cpu.cfs_period_us";

function readFile(file, read) {
try {
return read(file).trim();
} catch {
return null;
}
}

/**
* The container's CPU limit in CPUs (e.g. 0.25 for "250m"), or null when there is none or it can't be read.
* os.cpus() reports the host's CPUs, not what the container may use.
*/
function containerCpuLimit(read = file => fs.readFileSync(file, "utf8")) {
const v2 = readFile(CGROUP_V2_CPU_MAX, read);
if (v2) {
const [quota, period] = v2.split(/\s+/);
if (quota !== "max" && Number(quota) > 0 && Number(period) > 0) {
return Number(quota) / Number(period);
}
return null;
}
const quota = Number(readFile(CGROUP_V1_QUOTA, read));
const period = Number(readFile(CGROUP_V1_PERIOD, read));
if (quota > 0 && period > 0) {
return quota / period;
}
return null;
}

/**
* How many workers to fork: WEB_CONCURRENCY when set; else the container's CPU limit rounded up (at least 1); else 1.
* Never the host's CPU count: in a container without a limit that is the whole node's, and every worker holds its own
* memory and database pool.
*/
function workerCount({ env = process.env, read } = {}) {
const configured = Number.parseInt(env.WEB_CONCURRENCY, 10);
if (configured > 0) {
return configured;
}
const limit = containerCpuLimit(read);
if (limit) {
return Math.max(1, Math.ceil(limit));
}
return 1;
}

module.exports = { containerCpuLimit, workerCount, hostCpus: () => os.cpus().length };
15 changes: 0 additions & 15 deletions docker-compose.yml

This file was deleted.

4 changes: 2 additions & 2 deletions middleware/logger.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const { v4: uuidv4 } = require("uuid");
const { randomUUID } = require("crypto");
const logger = require("../utils/logger");
const requestLogger = require("pino-http")({
// Reuse an existing logger instance
Expand All @@ -10,7 +10,7 @@ const requestLogger = require("pino-http")({
if (existingID) {
return existingID;
}
const id = uuidv4();
const id = randomUUID();
res.setHeader("X-Request-Id", id);
return id;
},
Expand Down
Loading
Loading