feat(backoffice): migrate to Fastify adapter with pnpm, Pino logging, and ultra-optimized Dockerfile

This commit is contained in:
Tiago Yamamoto 2025-12-23 23:44:02 -03:00
parent 22315e0231
commit c6f7f41452
8 changed files with 6758 additions and 11870 deletions

View file

@ -1,36 +1,69 @@
# Dependencies
# =============================================================================
# pnpm / Node
# =============================================================================
node_modules
.pnpm-store
pnpm-debug.log
npm-debug.log
yarn-error.log
.pnpm-lockfile
# Build output (we rebuild in Docker)
# =============================================================================
# Build output (rebuilt in Docker)
# =============================================================================
dist
build
.next
# Development
# =============================================================================
# Development & IDE
# =============================================================================
.git
.gitignore
.vscode
.idea
*.swp
*.swo
.DS_Store
Thumbs.db
# =============================================================================
# Environment & Secrets
# =============================================================================
.env
.env.*
!.env.example
*.pem
*.key
# IDE
.idea
.vscode
*.swp
*.swo
# Test
# =============================================================================
# Tests & Coverage
# =============================================================================
coverage
.nyc_output
test
tests
__tests__
*.spec.ts
*.test.ts
jest.config.*
*.e2e-spec.ts
# Documentation
# =============================================================================
# Documentation & Misc
# =============================================================================
README.md
CHANGELOG.md
docs
# Misc
*.md
*.log
.DS_Store
Thumbs.db
.eslintcache
.prettierignore
.editorconfig
tsconfig.tsbuildinfo
# =============================================================================
# Docker
# =============================================================================
Dockerfile*
docker-compose*
.dockerignore

View file

