diff --git a/.env.production.example b/.env.production.example new file mode 100644 index 0000000..df9f53a --- /dev/null +++ b/.env.production.example @@ -0,0 +1,45 @@ +NODE_ENV=production +PORT=3001 +APP_NAME=STATION BACKEND + +JWT_SECRET=replace-with-a-long-random-secret +ALLOWED_ORIGIN=https://station.drdnt.org +FRONTEND_URL=https://station.drdnt.org + +DATABASE_HOST=postgres +DATABASE_PORT=5432 +DATABASE_USER=stationDbUser +DATABASE_PASSWORD=replace-with-a-strong-database-password +DATABASE_NAME=stationDb + +REDIS_HOST=redis +REDIS_PORT=6379 +REDIS_PASSWORD=replace-with-a-strong-redis-password +USE_REDIS_CACHE=true + +REFRESH_TOKEN_CLEANUP_CRON=0 3 * * * + +THROTTLE_TTL_MS=60000 +THROTTLE_LIMIT=100 +AUTH_LOGIN_THROTTLE_TTL_MS=60000 +AUTH_LOGIN_THROTTLE_LIMIT=10 +AUTH_REGISTER_THROTTLE_TTL_MS=60000 +AUTH_REGISTER_THROTTLE_LIMIT=5 +AUTH_FORGOT_THROTTLE_TTL_MS=60000 +AUTH_FORGOT_THROTTLE_LIMIT=5 + +UEX_SYNC_ENABLED=true +UEX_CATEGORIES_SYNC_ENABLED=true +UEX_ITEMS_SYNC_ENABLED=true +UEX_LOCATIONS_SYNC_ENABLED=true +UEX_API_BASE_URL=https://uexcorp.space/api/2.0 +UEX_TIMEOUT_MS=60000 +UEX_BATCH_SIZE=100 +UEX_CONCURRENT_CATEGORIES=3 +UEX_RETRY_ATTEMPTS=3 +UEX_BACKOFF_BASE_MS=1000 +UEX_RATE_LIMIT_PAUSE_MS=2000 +UEX_ENDPOINTS_PAUSE_MS=2000 +UEX_API_KEY= + +STATION_VERSION=latest diff --git a/.gitignore b/.gitignore index 4ae6a7b..adf90bb 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ yarn-error.log* # Environment variables .env +.env.production # Logs logs/ diff --git a/backend/src/main.ts b/backend/src/main.ts index 1a0a583..7c8d726 100644 --- a/backend/src/main.ts +++ b/backend/src/main.ts @@ -17,6 +17,7 @@ import { HttpExceptionFilter } from './common/filters/http-exception.filter'; async function bootstrap() { const app = await NestFactory.create(AppModule); + app.enableShutdownHooks(); // Application configuration const configService = app.get(ConfigService); diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml new file mode 100644 index 0000000..1d23adb --- /dev/null +++ b/docker-compose.prod.yml @@ -0,0 +1,73 @@ +services: + backend: + image: ghcr.io/gitaddremote/station-backend:${STATION_VERSION:-latest} + restart: unless-stopped + env_file: + - .env.production + ports: + - '127.0.0.1:3001:3001' + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + healthcheck: + test: ['CMD', 'wget', '-qO-', 'http://localhost:3001/health'] + interval: 15s + timeout: 5s + retries: 3 + start_period: 30s + stop_grace_period: 30s + + frontend: + image: ghcr.io/gitaddremote/station-frontend:${STATION_VERSION:-latest} + restart: unless-stopped + ports: + - '127.0.0.1:3000:80' + healthcheck: + test: ['CMD', 'wget', '-qO-', 'http://localhost:80'] + interval: 15s + timeout: 5s + retries: 3 + start_period: 15s + + postgres: + image: postgres:16-alpine + restart: unless-stopped + env_file: + - .env.production + environment: + POSTGRES_USER: ${DATABASE_USER} + POSTGRES_PASSWORD: ${DATABASE_PASSWORD} + POSTGRES_DB: ${DATABASE_NAME} + volumes: + - postgres_data:/var/lib/postgresql/data + healthcheck: + test: + [ + 'CMD-SHELL', + 'pg_isready -U ${DATABASE_USER} -d ${DATABASE_NAME}', + ] + interval: 10s + timeout: 5s + retries: 5 + start_period: 10s + + redis: + image: redis:7-alpine + restart: unless-stopped + env_file: + - .env.production + command: sh -c 'redis-server --requirepass "$$REDIS_PASSWORD" --appendonly yes' + volumes: + - redis_data:/data + healthcheck: + test: ['CMD-SHELL', 'redis-cli -a "$$REDIS_PASSWORD" ping'] + interval: 10s + timeout: 3s + retries: 3 + start_period: 5s + +volumes: + postgres_data: + redis_data: diff --git a/infra/nginx/station.drdnt.org.conf b/infra/nginx/station.drdnt.org.conf new file mode 100644 index 0000000..86f734b --- /dev/null +++ b/infra/nginx/station.drdnt.org.conf @@ -0,0 +1,22 @@ +server { + listen 80; + server_name station.drdnt.org; + + location /api/ { + proxy_pass http://127.0.0.1:3001; + proxy_http_version 1.1; + 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; + } + + location / { + proxy_pass http://127.0.0.1:3000; + proxy_http_version 1.1; + 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; + } +} diff --git a/infra/scripts/deploy.sh b/infra/scripts/deploy.sh new file mode 100755 index 0000000..79375c4 --- /dev/null +++ b/infra/scripts/deploy.sh @@ -0,0 +1,7 @@ +#!/bin/bash +set -euo pipefail + +cd /opt/station +docker compose --env-file .env.production -f docker-compose.prod.yml pull +docker compose --env-file .env.production -f docker-compose.prod.yml up -d --no-deps backend frontend +docker compose --env-file .env.production -f docker-compose.prod.yml ps