update production setup

This commit is contained in:
root
2026-05-10 19:19:56 -04:00
parent 6322b7d2a1
commit 2a75f47693
8 changed files with 191 additions and 34 deletions
+5 -5
View File
@@ -24,14 +24,14 @@ CORS_ORIGINS=http://localhost:3000,http://localhost:3001,http://localhost:3002,h
# Email - disable Resend (placeholder), use SMTP instead
RESEND_API_KEY=re_...
EMAIL_FROM=moulay.elabidi@gmail.com
EMAIL_FROM=rentaldrivego@gmail.com
EMAIL_FROM_NAME=RentalDriveGo
MAIL_HOST=smtp.gmail.com
MAIL_PORT=587
MAIL_SCHEME=smtp
MAIL_USERNAME=moulay.elabidi@gmail.com
MAIL_PASSWORD=seld rlvl bzuy npar
MAIL_FROM_ADDRESS=moulay.elabidi@gmail.com
MAIL_USERNAME=rentaldrivego@gmail.com
MAIL_PASSWORD=xmhg ibqy muoc rntc
MAIL_FROM_ADDRESS=rentaldrivego@gmail.com
MAIL_FROM_NAME=RentalDriveGo
MAIL_REPLY_TO_ADDRESS=moulay.elabidi@gmail.com
MAIL_REPLY_TO_ADDRESS=rentaldrivego@gmail.com
MAIL_REPLY_TO_NAME=RentalDriveGo
+57 -8
View File
@@ -1,17 +1,66 @@
# ── Traefik domains — used in docker-compose labels ──────────────────────────
ACME_EMAIL=admin@rentaldrivego.ma
MARKETPLACE_DOMAIN=rentaldrivego.ma
API_DOMAIN=api.rentaldrivego.ma
DASHBOARD_DOMAIN=dashboard.rentaldrivego.ma
ADMIN_DOMAIN=admin.rentaldrivego.ma
PUBLIC_SITE_DOMAIN=rentaldrivego.ma
PGMANAGE_DOMAIN=pgmanage.rentaldrivego.ma
DATABASE_URL=postgresql://postgres:change-me@postgres:5432/rentaldrivego
REDIS_URL=redis://redis:6379
# ── API ────────────────────────────────────────────────────────────────────────
API_PORT=4000
API_URL=http://localhost:4000
# SSR server-side calls go via the internal Docker network
API_INTERNAL_URL=http://api:4000/api/v1
NEXT_PUBLIC_API_URL=http://localhost:4000/api/v1
NEXT_PUBLIC_MARKETING_URL=http://localhost:3000
NEXT_PUBLIC_MARKETPLACE_URL=http://localhost:3000/explore
NEXT_PUBLIC_DASHBOARD_URL=http://localhost:3001
NEXT_PUBLIC_ADMIN_URL=http://localhost:3002
NEXT_PUBLIC_PUBLIC_SITE_DOMAIN=localhost:3003
DASHBOARD_URL=http://localhost:3001
# Public-facing API URL visible to browsers — MUST be your real domain, not localhost
API_URL=https://api.rentaldrivego.ma
# Baked into Next.js bundles at build time — MUST be set before running `npm run build`
NEXT_PUBLIC_API_URL=https://api.rentaldrivego.ma/api/v1
# ── Frontend public URLs ───────────────────────────────────────────────────────
NEXT_PUBLIC_MARKETING_URL=https://rentaldrivego.ma
NEXT_PUBLIC_MARKETPLACE_URL=https://rentaldrivego.ma/explore
NEXT_PUBLIC_DASHBOARD_URL=https://dashboard.rentaldrivego.ma
NEXT_PUBLIC_ADMIN_URL=https://admin.rentaldrivego.ma
NEXT_PUBLIC_PUBLIC_SITE_DOMAIN=rentaldrivego.ma
DASHBOARD_URL=https://dashboard.rentaldrivego.ma
# ── CORS ──────────────────────────────────────────────────────────────────────
# Comma-separated list of allowed browser origins. REQUIRED in production.
CORS_ORIGINS=https://rentaldrivego.ma,https://dashboard.rentaldrivego.ma,https://admin.rentaldrivego.ma
# ── Auth ──────────────────────────────────────────────────────────────────────
JWT_SECRET=replace-with-a-long-random-secret
JWT_EXPIRY=8h
RENTER_JWT_EXPIRY=7d
# ── Database ──────────────────────────────────────────────────────────────────
POSTGRES_PASSWORD=replace-with-a-real-password
NODE_ENV=production
# ── Email (choose one: Resend API or SMTP) ────────────────────────────────────
# Option A — Resend
RESEND_API_KEY=re_your_resend_api_key
EMAIL_FROM=noreply@rentaldrivego.ma
EMAIL_FROM_NAME=RentalDriveGo
# Option B — SMTP
# MAIL_HOST=smtp.rentaldrivego.ma
# MAIL_PORT=587
# MAIL_USERNAME=smtp-user
# MAIL_PASSWORD=smtp-password
# MAIL_SCHEME=smtp
# MAIL_FROM_ADDRESS=noreply@rentaldrivego.ma
# MAIL_FROM_NAME=RentalDriveGo
# ── Firebase push notifications (optional) ────────────────────────────────────
# FIREBASE_PROJECT_ID=your-firebase-project-id
# FIREBASE_CLIENT_EMAIL=firebase-adminsdk@your-project.iam.gserviceaccount.com
# FIREBASE_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n"
# ── Twilio SMS / WhatsApp (optional) ─────────────────────────────────────────
# TWILIO_ACCOUNT_SID=ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
# TWILIO_AUTH_TOKEN=your_auth_token
# TWILIO_PHONE_NUMBER=+1234567890
# TWILIO_WHATSAPP_NUMBER=+1234567890
+3
View File
@@ -0,0 +1,3 @@
ACME_EMAIL=rentaldrivego@gmail.com
TRAEFIK_DOMAIN=rentaldrivego.ma
TRAEFIK_DOMAIN_WWW=www.rentaldrivego.ma
+1 -1
View File
@@ -5,7 +5,7 @@
"scripts": {
"dev": "node --env-file=../../.env.local ../../node_modules/.bin/ts-node-dev --respawn --transpile-only src/index.ts",
"build": "tsc",
"start": "node --env-file=../../.env.local dist/index.js",
"start": "node dist/index.js",
"type-check": "tsc --noEmit"
},
"dependencies": {
+6
View File
@@ -32,6 +32,12 @@ import paymentsRouter from './routes/payments'
const app = express()
const server = http.createServer(app)
// Trust the first hop from a reverse proxy so req.ip and rate-limiting
// use the real client IP (from X-Forwarded-For) rather than the proxy's IP.
if (process.env.NODE_ENV === 'production') {
app.set('trust proxy', 1)
}
const v1 = '/api/v1'
const defaultCorsOrigins = [
'http://localhost:3000',
+8 -10
View File
@@ -1,15 +1,13 @@
import rateLimit, { ipKeyGenerator } from 'express-rate-limit'
import type { Request } from 'express'
const trustProxy = process.env.NODE_ENV === 'production'
const getClientIpKey = (req: Parameters<typeof ipKeyGenerator>[0]) =>
trustProxy
? ipKeyGenerator((req.headers['x-forwarded-for'] as string)?.split(',')[0]?.trim() ?? req.ip ?? '')
: ipKeyGenerator(req.ip ?? '')
// req.ip is already the real client IP when app.set('trust proxy', 1) is configured
const getClientIpKey = (req: Request) => ipKeyGenerator(req.ip ?? '')
// Strict limiter for auth endpoints — prevents brute-force and credential stuffing
export const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 960,
max: 20,
standardHeaders: 'draft-7',
legacyHeaders: false,
skipSuccessfulRequests: false,
@@ -20,7 +18,7 @@ export const authLimiter = rateLimit({
// Standard limiter for general authenticated API endpoints
export const apiLimiter = rateLimit({
windowMs: 60 * 1000, // 1 minute
max: 960,
max: 120,
standardHeaders: 'draft-7',
legacyHeaders: false,
keyGenerator: (req) => {
@@ -32,10 +30,10 @@ export const apiLimiter = rateLimit({
message: { error: 'too_many_requests', message: 'Rate limit exceeded', statusCode: 429 },
})
// Tight limiter for public marketplace and site endpoints (no auth)
// Limiter for public marketplace and site endpoints (no auth)
export const publicLimiter = rateLimit({
windowMs: 60 * 1000,
max: 960,
max: 60,
standardHeaders: 'draft-7',
legacyHeaders: false,
keyGenerator: (req) => getClientIpKey(req),
@@ -45,7 +43,7 @@ export const publicLimiter = rateLimit({
// Tight limiter for admin endpoints
export const adminLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 960,
max: 100,
standardHeaders: 'draft-7',
legacyHeaders: false,
keyGenerator: (req) => getClientIpKey(req),
+79 -10
View File
@@ -2,6 +2,8 @@ services:
postgres:
image: postgres:16-alpine
restart: unless-stopped
networks:
- internal
environment:
POSTGRES_DB: rentaldrivego
POSTGRES_USER: postgres
@@ -17,6 +19,8 @@ services:
redis:
image: redis:7-alpine
restart: unless-stopped
networks:
- internal
volumes:
- redis_prod_data:/data
@@ -25,6 +29,8 @@ services:
context: .
dockerfile: Dockerfile.production
command: ["sh", "-c", "npm run db:deploy"]
networks:
- internal
depends_on:
postgres:
condition: service_healthy
@@ -37,6 +43,9 @@ services:
context: .
dockerfile: Dockerfile.production
command: ["npm", "run", "start", "--workspace", "@rentaldrivego/api"]
networks:
- internal
- traefik-proxy
depends_on:
postgres:
condition: service_healthy
@@ -46,62 +55,122 @@ services:
condition: service_completed_successfully
env_file:
- .env.docker.production
ports:
- "4000:4000"
restart: unless-stopped
labels:
- traefik.enable=true
- traefik.http.routers.api.rule=Host(`${API_DOMAIN}`)
- traefik.http.routers.api.entrypoints=websecure
- traefik.http.routers.api.tls.certresolver=letsencrypt
- traefik.http.services.api.loadbalancer.server.port=4000
marketplace:
build:
context: .
dockerfile: Dockerfile.production
command: ["npm", "run", "start", "--workspace", "@rentaldrivego/marketplace"]
networks:
- internal
- traefik-proxy
depends_on:
- api
env_file:
- .env.docker.production
ports:
- "3000:3000"
restart: unless-stopped
labels:
- traefik.enable=true
- traefik.http.routers.marketplace.rule=Host(`${MARKETPLACE_DOMAIN}`)
- traefik.http.routers.marketplace.entrypoints=websecure
- traefik.http.routers.marketplace.tls.certresolver=letsencrypt
- traefik.http.services.marketplace.loadbalancer.server.port=3000
dashboard:
build:
context: .
dockerfile: Dockerfile.production
command: ["npm", "run", "start", "--workspace", "@rentaldrivego/dashboard"]
networks:
- internal
- traefik-proxy
depends_on:
- api
env_file:
- .env.docker.production
ports:
- "3001:3001"
restart: unless-stopped
labels:
- traefik.enable=true
- traefik.http.routers.dashboard.rule=Host(`${DASHBOARD_DOMAIN}`)
- traefik.http.routers.dashboard.entrypoints=websecure
- traefik.http.routers.dashboard.tls.certresolver=letsencrypt
- traefik.http.services.dashboard.loadbalancer.server.port=3001
admin:
build:
context: .
dockerfile: Dockerfile.production
command: ["npm", "run", "start", "--workspace", "@rentaldrivego/admin"]
networks:
- internal
- traefik-proxy
depends_on:
- api
env_file:
- .env.docker.production
ports:
- "3002:3002"
restart: unless-stopped
labels:
- traefik.enable=true
- traefik.http.routers.admin.rule=Host(`${ADMIN_DOMAIN}`)
- traefik.http.routers.admin.entrypoints=websecure
- traefik.http.routers.admin.tls.certresolver=letsencrypt
- traefik.http.services.admin.loadbalancer.server.port=3002
public-site:
build:
context: .
dockerfile: Dockerfile.production
command: ["npm", "run", "start", "--workspace", "@rentaldrivego/public-site"]
networks:
- internal
- traefik-proxy
depends_on:
- api
env_file:
- .env.docker.production
ports:
- "3003:3003"
restart: unless-stopped
labels:
- traefik.enable=true
- traefik.http.routers.public-site.rule=Host(`${PUBLIC_SITE_DOMAIN}`)
- traefik.http.routers.public-site.entrypoints=websecure
- traefik.http.routers.public-site.tls.certresolver=letsencrypt
- traefik.http.services.public-site.loadbalancer.server.port=3003
pgmanage:
image: cmdpromptinc/pgmanage-enterprise:latest
restart: unless-stopped
user: "0:0"
networks:
- internal
- traefik-proxy
depends_on:
postgres:
condition: service_healthy
environment:
PGMANAGE_LISTEN_PORT: "8000"
volumes:
- pgmanage_prod_data:/appdata
labels:
- traefik.enable=true
- traefik.http.routers.pgmanage.rule=Host(`${PGMANAGE_DOMAIN}`)
- traefik.http.routers.pgmanage.entrypoints=websecure
- traefik.http.routers.pgmanage.tls.certresolver=letsencrypt
- traefik.http.services.pgmanage.loadbalancer.server.port=8000
networks:
internal:
driver: bridge
traefik-proxy:
external: true
volumes:
postgres_prod_data:
redis_prod_data:
pgmanage_prod_data:
+32
View File
@@ -0,0 +1,32 @@
services:
traefik:
image: traefik:latest
restart: unless-stopped
command:
- --api.dashboard=false
- --api.insecure=false
- --providers.docker=true
- --providers.docker.exposedbydefault=false
- --entrypoints.web.address=:80
- --entrypoints.websecure.address=:443
- --certificatesresolvers.letsencrypt.acme.tlschallenge=true
- --certificatesresolvers.letsencrypt.acme.email=${ACME_EMAIL}
- --certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json
- --entrypoints.web.http.redirections.entrypoint.to=websecure
- --entrypoints.web.http.redirections.entrypoint.scheme=https
- --providers.docker.network=traefik-proxy
ports:
- "80:80"
- "443:443"
networks:
- traefik-proxy
volumes:
- traefik-letsencrypt:/letsencrypt
- /var/run/docker.sock:/var/run/docker.sock:ro
volumes:
traefik-letsencrypt:
networks:
traefik-proxy:
external: true