From 5682da87aa31809b4755d4218e9b27eb6e6bc81b Mon Sep 17 00:00:00 2001 From: Admin Date: Sun, 3 May 2026 20:15:39 +0330 Subject: [PATCH 1/2] improvement: docker infrastructure overhauled. --- .gitea/workflows/registry-build-deploy.yml | 117 ++++++++++++++++++++ .gitignore | 6 +- backend/Dockerfile | 44 +------- backend/docker-entrypoint.sh | 82 ++++---------- backend/src/app.controller.ts | 9 ++ frontend/Dockerfile | 63 ++++------- frontend/next.config.ts | 30 ++++- frontend/package.json | 4 +- infrastructure/.env.example | 5 +- infrastructure/backend.staging.env.example | 12 ++ infrastructure/database.staging.env.example | 4 + infrastructure/deploy.registry.env.example | 19 ++++ infrastructure/docker-compose.prod.yml | 4 +- infrastructure/docker-compose.registry.yml | 105 ++++++++++++++++++ infrastructure/docker-compose.staging.yml | 107 ++++++++++++++++++ infrastructure/docker-compose.yml | 16 +-- infrastructure/env.docker.example | 6 + infrastructure/env.staging.example | 13 +++ infrastructure/nginx/Dockerfile | 14 +-- infrastructure/nginx/http-only.conf | 54 +++++++++ 20 files changed, 542 insertions(+), 172 deletions(-) create mode 100644 .gitea/workflows/registry-build-deploy.yml create mode 100644 infrastructure/backend.staging.env.example create mode 100644 infrastructure/database.staging.env.example create mode 100644 infrastructure/deploy.registry.env.example create mode 100644 infrastructure/docker-compose.registry.yml create mode 100644 infrastructure/docker-compose.staging.yml create mode 100644 infrastructure/env.docker.example create mode 100644 infrastructure/env.staging.example create mode 100644 infrastructure/nginx/http-only.conf diff --git a/.gitea/workflows/registry-build-deploy.yml b/.gitea/workflows/registry-build-deploy.yml new file mode 100644 index 0000000..36e2d83 --- /dev/null +++ b/.gitea/workflows/registry-build-deploy.yml @@ -0,0 +1,117 @@ +# Build backend/frontend images, push to Gitea Container Registry, deploy with pull-only compose. +# +# Repository Variables (Settings → Actions → Variables) — non-secret: +# REGISTRY_HOST e.g. 178.131.50.201:3000 (no http/https) +# REGISTRY_OWNER Gitea user or org that owns the packages (same as image namespace) +# PUBLIC_BASE_URL URL users open in browser, e.g. http://178.131.50.201:8088 (no trailing slash) +# +# Repository Secrets (Settings → Actions → Secrets): +# REGISTRY_USERNAME Gitea username for docker login +# REGISTRY_PASSWORD Gitea access token (packages:read/write) or account password +# +# Optional: +# STAGING_HTTP_PORT host port for nginx (default 8088) +# +# Required for deploy job (absolute path on the runner host — forward slashes ok on Windows): +# DEPLOY_SECRETS_DIR folder containing database.staging.env + backend.staging.env +# +# Runner: self-hosted with Docker; Git Bash recommended on Windows (shell: bash). + +name: Registry — build, push, deploy + +on: + push: + branches: [main, master] + workflow_dispatch: + +defaults: + run: + shell: bash + +jobs: + build-and-push: + runs-on: self-hosted + outputs: + image_tag: ${{ steps.meta.outputs.image_tag }} + steps: + - name: Checkout + uses: https://gitea.com/actions/checkout@v4 + + - name: Image tag and registry prefix + id: meta + run: | + echo "image_tag=$(git rev-parse --short HEAD)" >> "$GITHUB_OUTPUT" + echo "REGISTRY_PREFIX=${{ vars.REGISTRY_HOST }}/${{ vars.REGISTRY_OWNER }}" >> "$GITHUB_ENV" + + - name: Log in to container registry + run: | + echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login "${{ vars.REGISTRY_HOST }}" \ + -u "${{ secrets.REGISTRY_USERNAME }}" --password-stdin + + - name: Build and push backend + run: | + TAG="${{ steps.meta.outputs.image_tag }}" + docker build \ + -t "${REGISTRY_PREFIX}/dyolink-backend:${TAG}" \ + -t "${REGISTRY_PREFIX}/dyolink-backend:latest" \ + ./backend + docker push "${REGISTRY_PREFIX}/dyolink-backend:${TAG}" + docker push "${REGISTRY_PREFIX}/dyolink-backend:latest" + + - name: Build and push frontend + env: + PUBLIC_BASE_URL: ${{ vars.PUBLIC_BASE_URL }} + run: | + TAG="${{ steps.meta.outputs.image_tag }}" + docker build \ + --build-arg NEXT_PUBLIC_API_URL="${PUBLIC_BASE_URL}/api" \ + --build-arg NEXT_PUBLIC_APP_URL="${PUBLIC_BASE_URL}" \ + --build-arg NEXT_PUBLIC_APP_NAME="Dyolink" \ + -t "${REGISTRY_PREFIX}/dyolink-frontend:${TAG}" \ + -t "${REGISTRY_PREFIX}/dyolink-frontend:latest" \ + ./frontend + docker push "${REGISTRY_PREFIX}/dyolink-frontend:${TAG}" + docker push "${REGISTRY_PREFIX}/dyolink-frontend:latest" + + deploy: + needs: build-and-push + runs-on: self-hosted + steps: + - name: Checkout infrastructure only + uses: https://gitea.com/actions/checkout@v4 + with: + sparse-checkout: | + infrastructure + sparse-checkout-cone-mode: true + + - name: Write deploy.registry.env and validate secrets path + run: | + SD='${{ vars.DEPLOY_SECRETS_DIR }}' + if [ -z "$SD" ]; then + echo "Set repository variable DEPLOY_SECRETS_DIR to the absolute path on this runner" + echo "where database.staging.env and backend.staging.env live (not in git)." + exit 1 + fi + test -f "${SD}/database.staging.env" || { echo "Missing ${SD}/database.staging.env"; exit 1; } + test -f "${SD}/backend.staging.env" || { echo "Missing ${SD}/backend.staging.env"; exit 1; } + cd infrastructure + STAGING_PORT='${{ vars.STAGING_HTTP_PORT }}' + STAGING_PORT="${STAGING_PORT:-8088}" + { + echo "REGISTRY_PREFIX=${{ vars.REGISTRY_HOST }}/${{ vars.REGISTRY_OWNER }}" + echo "IMAGE_TAG=${{ needs.build-and-push.outputs.image_tag }}" + echo "STAGING_HTTP_PORT=${STAGING_PORT}" + echo "DEPLOY_SECRETS_DIR=${SD}" + } > deploy.registry.env + + - name: Log in to container registry (for pull) + run: | + echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login "${{ vars.REGISTRY_HOST }}" \ + -u "${{ secrets.REGISTRY_USERNAME }}" --password-stdin + + - name: Pull and start stack + run: | + set -e + cd infrastructure + docker compose -f docker-compose.registry.yml --env-file deploy.registry.env pull backend frontend + docker compose -f docker-compose.registry.yml --env-file deploy.registry.env up -d diff --git a/.gitignore b/.gitignore index 5fb0c99..4c12859 100644 --- a/.gitignore +++ b/.gitignore @@ -82,8 +82,10 @@ secrets/ *.db *.sqlite3 -# === Gitea specific === -.gitea/ +# === Gitea specific (keep Actions workflows; ignore other local .gitea noise) === +.gitea/* +!.gitea/workflows +!.gitea/workflows/** # Prisma generated files prisma/*.db diff --git a/backend/Dockerfile b/backend/Dockerfile index e894c77..2af1755 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -1,92 +1,58 @@ # ============================================ # STAGE 1: BUILDER STAGE # ============================================ -# This stage builds the application and prepares assets FROM node:18-alpine AS builder -# Set working directory WORKDIR /app -# Copy package.json and package-lock.json first (for better caching) COPY package*.json ./ - -# Copy Prisma schema (needed for Prisma client generation) COPY prisma ./prisma/ -# Install ALL dependencies (including dev dependencies for build) RUN npm ci -# Copy source code COPY . . -# Generate Prisma client RUN npx prisma generate - -# Build the NestJS application RUN npm run build - -# Remove development dependencies to reduce size RUN npm prune --production # ============================================ # STAGE 2: PRODUCTION STAGE # ============================================ -# This stage creates the final production image FROM node:18-alpine -# Install dumb-init for proper signal handling RUN apk add --no-cache dumb-init -# Set working directory WORKDIR /app -# Create non-root user for security RUN addgroup -g 1001 -S nodejs && \ adduser -S dyolink -u 1001 -# Copy package.json files COPY package*.json ./ - -# Copy Prisma schema COPY prisma ./prisma/ -# Install ONLY production dependencies RUN npm ci --only=production && \ npm cache clean --force -# Generate Prisma client in production RUN npx prisma generate -# Copy built application from builder stage COPY --from=builder /app/dist ./dist - -# Copy node_modules (already pruned) COPY --from=builder /app/node_modules ./node_modules -# Create necessary directories with proper permissions +COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh +RUN chmod +x /usr/local/bin/docker-entrypoint.sh + RUN mkdir -p /app/logs && \ chown -R dyolink:nodejs /app -# Set ownership of all files to non-root user -RUN chown -R dyolink:nodejs /app - -# Switch to non-root user USER dyolink -# Expose the application port EXPOSE 3000 -# Health check configuration HEALTHCHECK --interval=30s --timeout=5s --start-period=40s --retries=3 \ - CMD node -e "require('http').get('http://localhost:3000/api/health', (r) => {if(r.statusCode!==200)throw new Error()})" || exit 1 + CMD node -e "require('http').get('http://127.0.0.1:3000/api/health', (r) => {if(r.statusCode!==200)process.exit(1)})" -# Copy entrypoint script -COPY docker-entrypoint.sh /usr/local/bin/ -RUN chmod +x /usr/local/bin/docker-entrypoint.sh - -# Use dumb-init to properly handle signals ENTRYPOINT ["dumb-init", "--", "docker-entrypoint.sh"] -# Start the application -CMD ["node", "dist/main"] \ No newline at end of file +CMD ["node", "dist/main"] diff --git a/backend/docker-entrypoint.sh b/backend/docker-entrypoint.sh index d15cbd3..6d41ead 100644 --- a/backend/docker-entrypoint.sh +++ b/backend/docker-entrypoint.sh @@ -1,76 +1,36 @@ #!/bin/sh set -e -# ============================================ -# DOCKER ENTRYPOINT SCRIPT -# This script runs BEFORE the application starts -# ============================================ +echo "==========================================" +echo " Dyolink Backend - Docker Entrypoint" +echo "==========================================" -# Colors for logging (optional, for better readability) -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -NC='\033[0m' # No Color - -echo "${GREEN}========================================${NC}" -echo "${GREEN} Dyolink Backend - Docker Entrypoint ${NC}" -echo "${GREEN}========================================${NC}" - -# Check if we're in development or production if [ "$NODE_ENV" = "production" ]; then - echo "${GREEN}Running in PRODUCTION mode${NC}" - - # Run database migrations - echo "${YELLOW}Running database migrations...${NC}" + echo "Running in PRODUCTION mode" + echo "Running database migrations..." npx prisma migrate deploy - - # Check if migrations were successful - if [ $? -eq 0 ]; then - echo "${GREEN}✓ Database migrations completed successfully${NC}" - else - echo "${RED}✗ Database migrations failed!${NC}" - exit 1 - fi else - echo "${YELLOW}Running in DEVELOPMENT mode${NC}" - - # In development, we might want to push schema instead of migrations - echo "${YELLOW}Syncing database schema...${NC}" + echo "Running in DEVELOPMENT mode" + echo "Syncing database schema..." npx prisma db push - - if [ $? -eq 0 ]; then - echo "${GREEN}✓ Database schema synced successfully${NC}" - else - echo "${RED}✗ Database schema sync failed!${NC}" - exit 1 +fi + +if [ "$NODE_ENV" != "production" ]; then + if [ -f "prisma/seed.ts" ] || [ -f "prisma/seed.js" ]; then + echo "Running database seed..." + npx prisma db seed fi fi -# Optional: Run seed script if it exists and NODE_ENV is not production -if [ "$NODE_ENV" != "production" ] && [ -f "prisma/seed.js" ]; then - echo "${YELLOW}Running database seed...${NC}" - npx prisma db seed - echo "${GREEN}✓ Database seeded successfully${NC}" -fi - -# Verify database connection -echo "${YELLOW}Verifying database connection...${NC}" -npx prisma db execute --file /dev/null --schema prisma/schema.prisma 2>/dev/null - -if [ $? -eq 0 ]; then - echo "${GREEN}✓ Database connection verified${NC}" -else - echo "${RED}✗ Cannot connect to database!${NC}" +echo "Verifying database connection..." +if ! echo "SELECT 1" | npx prisma db execute --stdin --schema prisma/schema.prisma >/dev/null 2>&1; then + echo "Cannot connect to database or execute query." exit 1 fi +echo "Database connection OK" -# Print application information -echo "${GREEN}========================================${NC}" -echo "${GREEN}Starting Dyolink Backend Application...${NC}" -echo "${GREEN} • Environment: ${NODE_ENV:-development}${NC}" -echo "${GREEN} • Port: ${PORT:-3000}${NC}" -echo "${GREEN} • Database: ${DATABASE_URL%%@*}@***${NC}" -echo "${GREEN}========================================${NC}" +echo "Starting Dyolink Backend..." +echo " Environment: ${NODE_ENV:-development}" +echo " Port: ${PORT:-3000}" -# Execute the main command (passed as CMD) -exec "$@" \ No newline at end of file +exec "$@" diff --git a/backend/src/app.controller.ts b/backend/src/app.controller.ts index 969a91f..908a24b 100644 --- a/backend/src/app.controller.ts +++ b/backend/src/app.controller.ts @@ -22,4 +22,13 @@ export class AppController { getHello(): string { return this.appService.getHello(); } + + /** Used by Docker / load balancer health checks (GET /api/health) */ + @Get('health') + health() { + return { + status: 'ok', + timestamp: new Date().toISOString(), + }; + } } \ No newline at end of file diff --git a/frontend/Dockerfile b/frontend/Dockerfile index e785209..42780b4 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -1,72 +1,53 @@ -# Build stage +# Build stage — produces `.next/standalone` (see next.config.ts output: standalone) FROM node:18-alpine AS builder WORKDIR /app -# Copy package files COPY package*.json ./ RUN npm ci -# Copy source code COPY . . -# Set build-time environment variables +ARG NEXT_PUBLIC_API_URL +ARG NEXT_PUBLIC_APP_URL +ARG NEXT_PUBLIC_APP_NAME + ENV NEXT_TELEMETRY_DISABLED=1 ENV NODE_ENV=production +ENV NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL} +ENV NEXT_PUBLIC_APP_URL=${NEXT_PUBLIC_APP_URL} +ENV NEXT_PUBLIC_APP_NAME=${NEXT_PUBLIC_APP_NAME} -# Build Next.js application RUN npm run build -# Production stage -FROM node:18-alpine +# Production — minimal runtime using Next.js standalone bundle +FROM node:18-alpine AS runner + +RUN apk add --no-cache dumb-init WORKDIR /app -# Install dumb-init for proper signal handling -RUN apk add --no-cache dumb-init - -# Create non-root user RUN addgroup -g 1001 -S nodejs && \ adduser -S dyolink -u 1001 -# Copy package files -COPY package*.json ./ +ENV NODE_ENV=production +ENV PORT=3000 +ENV HOSTNAME=0.0.0.0 -# Install production dependencies only -RUN npm ci --only=production && \ - npm cache clean --force - -# Copy built application -COPY --from=builder /app/.next ./.next COPY --from=builder /app/public ./public -COPY --from=builder /app/next.config.js ./next.config.js -COPY --from=builder /app/package.json ./package.json +COPY --from=builder --chown=dyolink:nodejs /app/.next/standalone ./ +COPY --from=builder --chown=dyolink:nodejs /app/.next/static ./.next/static -# Create logs directory -RUN mkdir -p /app/logs && \ - chown -R dyolink:nodejs /app +COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh +RUN chmod +x /usr/local/bin/docker-entrypoint.sh -# Set ownership -RUN chown -R dyolink:nodejs /app - -# Switch to non-root user USER dyolink -# Health check -HEALTHCHECK --interval=30s --timeout=5s --start-period=40s --retries=3 \ - CMD node -e "require('http').get('http://localhost:3000', (r) => {if(r.statusCode!==200)throw new Error()})" || exit 1 - EXPOSE 3000 -ENV PORT=3000 -ENV HOSTNAME="0.0.0.0" -ENV NODE_ENV=production +HEALTHCHECK --interval=30s --timeout=5s --start-period=40s --retries=3 \ + CMD node -e "require('http').get('http://127.0.0.1:3000/', (r) => {if(r.statusCode!==200)process.exit(1)})" -# Copy entrypoint script -COPY docker-entrypoint.sh /usr/local/bin/ -RUN chmod +x /usr/local/bin/docker-entrypoint.sh - -# Use dumb-init for signal handling ENTRYPOINT ["dumb-init", "--", "docker-entrypoint.sh"] -CMD ["npm", "start"] \ No newline at end of file +CMD ["node", "server.js"] diff --git a/frontend/next.config.ts b/frontend/next.config.ts index cb6f00d..f18b314 100644 --- a/frontend/next.config.ts +++ b/frontend/next.config.ts @@ -1,6 +1,17 @@ import type { NextConfig } from "next"; -// frontend/next.config.js +function publicAppHostname(): string | null { + const url = process.env.NEXT_PUBLIC_APP_URL; + if (!url) return null; + try { + return new URL(url).hostname; + } catch { + return null; + } +} + +const appHost = publicAppHostname(); + /** @type {import('next').NextConfig} */ const nextConfig = { // Enable React strict mode @@ -9,12 +20,19 @@ const nextConfig = { // Disable x-powered-by header for security poweredByHeader: false, - // Configure allowed remote image sources + // Configure allowed remote image sources (hostname derived from NEXT_PUBLIC_APP_URL at build time) images: { - remotePatterns: - process.env.NODE_ENV === 'production' - ? [{ protocol: 'https', hostname: 'yourdomain.com' }] - : [{ protocol: 'http', hostname: 'localhost' }], + remotePatterns: [ + { protocol: "http", hostname: "localhost" }, + ...(appHost + ? [ + { protocol: "http" as const, hostname: appHost }, + { protocol: "https" as const, hostname: appHost }, + ] + : []), + { protocol: "https", hostname: "dyolink.com" }, + { protocol: "https", hostname: "www.dyolink.com" }, + ], }, // Environment variables that will be available at build time diff --git a/frontend/package.json b/frontend/package.json index f07681e..b451fcc 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -3,9 +3,9 @@ "version": "0.1.0", "private": true, "scripts": { - "dev": "next dev -p 3001", + "dev": "next dev -p 3000", "build": "next build", - "start": "next start -p 3001", + "start": "next start -p 3000", "lint": "next lint" }, "dependencies": { diff --git a/infrastructure/.env.example b/infrastructure/.env.example index 6f9a10f..d96c8b7 100644 --- a/infrastructure/.env.example +++ b/infrastructure/.env.example @@ -19,4 +19,7 @@ DOMAIN=dyolink.com # Frontend Environment (create frontend.env from this) # NEXT_PUBLIC_API_URL=/api # NEXT_PUBLIC_APP_NAME=Dyolink -# NEXT_PUBLIC_APP_URL=https://dyolink.com \ No newline at end of file +# NEXT_PUBLIC_APP_URL=https://dyolink.com + +# --- Staging on your server (docker-compose.staging.yml) --- +# See env.staging.example, database.staging.env.example, backend.staging.env.example \ No newline at end of file diff --git a/infrastructure/backend.staging.env.example b/infrastructure/backend.staging.env.example new file mode 100644 index 0000000..19f6ab8 --- /dev/null +++ b/infrastructure/backend.staging.env.example @@ -0,0 +1,12 @@ +# Copy to backend.staging.env — DATABASE_URL must match database.staging.env credentials. +NODE_ENV=production +PORT=3000 + +DATABASE_URL=postgresql://postgres:changeme_staging_strong_password@postgres:5432/dyolink_db + +JWT_SECRET=replace_with_a_long_random_secret +JWT_EXPIRES_IN=15m +JWT_REFRESH_SECRET=another_long_random_secret_different_from_JWT_SECRET +JWT_REFRESH_EXPIRES_IN=30d + +FRONTEND_URL=http://178.131.50.201:8088 diff --git a/infrastructure/database.staging.env.example b/infrastructure/database.staging.env.example new file mode 100644 index 0000000..50a2665 --- /dev/null +++ b/infrastructure/database.staging.env.example @@ -0,0 +1,4 @@ +# Copy to database.staging.env (do not commit real passwords). +POSTGRES_USER=postgres +POSTGRES_PASSWORD=changeme_staging_strong_password +POSTGRES_DB=dyolink_db diff --git a/infrastructure/deploy.registry.env.example b/infrastructure/deploy.registry.env.example new file mode 100644 index 0000000..4e8eb3b --- /dev/null +++ b/infrastructure/deploy.registry.env.example @@ -0,0 +1,19 @@ +# Template for manual pull-only deploy (when not using CI-generated deploy.registry.env). +# CI workflow generates this file automatically; you normally only need secrets on disk. +# +# docker compose -f docker-compose.registry.yml --env-file deploy.registry.env up -d + +# --- Registry boundary (swap when moving Gitea → Docker Hub) --- +# Gitea: REGISTRY_PREFIX = :/ +# Hub: REGISTRY_PREFIX = docker.io/ (or your username for implicit hub) +REGISTRY_PREFIX=178.131.50.201:3000/yourgiteauser + +# Short git SHA from CI, or "latest" after a manual pull of :latest +IMAGE_TAG=latest + +# Host port published for nginx (URL = http://:) +STAGING_HTTP_PORT=8088 + +# Absolute path on the server where database.staging.env and backend.staging.env live. +# Use forward slashes on Windows. Same variable as Gitea Actions → DEPLOY_SECRETS_DIR. +# DEPLOY_SECRETS_DIR=D:/dyolink/secrets diff --git a/infrastructure/docker-compose.prod.yml b/infrastructure/docker-compose.prod.yml index 2b99ade..b079859 100644 --- a/infrastructure/docker-compose.prod.yml +++ b/infrastructure/docker-compose.prod.yml @@ -53,7 +53,7 @@ services: max-size: "10m" max-file: "3" healthcheck: - test: ["CMD", "node", "-e", "require('http').get('http://localhost:3000/api/health', (r) => {if(r.statusCode!==200)process.exit(1)})"] + test: ["CMD", "node", "-e", "require('http').get('http://127.0.0.1:3000/api/health', (r) => {if(r.statusCode!==200)process.exit(1)})"] interval: 30s timeout: 10s retries: 3 @@ -81,7 +81,7 @@ services: max-size: "10m" max-file: "3" healthcheck: - test: ["CMD", "node", "-e", "require('http').get('http://localhost:3000', (r) => {if(r.statusCode!==200)process.exit(1)})"] + test: ["CMD", "node", "-e", "require('http').get('http://127.0.0.1:3000/', (r) => {if(r.statusCode!==200)process.exit(1)})"] interval: 30s timeout: 10s retries: 3 diff --git a/infrastructure/docker-compose.registry.yml b/infrastructure/docker-compose.registry.yml new file mode 100644 index 0000000..acc2caf --- /dev/null +++ b/infrastructure/docker-compose.registry.yml @@ -0,0 +1,105 @@ +# Pull-only staging stack — uses images from a registry (Gitea Packages / Docker Hub / etc.). +# No backend/frontend source on the deployment host except this compose file + config + secrets. +# +# Required env (see deploy.registry.env.example): +# REGISTRY_PREFIX e.g. 178.131.50.201:3000/yourgiteauser (no protocol, no trailing slash) +# IMAGE_TAG short sha or "latest" (CI sets this per deploy) +# Optional: +# DEPLOY_SECRETS_DIR absolute path on the server to database/backend *.env files (see below) +# +# Deploy: +# docker compose -f docker-compose.registry.yml --env-file deploy.registry.env pull +# docker compose -f docker-compose.registry.yml --env-file deploy.registry.env up -d + +name: dyolink-registry + +services: + postgres: + image: postgres:15-alpine + container_name: dyolink_postgres_staging + env_file: + - ${DEPLOY_SECRETS_DIR:-.}/database.staging.env + environment: + TZ: UTC + volumes: + - postgres_data_staging:/var/lib/postgresql/data + - ./database/init.sql:/docker-entrypoint-initdb.d/init.sql:ro + - ./database/backups:/backups + networks: + - dyolink_staging + restart: unless-stopped + healthcheck: + test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER"] + interval: 15s + timeout: 10s + retries: 5 + start_period: 40s + + backend: + image: ${REGISTRY_PREFIX}/dyolink-backend:${IMAGE_TAG:-latest} + container_name: dyolink_backend_staging + depends_on: + postgres: + condition: service_healthy + env_file: + - ${DEPLOY_SECRETS_DIR:-.}/backend.staging.env + environment: + NODE_ENV: production + TZ: UTC + PORT: "3000" + expose: + - "3000" + networks: + - dyolink_staging + restart: unless-stopped + healthcheck: + test: ["CMD", "node", "-e", "require('http').get('http://127.0.0.1:3000/api/health', (r) => {if(r.statusCode!==200)process.exit(1)})"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 60s + + frontend: + image: ${REGISTRY_PREFIX}/dyolink-frontend:${IMAGE_TAG:-latest} + container_name: dyolink_frontend_staging + depends_on: + - backend + environment: + NODE_ENV: production + TZ: UTC + PORT: "3000" + HOSTNAME: "0.0.0.0" + expose: + - "3000" + networks: + - dyolink_staging + restart: unless-stopped + healthcheck: + test: ["CMD", "node", "-e", "require('http').get('http://127.0.0.1:3000/', (r) => {if(r.statusCode!==200)process.exit(1)})"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 60s + + nginx: + image: nginx:alpine + container_name: dyolink_nginx_staging + depends_on: + - backend + - frontend + ports: + - "${STAGING_HTTP_PORT:-8088}:80" + volumes: + - ./nginx/http-only.conf:/etc/nginx/conf.d/default.conf:ro + - ./logs/nginx-staging:/var/log/nginx + networks: + - dyolink_staging + restart: unless-stopped + +networks: + dyolink_staging: + name: dyolink_staging + +volumes: + postgres_data_staging: + name: dyolink_postgres_data_staging diff --git a/infrastructure/docker-compose.staging.yml b/infrastructure/docker-compose.staging.yml new file mode 100644 index 0000000..81e7ef4 --- /dev/null +++ b/infrastructure/docker-compose.staging.yml @@ -0,0 +1,107 @@ +# Staging stack — builds images from local backend/frontend (needs full repo clone). +# For pull-only images + registry (no app source on server), use docker-compose.registry.yml +# and .gitea/workflows/registry-build-deploy.yml instead. +# +# From this directory: +# docker compose -f docker-compose.staging.yml --env-file .env.staging up -d --build + +name: dyolink-staging + +services: + postgres: + image: postgres:15-alpine + container_name: dyolink_postgres_staging + env_file: + - database.staging.env + environment: + TZ: UTC + volumes: + - postgres_data_staging:/var/lib/postgresql/data + - ./database/init.sql:/docker-entrypoint-initdb.d/init.sql:ro + - ./database/backups:/backups + networks: + - dyolink_staging + restart: unless-stopped + healthcheck: + test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER"] + interval: 15s + timeout: 10s + retries: 5 + start_period: 40s + + backend: + build: + context: ../backend + dockerfile: Dockerfile + container_name: dyolink_backend_staging + depends_on: + postgres: + condition: service_healthy + env_file: + - backend.staging.env + environment: + NODE_ENV: production + TZ: UTC + PORT: "3000" + expose: + - "3000" + networks: + - dyolink_staging + restart: unless-stopped + healthcheck: + test: ["CMD", "node", "-e", "require('http').get('http://127.0.0.1:3000/api/health', (r) => {if(r.statusCode!==200)process.exit(1)})"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 60s + + frontend: + build: + context: ../frontend + dockerfile: Dockerfile + args: + NEXT_PUBLIC_API_URL: ${STAGING_NEXT_PUBLIC_API_URL:-http://178.131.50.201:8088/api} + NEXT_PUBLIC_APP_URL: ${STAGING_NEXT_PUBLIC_APP_URL:-http://178.131.50.201:8088} + NEXT_PUBLIC_APP_NAME: ${STAGING_NEXT_PUBLIC_APP_NAME:-Dyolink} + container_name: dyolink_frontend_staging + depends_on: + - backend + environment: + NODE_ENV: production + TZ: UTC + PORT: "3000" + HOSTNAME: "0.0.0.0" + expose: + - "3000" + networks: + - dyolink_staging + restart: unless-stopped + healthcheck: + test: ["CMD", "node", "-e", "require('http').get('http://127.0.0.1:3000/', (r) => {if(r.statusCode!==200)process.exit(1)})"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 60s + + nginx: + image: nginx:alpine + container_name: dyolink_nginx_staging + depends_on: + - backend + - frontend + ports: + - "${STAGING_HTTP_PORT:-8088}:80" + volumes: + - ./nginx/http-only.conf:/etc/nginx/conf.d/default.conf:ro + - ./logs/nginx-staging:/var/log/nginx + networks: + - dyolink_staging + restart: unless-stopped + +networks: + dyolink_staging: + name: dyolink_staging + +volumes: + postgres_data_staging: + name: dyolink_postgres_data_staging diff --git a/infrastructure/docker-compose.yml b/infrastructure/docker-compose.yml index 23a69e3..60c04b6 100644 --- a/infrastructure/docker-compose.yml +++ b/infrastructure/docker-compose.yml @@ -1,11 +1,14 @@ +# Copy .env.docker.example to .env.docker and adjust (optional). +# Defaults below are for local development only. + services: postgres: image: postgres:15-alpine container_name: dyolink_db_container environment: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: 1234 - POSTGRES_DB: dyolink_db + POSTGRES_USER: ${POSTGRES_USER:-postgres} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-dyolink_dev_change_me} + POSTGRES_DB: ${POSTGRES_DB:-dyolink_db} ports: - "5433:5432" volumes: @@ -16,7 +19,7 @@ services: - dyolink_network restart: unless-stopped healthcheck: - test: ["CMD-SHELL", "pg_isready -U postgres"] + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-postgres}"] interval: 10s timeout: 5s retries: 5 @@ -30,7 +33,7 @@ services: env_file: - ../backend/.env environment: - - DATABASE_URL=postgresql://postgres:1234@postgres:5432/dyolink_db + - DATABASE_URL=postgresql://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-dyolink_dev_change_me}@postgres:5432/${POSTGRES_DB:-dyolink_db} - FRONTEND_URL=http://frontend:3000 - PORT=3000 ports: @@ -73,10 +76,7 @@ services: - frontend ports: - "8080:80" - - "8443:443" volumes: - - ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf:ro - - ./ssl:/etc/nginx/ssl:ro - ./logs/nginx:/var/log/nginx networks: - dyolink_network diff --git a/infrastructure/env.docker.example b/infrastructure/env.docker.example new file mode 100644 index 0000000..cf2b266 --- /dev/null +++ b/infrastructure/env.docker.example @@ -0,0 +1,6 @@ +# Optional: save as .env next to infrastructure/docker-compose.yml +# Docker Compose reads this file automatically for variable substitution. + +POSTGRES_USER=postgres +POSTGRES_PASSWORD=dyolink_dev_change_me +POSTGRES_DB=dyolink_db diff --git a/infrastructure/env.staging.example b/infrastructure/env.staging.example new file mode 100644 index 0000000..afa238a --- /dev/null +++ b/infrastructure/env.staging.example @@ -0,0 +1,13 @@ +# Copy to .env.staging next to docker-compose.staging.yml (optional). +# Used only for compose variable substitution (build args, host port). + +STAGING_HTTP_PORT=8088 + +# Public URLs baked into the frontend image at build time — must match how users open the app. +STAGING_NEXT_PUBLIC_API_URL=http://178.131.50.201:8088/api +STAGING_NEXT_PUBLIC_APP_URL=http://178.131.50.201:8088 +STAGING_NEXT_PUBLIC_APP_NAME=Dyolink + +# Change the IP/port if your server address differs. + +# Registry / pull-only deploy (see deploy.registry.env.example + docker-compose.registry.yml). diff --git a/infrastructure/nginx/Dockerfile b/infrastructure/nginx/Dockerfile index a2a3e3f..2051f8a 100644 --- a/infrastructure/nginx/Dockerfile +++ b/infrastructure/nginx/Dockerfile @@ -1,19 +1,13 @@ FROM nginx:alpine -# Remove default configuration -RUN rm /etc/nginx/conf.d/default.conf +RUN rm -f /etc/nginx/conf.d/default.conf -# Copy custom configuration -COPY nginx.conf /etc/nginx/conf.d/ +COPY http-only.conf /etc/nginx/conf.d/default.conf -# Create log directory RUN mkdir -p /var/log/nginx && \ chown -R nginx:nginx /var/log/nginx && \ chmod -R 755 /var/log/nginx -# Switch to non-root user -USER nginx +EXPOSE 80 -EXPOSE 80 443 - -CMD ["nginx", "-g", "daemon off;"] \ No newline at end of file +CMD ["nginx", "-g", "daemon off;"] diff --git a/infrastructure/nginx/http-only.conf b/infrastructure/nginx/http-only.conf new file mode 100644 index 0000000..133c685 --- /dev/null +++ b/infrastructure/nginx/http-only.conf @@ -0,0 +1,54 @@ +# HTTP only — local dev and IP-based staging (no TLS). +# Use with: docker compose and map host port e.g. 8080:80 or 8088:80 + +upstream dyolink_backend { + server backend:3000; + keepalive 32; +} + +upstream dyolink_frontend { + server frontend:3000; + keepalive 32; +} + +server { + listen 80; + listen [::]:80; + server_name _; + + client_max_body_size 50M; + + location / { + proxy_pass http://dyolink_frontend; + 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; + proxy_read_timeout 300; + proxy_connect_timeout 300; + } + + location /api { + proxy_pass http://dyolink_backend; + 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; + proxy_read_timeout 300; + proxy_connect_timeout 300; + } + + location /health { + access_log off; + return 200 "healthy\n"; + add_header Content-Type text/plain; + } +} -- 2.53.0.windows.1 From 4c61885cef82706c74adb12a8b8a775015fb4f2c Mon Sep 17 00:00:00 2001 From: Admin Date: Sun, 3 May 2026 20:29:58 +0330 Subject: [PATCH 2/2] improvement: a port issue for local dev fixed. --- frontend/package.json | 2 +- infrastructure/docker-compose.yml | 6 +-- infrastructure/nginx/http-only.dev.conf | 54 +++++++++++++++++++++++++ 3 files changed, 58 insertions(+), 4 deletions(-) create mode 100644 infrastructure/nginx/http-only.dev.conf diff --git a/frontend/package.json b/frontend/package.json index b451fcc..2cf888b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -3,7 +3,7 @@ "version": "0.1.0", "private": true, "scripts": { - "dev": "next dev -p 3000", + "dev": "next dev -p 3001", "build": "next build", "start": "next start -p 3000", "lint": "next lint" diff --git a/infrastructure/docker-compose.yml b/infrastructure/docker-compose.yml index 60c04b6..adf9977 100644 --- a/infrastructure/docker-compose.yml +++ b/infrastructure/docker-compose.yml @@ -34,7 +34,7 @@ services: - ../backend/.env environment: - DATABASE_URL=postgresql://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-dyolink_dev_change_me}@postgres:5432/${POSTGRES_DB:-dyolink_db} - - FRONTEND_URL=http://frontend:3000 + - FRONTEND_URL=http://localhost:4000 - PORT=3000 ports: - "4001:3000" @@ -56,9 +56,8 @@ services: environment: - NEXT_PUBLIC_API_URL=http://localhost:4001/api - NEXT_PUBLIC_APP_URL=http://localhost:4000 - - PORT=3000 ports: - - "4000:3000" + - "4000:3001" volumes: - ../frontend:/app:rw - /app/node_modules @@ -77,6 +76,7 @@ services: ports: - "8080:80" volumes: + - ./nginx/http-only.dev.conf:/etc/nginx/conf.d/default.conf:ro - ./logs/nginx:/var/log/nginx networks: - dyolink_network diff --git a/infrastructure/nginx/http-only.dev.conf b/infrastructure/nginx/http-only.dev.conf new file mode 100644 index 0000000..6c6197d --- /dev/null +++ b/infrastructure/nginx/http-only.dev.conf @@ -0,0 +1,54 @@ +# Dev docker-compose only: local `npm run dev` uses port 3001 (see frontend package.json). +# Staging / registry stacks use http-only.conf (frontend:3000). + +upstream dyolink_backend { + server backend:3000; + keepalive 32; +} + +upstream dyolink_frontend { + server frontend:3001; + keepalive 32; +} + +server { + listen 80; + listen [::]:80; + server_name _; + + client_max_body_size 50M; + + location / { + proxy_pass http://dyolink_frontend; + 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; + proxy_read_timeout 300; + proxy_connect_timeout 300; + } + + location /api { + proxy_pass http://dyolink_backend; + 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; + proxy_read_timeout 300; + proxy_connect_timeout 300; + } + + location /health { + access_log off; + return 200 "healthy\n"; + add_header Content-Type text/plain; + } +} -- 2.53.0.windows.1