@ -1,60 +1,80 @@
# =============================================================================
# Stage 1: Builder - Install dependencies and build
# GoHorse Backoffice - Ultra-Optimized Dockerfile with pnpm
# Target: < 200MB final image
# =============================================================================
FROM node:20-alpine AS builder
# Stage 1: Base with pnpm
FROM node:20-alpine AS base
RUN corepack enable && corepack prepare pnpm@9.15.4 --activate
RUN apk add --no-cache libc6-compat
# Stage 2: Fetch dependencies (better cache utilization)
FROM base AS deps
WORKDIR /app
COPY pnpm-lock.yaml ./
RUN pnpm fetch
# Stage 3: Build
FROM base AS builder
WORKDIR /app
# Copy package files first (better layer caching)
COPY package*.json ./
# Copy lockfile and fetch cache
COPY pnpm-lock.yaml ./
COPY --from=deps /app/node_modules ./node_modules
# Install ALL dependencies (including devDependencies for build)
RUN npm ci --ignore-scripts
# Copy package files and install
COPY package.json ./
RUN pnpm install --frozen-lockfile --offline
# Copy source code
# Copy source and build
COPY . .
RUN pnpm build
# Build the application
RUN npm run build
# Prune dev dependencies
RUN pnpm prune --prod && \
pnpm store prune && \
rm -rf /root/.local/share/pnpm/store
# =============================================================================
# Stage 2: Production dependencies only
# =============================================================================
FROM node:20-alpine AS deps
WORKDIR /app
COPY package*.json ./
# Install ONLY production dependencies (smaller image)
RUN npm ci --omit=dev --ignore-scripts && npm cache clean --force
# =============================================================================
# Stage 3: Production - Minimal runtime image
# Stage 4: Production - Minimal runtime (Distroless alternative: Alpine)
# =============================================================================
FROM node:20-alpine AS production
# Add non-root user for security
RUN addgroup -g 1001 -S nodejs && adduser -S nestjs -u 1001
# Security: non-root user
RUN addgroup -g 1001 -S nodejs && \
adduser -S nestjs -u 1001 -G nodejs
WORKDIR /app
# Copy only what's needed to run
COPY --from=deps --chown=nestjs:nodejs /app/node_modules ./node_modules
# Copy only production artifacts
COPY --from=builder --chown=nestjs:nodejs /app/dist ./dist
COPY --from=builder --chown=nestjs:nodejs /app/node_modules ./node_modules
COPY --from=builder --chown=nestjs:nodejs /app/package.json ./
# Set environment
# Remove unnecessary files to reduce size
RUN find node_modules -name "*.md" -delete 2>/dev/null || true && \
find node_modules -name "*.d.ts" -delete 2>/dev/null || true && \
find node_modules -name "CHANGELOG*" -delete 2>/dev/null || true && \
find node_modules -name "LICENSE*" -delete 2>/dev/null || true && \
find node_modules -name "*.map" -delete 2>/dev/null || true && \
find node_modules -type d -name "test" -exec rm -rf {} + 2>/dev/null || true && \
find node_modules -type d -name "tests" -exec rm -rf {} + 2>/dev/null || true && \
find node_modules -type d -name "__tests__" -exec rm -rf {} + 2>/dev/null || true && \
find node_modules -type d -name "docs" -exec rm -rf {} + 2>/dev/null || true && \
rm -rf /tmp/* /var/cache/apk/*
# Environment
ENV NODE_ENV=production
ENV PORT=3001
ENV HOST=0.0.0.0
# Use non-root user
# Switch to non-root user
USER nestjs
EXPOSE 3001
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD node -e "require('http').get('http://localhost:3001/health', (r) => process.exit(r.statusCode === 200 ? 0 : 1))" || exit 1
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
CMD node -e "const http = require('http'); http.get('http://localhost:3001/health', (r) => process.exit(r.statusCode === 200 ? 0 : 1)).on('error', () => process.exit(1))"
CMD ["node", "dist/main.js"]

File diff suppressed because it is too large Load diff

View file

@ -1,10 +1,11 @@
{
"name": "backoffice",
"version": "0.0.1",
"description": "",
"description": "GoHorse Jobs Backoffice API",
"author": "",
"private": true,
"license": "UNLICENSED",
"packageManager": "pnpm@9.15.4",
"scripts": {
"build": "nest build",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
@ -24,15 +25,19 @@
"@nestjs/common": "^11.0.1",
"@nestjs/config": "^4.0.2",
"@nestjs/core": "^11.0.1",
"@nestjs/platform-express": "^11.0.1",
"@nestjs/platform-fastify": "^11.0.1",
"@nestjs/swagger": "^11.2.3",
"@fastify/cors": "^10.0.2",
"@fastify/helmet": "^13.0.1",
"@fastify/compress": "^8.0.1",
"axios": "^1.13.2",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.3",
"pino": "^9.6.0",
"pino-pretty": "^13.0.0",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1",
"stripe": "^20.0.0",
"swagger-ui-express": "^5.0.1"
"stripe": "^20.0.0"
},
"devDependencies": {
"@eslint/eslintrc": "^3.2.0",
@ -40,7 +45,6 @@
"@nestjs/cli": "^11.0.0",
"@nestjs/schematics": "^11.0.0",
"@nestjs/testing": "^11.0.1",
"@types/express": "^5.0.0",
"@types/jest": "^30.0.0",
"@types/node": "^22.10.7",
"@types/supertest": "^6.0.2",

6566
backoffice/pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,17 +1,74 @@
import { NestFactory } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common';
import {
FastifyAdapter,
NestFastifyApplication,
} from '@nestjs/platform-fastify';
import { ValidationPipe, Logger } from '@nestjs/common';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import { AppModule } from './app.module';
import compression from '@fastify/compress';
import helmet from '@fastify/helmet';
async function bootstrap() {
const app = await NestFactory.create(AppModule, { rawBody: true });
app.enableCors({
origin: ['http://localhost:3000', 'https://gohorsejobs.com'],
credentials: true,
// Create Fastify adapter with Pino logging
const adapter = new FastifyAdapter({
logger: {
level: process.env.NODE_ENV === 'production' ? 'info' : 'debug',
transport:
process.env.NODE_ENV !== 'production'
? { target: 'pino-pretty', options: { colorize: true } }
: undefined,
},
trustProxy: true, // Required for getting real IP behind reverse proxy
bodyLimit: 10 * 1024 * 1024, // 10MB
});
app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true }));
const app = await NestFactory.create<NestFastifyApplication>(
AppModule,
adapter,
{ rawBody: true },
);
// Register Fastify plugins
await app.register(compression, { encodings: ['gzip', 'deflate'] });
await app.register(helmet, {
contentSecurityPolicy: process.env.NODE_ENV === 'production',
});
// CORS configuration (Fastify-native)
app.enableCors({
origin: (origin, callback) => {
const allowedOrigins = [
'http://localhost:3000',
'http://localhost:8963',
'https://gohorsejobs.com',
'https://admin.gohorsejobs.com',
process.env.FRONTEND_URL,
].filter(Boolean);
if (!origin || allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'), false);
}
},
credentials: true,
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With'],
maxAge: 86400, // 24 hours preflight cache
});
// Global validation pipe
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
transform: true,
forbidNonWhitelisted: true,
transformOptions: { enableImplicitConversion: true },
}),
);
// Swagger documentation
const config = new DocumentBuilder()
.setTitle('GoHorse Backoffice API')
.setDescription('SaaS Administration and Subscription Management')
@ -21,15 +78,23 @@ async function bootstrap() {
.addTag('Admin Dashboard')
.addBearerAuth()
.build();
SwaggerModule.setup(
'api/docs',
app,
SwaggerModule.createDocument(app, config),
);
SwaggerModule.setup('api/docs', app, SwaggerModule.createDocument(app, config));
// Health check endpoint
const fastifyInstance = app.getHttpAdapter().getInstance();
fastifyInstance.get('/health', async () => ({ status: 'ok', timestamp: new Date().toISOString() }));
// Start server
const port = process.env.PORT || 3001;
await app.listen(port);
console.log(`🚀 Backoffice API: http://localhost:${port}`);
console.log(`📚 Swagger docs: http://localhost:${port}/api/docs`);
const host = process.env.HOST || '0.0.0.0';
await app.listen(port, host);
const logger = new Logger('Bootstrap');
logger.log(`🚀 Backoffice API running on: http://${host}:${port}`);
logger.log(`📚 Swagger docs: http://${host}:${port}/api/docs`);
logger.log(`❤️ Health check: http://${host}:${port}/health`);
}
void bootstrap();

View file

@ -6,7 +6,7 @@ import Stripe from 'stripe';
export class StripeService implements OnModuleInit {
private stripe: Stripe;
constructor(private configService: ConfigService) {}
constructor(private configService: ConfigService) { }
onModuleInit() {
const secretKey = this.configService.get<string>('STRIPE_SECRET_KEY');
@ -15,7 +15,7 @@ export class StripeService implements OnModuleInit {
return;
}
this.stripe = new Stripe(secretKey, {
apiVersion: '2025-11-17.clover' as const,
apiVersion: '2025-12-15.clover' as const,
});
}

View file

@ -2,8 +2,7 @@
"compilerOptions": {
"types": [
"node",
"jest",
"express"
"jest"
],
"module": "nodenext",
"moduleResolution": "nodenext",