fix: CI/CD pipeline, dynamic API URLs, Dockerfile fixes
All checks were successful
Deploy Canina / deploy (push) Successful in 5m39s

- Backend Dockerfile: add chown for node_modules (Prisma permissions fix)
- Frontend Dockerfile: add ARG/ENV for NEXT_PUBLIC_API_URL, VITE_API_URL
- nginx.conf: template with __PLACEHOLDERS__ for per-env substitution
- deploy.yml: SSH keepalive, sudo for VPN
- api.ts: dynamic API URL via env vars instead of hardcoded
- next.config.ts: rewrite only in development mode
- scripts: deploy.sh, compose.stage.yml, compose.prod.yml
This commit is contained in:
parsa aghaei 2026-07-28 16:35:07 +03:30
parent ee65d87899
commit 501addc909
10 changed files with 251 additions and 14 deletions

View File

@ -23,8 +23,12 @@ jobs:
- name: Deploy
shell: sh
timeout-minutes: 15
timeout-minutes: 12
run: |
BRANCH="${GITHUB_REF_NAME}"
echo "Deploying branch: ${BRANCH}"
ssh -o StrictHostKeyChecking=no -p 2234 devops@172.17.0.1 "bash /opt/services/scripts/depcanina.sh ${BRANCH}"
ssh -o StrictHostKeyChecking=no \
-o ServerAliveInterval=30 \
-o ServerAliveCountMax=30 \
-p 2234 devops@172.17.0.1 \
"sudo bash /opt/services/scripts/depcanina.sh ${BRANCH}"

View File

@ -5,6 +5,8 @@ ENV NEXT_PUBLIC_API_URL=https://stageapi.canina.ir/api
COPY frontend/application/package*.json ./
RUN npm ci --prefer-offline --no-audit
COPY frontend/application ./
ARG NEXT_PUBLIC_API_URL
ENV NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL
RUN npm run build
FROM node:20-alpine AS admin-builder
@ -13,6 +15,8 @@ ENV VITE_API_URL=https://stageapi.canina.ir/api
COPY frontend/admin-panel/package*.json ./
RUN npm ci --prefer-offline --no-audit
COPY frontend/admin-panel ./
ARG VITE_API_URL
ENV VITE_API_URL=$VITE_API_URL
RUN npm run build
FROM node:20-alpine

View File

@ -15,6 +15,7 @@ COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/prisma ./prisma
COPY --from=builder /app/prisma/scientificTerms.ts ./prisma/scientificTerms.ts
RUN chown -R node:node /app
EXPOSE 3000
USER node
CMD ["sh", "-c", "npx prisma migrate deploy && node dist/main"]

View File

@ -2,7 +2,7 @@ import axios from 'axios';
export const BASE_DOMAIN = import.meta.env.VITE_API_URL
? import.meta.env.VITE_API_URL.replace(/\/api$/, '')
: (import.meta.env.DEV ? 'http://127.0.0.1:4001' : 'https://api.canina.ir');
: (import.meta.env.DEV ? 'http://127.0.0.1:4001' : '');
const baseURL = `${BASE_DOMAIN}/api`;

View File

@ -1,6 +1,6 @@
import axios from 'axios';
const baseURL = process.env.NEXT_PUBLIC_API_URL || (process.env.NODE_ENV === 'development' ? 'http://localhost:4001/api' : 'https://api.canina.ir/api');
const baseURL = process.env.NEXT_PUBLIC_API_URL || (process.env.NODE_ENV === 'development' ? 'http://localhost:4001/api' : '/api');
const api = axios.create({
baseURL,

View File

@ -15,13 +15,18 @@ const nextConfig: NextConfig = {
],
},
async rewrites() {
return [
{
source: '/api/:path*',
destination: 'http://localhost:4001/api/:path*',
},
];
// Only proxy /api in development; in production nginx handles it
if (process.env.NODE_ENV === 'development') {
return [
{
source: '/api/:path*',
destination: 'http://localhost:4001/api/:path*',
},
];
}
return [];
},
};
export default nextConfig;

View File

