feat(phase-0): setup Docker Compose with TypeScript monorepo structure

- Create monorepo structure with apps/ and packages/
- Add Docker Compose for api, web, db, redis, worker services
- Migrate existing Express.js logic to TypeScript with 100% backward compatibility
- Preserve all existing API endpoints (/api/track, /api/v1/track) with identical behavior
- Setup development environment with hot reload and proper networking
- Add comprehensive TypeScript configuration with path mapping
- Include production-ready Dockerfiles with multi-stage builds
- Maintain existing rate limiting (100 req/hour/IP) and response formats
- Add health checks and graceful shutdown handling
- Setup Turbo for efficient monorepo builds and development
This commit is contained in:
Andrei
2025-08-18 07:03:08 +00:00
parent fb63d91b7d
commit 956f1aeadb
28 changed files with 1748 additions and 14 deletions

63
apps/web/Dockerfile Normal file
View File

@@ -0,0 +1,63 @@
# Multi-stage build for production optimization
FROM node:20-alpine AS base
# Install dependencies only when needed
FROM base AS deps
WORKDIR /app
# Copy package files
COPY package*.json ./
COPY apps/web/package*.json ./apps/web/
COPY packages/shared/package*.json ./packages/shared/
# Install dependencies
RUN npm ci --only=production && npm cache clean --force
# Development stage
FROM base AS dev
WORKDIR /app
# Copy package files
COPY package*.json ./
COPY apps/web/package*.json ./apps/web/
COPY packages/shared/package*.json ./packages/shared/
# Install all dependencies including devDependencies
RUN npm ci
# Copy source code
COPY apps/web ./apps/web
COPY packages/shared ./packages/shared
WORKDIR /app/apps/web
EXPOSE 3000
CMD ["npm", "run", "dev"]
# Build stage
FROM base AS builder
WORKDIR /app
# Copy everything needed for build
COPY package*.json ./
COPY apps/web ./apps/web
COPY packages/shared ./packages/shared
# Install dependencies and build
RUN npm ci
WORKDIR /app/apps/web
RUN npm run build
# Production stage
FROM nginx:alpine AS production
# Copy built application
COPY --from=builder /app/apps/web/dist /usr/share/nginx/html
# Copy nginx configuration
COPY apps/web/nginx.conf /etc/nginx/nginx.conf
EXPOSE 3000
CMD ["nginx", "-g", "daemon off;"]

54
apps/web/nginx.conf Normal file
View File

@@ -0,0 +1,54 @@
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
server {
listen 3000;
server_name localhost;
root /usr/share/nginx/html;
index index.html index.htm;
# Handle client-side routing
location / {
try_files $uri $uri/ /index.html;
}
# API proxy
location /api/ {
proxy_pass http://api:3333;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
}
# Health check
location /health {
access_log off;
return 200 "healthy\n";
add_header Content-Type text/plain;
}
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; img-src 'self' data: https:; font-src 'self';" always;
# Gzip compression
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_proxied expired no-cache no-store private must-revalidate auth;
gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml+rss application/javascript;
}
}

59
apps/web/package.json Normal file
View File

@@ -0,0 +1,59 @@
{
"name": "@redirect-intelligence/web",
"version": "2.0.0",
"private": true,
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0",
"@chakra-ui/react": "^2.8.2",
"@chakra-ui/icons": "^2.1.1",
"@emotion/react": "^11.11.1",
"@emotion/styled": "^11.11.0",
"framer-motion": "^10.16.16",
"@tanstack/react-query": "^5.17.9",
"@tanstack/react-query-devtools": "^5.17.9",
"react-router-dom": "^6.20.1",
"mermaid": "^10.6.1",
"axios": "^1.6.7",
"react-hook-form": "^7.48.2",
"@hookform/resolvers": "^3.3.2",
"zod": "^3.22.4",
"react-dropzone": "^14.2.3",
"date-fns": "^3.0.6"
},
"devDependencies": {
"@types/react": "^18.2.43",
"@types/react-dom": "^18.2.17",
"@types/node": "^20.10.0",
"@vitejs/plugin-react": "^4.2.1",
"typescript": "^5.3.0",
"vite": "^5.0.8",
"eslint": "^8.55.0",
"@typescript-eslint/eslint-plugin": "^6.14.0",
"@typescript-eslint/parser": "^6.14.0",
"eslint-plugin-react": "^7.33.2",
"eslint-plugin-react-hooks": "^4.6.0"
},
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"lint:fix": "eslint src --ext ts,tsx --fix",
"test": "vitest",
"test:ui": "vitest --ui",
"clean": "rm -rf dist"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}

36
apps/web/tsconfig.json Normal file
View File

@@ -0,0 +1,36 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
/* Path mapping */
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"],
"@/components/*": ["./src/components/*"],
"@/pages/*": ["./src/pages/*"],
"@/hooks/*": ["./src/hooks/*"],
"@/types/*": ["./src/types/*"],
"@/lib/*": ["./src/lib/*"]
}
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}

View File

@@ -0,0 +1,10 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

27
apps/web/vite.config.ts Normal file
View File

@@ -0,0 +1,27 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import path from 'path'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
server: {
port: 3000,
host: '0.0.0.0',
proxy: {
'/api': {
target: 'http://localhost:3333',
changeOrigin: true,
},
},
},
build: {
outDir: 'dist',
sourcemap: true,
},
})