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
|
||||
.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
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
|
|
|||
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",
|
||||
"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",
|
||||
|
|
@ -76,4 +80,4 @@
|
|||
"coverageDirectory": "../coverage",
|
||||
"testEnvironment": "node"
|
||||
}
|
||||
}
|
||||
}
|
||||
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 { 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();
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,8 +2,7 @@
|
|||
"compilerOptions": {
|
||||
"types": [
|
||||
"node",
|
||||
"jest",
|
||||
"express"
|
||||
"jest"
|
||||
],
|
||||
"module": "nodenext",
|
||||
"moduleResolution": "nodenext",
|
||||
|
|
|
|||
Loading…
Reference in a new issue