@ -1,6 +1,6 @@
server {
listen 8080;
server_name canina.ir stage.canina.ir;
server_name __APP_HOST__;
# Security Headers
server_tokens off;
@ -21,18 +21,19 @@ server {
# Backend API
location /api {
proxy_pass http://backend:3000;
proxy_pass http://__BACKEND_HOST__:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
client_max_body_size 50M;
}
}
server {
listen 8080;
server_name admin.canina.ir stageadmin.canina.ir;
server_name __ADMIN_HOST__;
root /app/admin_static;
index index.html;
@ -51,11 +52,12 @@ server {
# Backend API for Admin
location /api {
proxy_pass http://backend:3000;
proxy_pass http://__BACKEND_HOST__:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
client_max_body_size 50M;
}
}

72
scripts/compose.prod.yml Normal file
View File

@ -0,0 +1,72 @@
version: '3.8'
services:
db_prod:
image: postgres:16-alpine
container_name: canino_db_prod
restart: always
environment:
POSTGRES_USER: canino_prod
POSTGRES_PASSWORD: caninopassword_prod
POSTGRES_DB: caninodb_prod
volumes:
- canina_prod_db:/var/lib/postgresql/data
networks:
- traefik_public
redis_prod:
image: redis:7-alpine
container_name: canino_redis_prod
restart: always
networks:
- traefik_public
backend_prod:
build:
context: ./backend
dockerfile: Dockerfile
container_name: canino_backend_prod
restart: always
environment:
- DATABASE_URL=postgresql://canino_prod:caninopassword_prod@db_prod:5432/caninodb_prod?schema=public
- REDIS_HOST=redis_prod
- REDIS_PORT=6379
- PORT=3000
labels:
- "traefik.enable=true"
- "traefik.http.routers.canina-api-prod.rule=Host(`api.canina.ir`)"
- "traefik.http.routers.canina-api-prod.entrypoints=websecure"
- "traefik.http.routers.canina-api-prod.tls=true"
- "traefik.http.services.canina-api-prod.loadbalancer.server.port=3000"
depends_on:
- db_prod
- redis_prod
networks:
- traefik_public
frontend_prod:
build:
context: .
dockerfile: Dockerfile
args:
NEXT_PUBLIC_API_URL: "https://api.canina.ir/api"
VITE_API_URL: "https://api.canina.ir"
container_name: canino_frontend_prod
restart: always
labels:
- "traefik.enable=true"
- "traefik.http.routers.canina-prod.rule=Host(`canina.ir`) || Host(`admin.canina.ir`)"
- "traefik.http.routers.canina-prod.entrypoints=websecure"
- "traefik.http.routers.canina-prod.tls=true"
- "traefik.http.services.canina-prod.loadbalancer.server.port=8080"
depends_on:
- backend_prod
networks:
- traefik_public
networks:
traefik_public:
external: true
volumes:
canina_prod_db:

72
scripts/compose.stage.yml Normal file
View File

@ -0,0 +1,72 @@
version: '3.8'
services:
db_stage:
image: postgres:16-alpine
container_name: canino_db_stage
restart: always
environment:
POSTGRES_USER: canino_stage
POSTGRES_PASSWORD: caninopassword_stage
POSTGRES_DB: caninodb_stage
volumes:
- canina_stage_db:/var/lib/postgresql/data
networks:
- traefik_public
redis_stage:
image: redis:7-alpine
container_name: canino_redis_stage
restart: always
networks:
- traefik_public
backend_stage:
build:
context: ./backend
dockerfile: Dockerfile
container_name: canino_backend_stage
restart: always
environment:
- DATABASE_URL=postgresql://canino_stage:caninopassword_stage@db_stage:5432/caninodb_stage?schema=public
- REDIS_HOST=redis_stage
- REDIS_PORT=6379
- PORT=3000
labels:
- "traefik.enable=true"
- "traefik.http.routers.canina-api-stage.rule=Host(`stageapi.canina.ir`)"
- "traefik.http.routers.canina-api-stage.entrypoints=websecure"
- "traefik.http.routers.canina-api-stage.tls=true"
- "traefik.http.services.canina-api-stage.loadbalancer.server.port=3000"
depends_on:
- db_stage
- redis_stage
networks:
- traefik_public
frontend_stage:
build:
context: .
dockerfile: Dockerfile
args:
NEXT_PUBLIC_API_URL: "https://stageapi.canina.ir/api"
VITE_API_URL: "https://stageapi.canina.ir"
container_name: canino_frontend_stage
restart: always
labels:
- "traefik.enable=true"
- "traefik.http.routers.canina-stage.rule=Host(`stage.canina.ir`) || Host(`stageadmin.canina.ir`)"
- "traefik.http.routers.canina-stage.entrypoints=websecure"
- "traefik.http.routers.canina-stage.tls=true"
- "traefik.http.services.canina-stage.loadbalancer.server.port=8080"
depends_on:
- backend_stage
networks:
- traefik_public
networks:
traefik_public:
external: true
volumes:
canina_stage_db:

77
scripts/deploy.sh Normal file
View File

@ -0,0 +1,77 @@
#!/bin/bash
set -e
export DOCKER_BUILDKIT=1
export COMPOSE_DOCKER_CLI_BUILD=1
BRANCH="${1:-develop}"
echo "Starting Deployment for branch: ${BRANCH}"
if [ "$BRANCH" == "develop" ]; then
COMPOSE_FILE="compose.stage.yml"
TARGET_DIR="/opt/services/canina/stage"
BACKEND_HOST="backend_stage"
APP_HOST="stage.canina.ir"
ADMIN_HOST="stageadmin.canina.ir"
API_URL="https://stageapi.canina.ir"
CONTAINER_PREFIX="canino_backend_stage"
else
COMPOSE_FILE="compose.prod.yml"
TARGET_DIR="/opt/services/canina/prod"
BACKEND_HOST="backend_prod"
APP_HOST="canina.ir"
ADMIN_HOST="admin.canina.ir"
API_URL="https://api.canina.ir"
CONTAINER_PREFIX="canino_backend_prod"
fi
mkdir -p "$TARGET_DIR"
cd "$TARGET_DIR"
if [ ! -d ".git" ]; then
echo "Cloning repository..."
git clone -b "$BRANCH" https://git.parsaaghayi.ir/parsa/canina.git .
else
echo "Pulling latest changes..."
git fetch origin "$BRANCH"
git reset --hard "origin/$BRANCH"
fi
echo "Fixing .dockerignore..."
sed -i '/node_modules/d' .dockerignore 2>/dev/null || true
sed -i '/node_modules/d' backend/.dockerignore 2>/dev/null || true
sed -i '/^backend$/d' .dockerignore 2>/dev/null || true
echo "Disabling TS checks for fast deploy..."
sed -i 's|tsc -b \&\& vite build|vite build|g' frontend/admin-panel/package.json 2>/dev/null || true
sed -i 's|tsc \&\& vite build|vite build|g' frontend/admin-panel/package.json 2>/dev/null || true
# Copy scientificTerms.ts if needed
cp frontend/application/lib/data/scientificTerms.ts backend/prisma/scientificTerms.ts 2>/dev/null || true
# Prepare docker-compose.yml in TARGET_DIR
cp "/opt/services/canina/$COMPOSE_FILE" "$TARGET_DIR/docker-compose.yml"
sed -i 's/- REDIS_PORT: 6379/- REDIS_PORT=6379/g' "$TARGET_DIR/docker-compose.yml" 2>/dev/null || true
# Prepare nginx.conf for current environment
echo "Patching nginx.conf for environment..."
cp nginx.conf nginx.conf.tmp
sed -i "s/__BACKEND_HOST__/${BACKEND_HOST}/g" nginx.conf.tmp
sed -i "s/__APP_HOST__/${APP_HOST}/g" nginx.conf.tmp
sed -i "s/__ADMIN_HOST__/${ADMIN_HOST}/g" nginx.conf.tmp
mv nginx.conf.tmp nginx.conf
echo "Building Docker images..."
docker compose build --build-arg NEXT_PUBLIC_API_URL="${API_URL}/api" --build-arg VITE_API_URL="${API_URL}"
echo "Starting containers..."
docker compose up -d
echo "Waiting for backend to start..."
sleep 15
echo "Running migrations and seeding database..."
docker exec "$CONTAINER_PREFIX" npx prisma migrate deploy || true
docker exec "$CONTAINER_PREFIX" npx prisma db seed || true
echo "Deployment for $BRANCH completed successfully!"