feat(backoffice): migrate to Fastify adapter with pnpm, Pino logging, and ultra-optimized Dockerfile
This commit is contained in:
parent
22315e0231
commit
c6f7f41452
8 changed files with 6758 additions and 11870 deletions
|
|
@ -1,36 +1,69 @@
|
||||||
# Dependencies
|
# =============================================================================
|
||||||
|
# pnpm / Node
|
||||||
|
# =============================================================================
|
||||||
node_modules
|
node_modules
|
||||||
|
.pnpm-store
|
||||||
|
pnpm-debug.log
|
||||||
npm-debug.log
|
npm-debug.log
|
||||||
|
yarn-error.log
|
||||||
|
.pnpm-lockfile
|
||||||
|
|
||||||
# Build output (we rebuild in Docker)
|
# =============================================================================
|
||||||
|
# Build output (rebuilt in Docker)
|
||||||
|
# =============================================================================
|
||||||
dist
|
dist
|
||||||
|
build
|
||||||
|
.next
|
||||||
|
|
||||||
# Development
|
# =============================================================================
|
||||||
|
# Development & IDE
|
||||||
|
# =============================================================================
|
||||||
.git
|
.git
|
||||||
.gitignore
|
.gitignore
|
||||||
|
.vscode
|
||||||
|
.idea
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Environment & Secrets
|
||||||
|
# =============================================================================
|
||||||
.env
|
.env
|
||||||
.env.*
|
.env.*
|
||||||
!.env.example
|
!.env.example
|
||||||
|
*.pem
|
||||||
|
*.key
|
||||||
|
|
||||||
# IDE
|
# =============================================================================
|
||||||
.idea
|
# Tests & Coverage
|
||||||
.vscode
|
# =============================================================================
|
||||||
*.swp
|
|
||||||
*.swo
|
|
||||||
|
|
||||||
# Test
|
|
||||||
coverage
|
coverage
|
||||||
.nyc_output
|
.nyc_output
|
||||||
test
|
test
|
||||||
|
tests
|
||||||
|
__tests__
|
||||||
*.spec.ts
|
*.spec.ts
|
||||||
*.test.ts
|
*.test.ts
|
||||||
|
jest.config.*
|
||||||
|
*.e2e-spec.ts
|
||||||
|
|
||||||
# Documentation
|
# =============================================================================
|
||||||
|
# Documentation & Misc
|
||||||
|
# =============================================================================
|
||||||
README.md
|
README.md
|
||||||
|
CHANGELOG.md
|
||||||
docs
|
docs
|
||||||
|
|
||||||
# Misc
|
|
||||||
*.md
|
|
||||||
*.log
|
*.log
|
||||||
.DS_Store
|
.eslintcache
|
||||||
Thumbs.db
|
.prettierignore
|
||||||
|
.editorconfig
|
||||||
|
tsconfig.tsbuildinfo
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Docker
|
||||||
|
# =============================================================================
|
||||||
|
Dockerfile*
|
||||||
|
docker-compose*
|
||||||
|
.dockerignore
|
||||||
|
|
|
||||||
|
|
@ -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
|
WORKDIR /app
|
||||||
|
|
||||||
# Copy package files first (better layer caching)
|
# Copy lockfile and fetch cache
|
||||||
COPY package*.json ./
|
COPY pnpm-lock.yaml ./
|
||||||
|
COPY --from=deps /app/node_modules ./node_modules
|
||||||
|
|
||||||
# Install ALL dependencies (including devDependencies for build)
|
# Copy package files and install
|
||||||
RUN npm ci --ignore-scripts
|
COPY package.json ./
|
||||||
|
RUN pnpm install --frozen-lockfile --offline
|
||||||
|
|
||||||
# Copy source code
|
# Copy source and build
|
||||||
COPY . .
|
COPY . .
|
||||||
|
RUN pnpm build
|
||||||
|
|
||||||
# Build the application
|
# Prune dev dependencies
|
||||||
RUN npm run build
|
RUN pnpm prune --prod && \
|
||||||
|
pnpm store prune && \
|
||||||
|
rm -rf /root/.local/share/pnpm/store
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# Stage 2: Production dependencies only
|
# Stage 4: Production - Minimal runtime (Distroless alternative: Alpine)
|
||||||
# =============================================================================
|
|
||||||
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
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
FROM node:20-alpine AS production
|
FROM node:20-alpine AS production
|
||||||
|
|
||||||
# Add non-root user for security
|
# Security: non-root user
|
||||||
RUN addgroup -g 1001 -S nodejs && adduser -S nestjs -u 1001
|
RUN addgroup -g 1001 -S nodejs && \
|
||||||
|
adduser -S nestjs -u 1001 -G nodejs
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Copy only what's needed to run
|
# Copy only production artifacts
|
||||||
COPY --from=deps --chown=nestjs:nodejs /app/node_modules ./node_modules
|
|
||||||
COPY --from=builder --chown=nestjs:nodejs /app/dist ./dist
|
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 ./
|
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 NODE_ENV=production
|
||||||
ENV PORT=3001
|
ENV PORT=3001
|
||||||
|
ENV HOST=0.0.0.0
|
||||||
|
|
||||||
# Use non-root user
|
# Switch to non-root user
|
||||||
USER nestjs
|
USER nestjs
|
||||||
|
|
||||||
EXPOSE 3001
|
EXPOSE 3001
|
||||||
|
|
||||||
# Health check
|
# Health check
|
||||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
|
||||||
CMD node -e "require('http').get('http://localhost:3001/health', (r) => process.exit(r.statusCode === 200 ? 0 : 1))" || exit 1
|
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"]
|
CMD ["node", "dist/main.js"]
|
||||||
|
|
|
||||||
11799
backoffice/package-lock.json
generated
11799
backoffice/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -1,10 +1,11 @@
|
||||||
{
|
{
|
||||||
"name": "backoffice",
|
"name": "backoffice",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"description": "",
|
"description": "GoHorse Jobs Backoffice API",
|
||||||
"author": "",
|
"author": "",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "UNLICENSED",
|
"license": "UNLICENSED",
|
||||||
|
"packageManager": "pnpm@9.15.4",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "nest build",
|
"build": "nest build",
|
||||||
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
||||||
|
|
@ -24,15 +25,19 @@
|
||||||
"@nestjs/common": "^11.0.1",
|
"@nestjs/common": "^11.0.1",
|
||||||
"@nestjs/config": "^4.0.2",
|
"@nestjs/config": "^4.0.2",
|
||||||
"@nestjs/core": "^11.0.1",
|
"@nestjs/core": "^11.0.1",
|
||||||
"@nestjs/platform-express": "^11.0.1",
|
"@nestjs/platform-fastify": "^11.0.1",
|
||||||
"@nestjs/swagger": "^11.2.3",
|
"@nestjs/swagger": "^11.2.3",
|
||||||
|
"@fastify/cors": "^10.0.2",
|
||||||
|
"@fastify/helmet": "^13.0.1",
|
||||||
|
"@fastify/compress": "^8.0.1",
|
||||||
"axios": "^1.13.2",
|
"axios": "^1.13.2",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.14.3",
|
"class-validator": "^0.14.3",
|
||||||
|
"pino": "^9.6.0",
|
||||||
|
"pino-pretty": "^13.0.0",
|
||||||
"reflect-metadata": "^0.2.2",
|
"reflect-metadata": "^0.2.2",
|
||||||
"rxjs": "^7.8.1",
|
"rxjs": "^7.8.1",
|
||||||
"stripe": "^20.0.0",
|
"stripe": "^20.0.0"
|
||||||
"swagger-ui-express": "^5.0.1"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "^3.2.0",
|
"@eslint/eslintrc": "^3.2.0",
|
||||||
|
|
@ -40,7 +45,6 @@
|
||||||
"@nestjs/cli": "^11.0.0",
|
"@nestjs/cli": "^11.0.0",
|
||||||
"@nestjs/schematics": "^11.0.0",
|
"@nestjs/schematics": "^11.0.0",
|
||||||
"@nestjs/testing": "^11.0.1",
|
"@nestjs/testing": "^11.0.1",
|
||||||
"@types/express": "^5.0.0",
|
|
||||||
"@types/jest": "^30.0.0",
|
"@types/jest": "^30.0.0",
|
||||||
"@types/node": "^22.10.7",
|
"@types/node": "^22.10.7",
|
||||||
"@types/supertest": "^6.0.2",
|
"@types/supertest": "^6.0.2",
|
||||||
|
|
|
||||||
6566
backoffice/pnpm-lock.yaml
Normal file
6566
backoffice/pnpm-lock.yaml
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -1,17 +1,74 @@
|
||||||
import { NestFactory } from '@nestjs/core';
|
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 { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
|
||||||
import { AppModule } from './app.module';
|
import { AppModule } from './app.module';
|
||||||
|
import compression from '@fastify/compress';
|
||||||
|
import helmet from '@fastify/helmet';
|
||||||
|
|
||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
const app = await NestFactory.create(AppModule, { rawBody: true });
|
// Create Fastify adapter with Pino logging
|
||||||
|
const adapter = new FastifyAdapter({
|
||||||
app.enableCors({
|
logger: {
|
||||||
origin: ['http://localhost:3000', 'https://gohorsejobs.com'],
|
level: process.env.NODE_ENV === 'production' ? 'info' : 'debug',
|
||||||
credentials: true,
|
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()
|
const config = new DocumentBuilder()
|
||||||
.setTitle('GoHorse Backoffice API')
|
.setTitle('GoHorse Backoffice API')
|
||||||
.setDescription('SaaS Administration and Subscription Management')
|
.setDescription('SaaS Administration and Subscription Management')
|
||||||
|
|
@ -21,15 +78,23 @@ async function bootstrap() {
|
||||||
.addTag('Admin Dashboard')
|
.addTag('Admin Dashboard')
|
||||||
.addBearerAuth()
|
.addBearerAuth()
|
||||||
.build();
|
.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;
|
const port = process.env.PORT || 3001;
|
||||||
await app.listen(port);
|
const host = process.env.HOST || '0.0.0.0';
|
||||||
console.log(`🚀 Backoffice API: http://localhost:${port}`);
|
|
||||||
console.log(`📚 Swagger docs: http://localhost:${port}/api/docs`);
|
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();
|
void bootstrap();
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import Stripe from 'stripe';
|
||||||
export class StripeService implements OnModuleInit {
|
export class StripeService implements OnModuleInit {
|
||||||
private stripe: Stripe;
|
private stripe: Stripe;
|
||||||
|
|
||||||
constructor(private configService: ConfigService) {}
|
constructor(private configService: ConfigService) { }
|
||||||
|
|
||||||
onModuleInit() {
|
onModuleInit() {
|
||||||
const secretKey = this.configService.get<string>('STRIPE_SECRET_KEY');
|
const secretKey = this.configService.get<string>('STRIPE_SECRET_KEY');
|
||||||
|
|
@ -15,7 +15,7 @@ export class StripeService implements OnModuleInit {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.stripe = new Stripe(secretKey, {
|
this.stripe = new Stripe(secretKey, {
|
||||||
apiVersion: '2025-11-17.clover' as const,
|
apiVersion: '2025-12-15.clover' as const,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,7 @@
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"types": [
|
"types": [
|
||||||
"node",
|
"node",
|
||||||
"jest",
|
"jest"
|
||||||
"express"
|
|
||||||
],
|
],
|
||||||
"module": "nodenext",
|
"module": "nodenext",
|
||||||
"moduleResolution": "nodenext",
|
"moduleResolution": "nodenext",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue