DEV Community

Roberto Luna
Roberto Luna

Posted on

Upgrading Next to 16.3.4, Adding Docker Resource Limits, and Shipping Condo‑Amenities Endpoints in VS Monorepo

Upgrading Next to 16.3.4, Adding Docker Resource Limits, and Shipping Condo‑Amenities Endpoints in VS Monorepo

TL;DR: I bumped the root Next.js version from 16.2.6 to 16.3.4 to fix Vercel preview builds, introduced per‑service CPU/RAM caps in docker‑compose, and added a full set of condo‑amenities, package, and billing APIs. The changes are isolated to a few files, but they required careful version alignment and container tuning.


The Problem

  1. Vercel preview failures – Vercel started rejecting builds because the monorepo’s root next dependency was locked at 16.2.6, while the Vercel platform had already upgraded to 16.3.x. The preview logs showed:
   Error: Cannot find module 'next/dist/compiled/webpack'
   Require stack:
   - node_modules/next/dist/next-server.js
Enter fullscreen mode Exit fullscreen mode
  1. Docker containers running out of memory – During local integration tests the Postgres container would be OOM‑killed, and the Gotenberg PDF service occasionally hung because the host had no limits defined.

  2. Missing condo‑amenities API – The product roadmap demanded an endpoint to list, create, and delete amenity reservations, but the API layer only had billing and webhook controllers. Without it the front‑end pages (/condominios/amenidades) returned 404.

What I Tried First

  • Pinning Vercel to an older Node version – I set NODE_VERSION=18 in the Vercel dashboard hoping it would ignore the Next version mismatch. It didn’t; Vercel still used its internal Next runtime, and the build failed the same way.

  • Adding mem_limit only to the DB service – I edited docker-compose.yml to set mem_limit: 512m for db but left the Gotenberg service untouched. The DB stopped OOM‑killing, but Gotenberg still consumed >1 GB RAM and caused the host to swap, slowing down all tests.

  • Creating a quick “amenities” mock controller in apps/web – I added a static JSON file and imported it in the page component. The UI rendered, but the API contract was missing, and the back‑end could not be exercised by integration tests.

All three attempts gave me partial relief but left the core issues unresolved. I needed a proper version bump, a holistic resource‑limit strategy, and real NestJS controllers.

The Implementation

1. Align Next.js version for Vercel previews

The only change required was a single line in package.json. I opened a PR (#14) and replaced the old version:

@@ -21,7 +21,7 @@
   "dependencies": {
     "@notionhq/client": "^5.13.0",
     "@types/node": "^20.14.10",
-    "next": "16.2.6",
+    "next": "16.3.4",
     "otplib": "^13.3.0",
     "qrcode": "^1.5
Enter fullscreen mode Exit fullscreen mode

After the bump I ran npm install locally, committed the lockfile, and pushed. Vercel now builds the preview successfully, as the runtime version matches the package version.

2. Add per‑service CPU/RAM caps and lazy‑load Gotenberg

I revised docker-compose.yml (PR #13) to set limits on both the database and the PDF generation service:

@@ -1,6 +1,8 @@
 services:
   db:
     image: postgres:16
+    mem_limit: 512m
+    cpus: "1"
     environment:
       POSTGRES_USER: ${POSTGRES_USER}
       POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
   gotenberg:
     image: thecodingmachine/gotenberg:7
+    mem_limit: 256m
+    cpus: "0.5"
     restart: unless-stopped
Enter fullscreen mode Exit fullscreen mode

To avoid keeping Gotenberg running when not needed, I added a lazy‑load wrapper in apps/api/src/condo-fees/condo-autopay.cron.ts. The cron now spins up a temporary container via Docker API only when a payment batch is processed:

// apps/api/src/condo-fees/condo-autopay.cron.ts
import { Docker } from 'dockerode';
const docker = new Docker();

async function runGotenbergJob(pdfBuffer: Buffer) {
  // Pull the image if missing
  await docker.pull('thecodingmachine/gotenberg:7');
  const container = await docker.createContainer({
    Image: 'thecodingmachine/gotenberg:7',
    Cmd: ['gotenberg', '--api'],
    HostConfig: { Memory: 256 * 1024 * 1024, CpuShares: 512 },
  });
  await container.start();
  // ... send PDF to container, get result, then cleanup
  await container.stop();
  await container.remove();
}
Enter fullscreen mode Exit fullscreen mode

This approach reduces idle RAM usage on dev machines and CI runners.

3. Implement condo‑amenities REST API

The biggest chunk of the PR (#12) added a full NestJS controller, DTOs, and service helpers. Below is the skeleton of the new controller (apps/api/src/condo-amenities/condo-amenities.controller.ts):

// apps/api/src/condo-amenities/condo-amenities.controller.ts
import {
  Body, Controller, Delete, Get, HttpException,
  HttpStatus, Param, Patch, Post, Query, UseGuards,
} from '@nestjs/common';
import { query as db } from '../db/db.js';
import { JwtAuthGuard } from '../auth/jwt-auth.guard.js';

@Controller('condo-amenities')
@UseGuards(JwtAuthGuard)
export class CondoAmenitiesController {
  @Get()
  async list(@Query('condoId') condoId: string) {
    const rows = await db(`
      SELECT * FROM amenity_reservations
      WHERE condo_id = $1
      ORDER BY reservation_date DESC
    `, [condoId]);
    return rows;
  }

  @Post()
  async create(@Body() payload: {
    condoId: string; amenityId: string; date: string; userId: string;
  }) {
    const { condoId, amenityId, date, userId } = payload;
    const result = await db(`
      INSERT INTO amenity_reservations (condo_id, amenity_id, reservation_date, user_id)
      VALUES ($1, $2, $3, $4) RETURNING *
    `, [condoId, amenityId, date, userId]);
    return result[0];
  }

  @Delete(':id')
  async delete(@Param('id') id: string, @Query('userId') userId: string) {
    const del = await db(`
      DELETE FROM amenity_reservations
      WHERE id = $1 AND user_id = $2
      RETURNING *
    `, [id, userId]);
    if (!del[0]) {
      throw new HttpException('Not found or unauthorized', HttpStatus.NOT_FOUND);
    }
    return { success: true };
  }
}
Enter fullscreen mode Exit fullscreen mode

I also registered the controller in apps/api/src/app.module.ts:

@@ -46,6 +46,8 @@ import { MllController } from "./mll/mll.controller.js";
 import { NonDebtCertificatesController } from "./condo-fees/non-debt-certificates.controller.js";
 import { CondoAnnouncemen
+import { CondoAmenitiesController } from "./condo-amenities/condo-amenities.controller.js";
+import { CondoPackagesController } from "./condo-packages/condo-packages.controller.js";

 @Module({
   imports: [...],
   controllers: [
     // existing controllers
+    CondoAmenitiesController,
+    CondoPackagesController,
   ],
   providers: [...],
 })
 export class AppModule {}
Enter fullscreen mode Exit fullscreen mode

The front‑end pages (apps/web/src/app/condominios/amenidades/page.tsx) now call /api/condo-amenities?condoId=123 and render the JSON directly, eliminating the previous 404.

4. Minor


Part of my Build in Public series — sharing the real process of building Building PlayaMXCRM from Playa del Carmen, México.

Repo: zaerohell/VS · 2026-10-07

#playadev #buildinpublic

Top comments (0)