ci: IDC 서버 자동배포 파이프라인 구축
- Dockerfile: Turborepo 멀티스테이지 빌드 (Next.js standalone) - docker-compose.prod.yml: PostgreSQL/Redis/Nginx/Web/Admin 프로덕션 스택 - deploy/poll-deploy.sh: cron 기반 자동배포 (매분 Gitea 폴링) - deploy/nginx/default.conf: 리버스 프록시 설정 - next.config.ts: output standalone 추가 - .env.production.example: 환경변수 템플릿 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
node_modules
|
||||
.next
|
||||
.git
|
||||
.gitignore
|
||||
*.md
|
||||
docker-compose*.yml
|
||||
Dockerfile
|
||||
.dockerignore
|
||||
.env*
|
||||
!.env.production.example
|
||||
coverage
|
||||
.turbo
|
||||
dist
|
||||
.omc
|
||||
@@ -0,0 +1,19 @@
|
||||
# ===========================================
|
||||
# Re:Link Production Environment Variables
|
||||
# Copy to .env.production and fill in values
|
||||
# ===========================================
|
||||
|
||||
# Database
|
||||
DB_USER=relink
|
||||
DB_PASSWORD=CHANGE_ME_STRONG_PASSWORD
|
||||
DB_NAME=relink_prod
|
||||
|
||||
# Redis
|
||||
REDIS_PASSWORD=CHANGE_ME_STRONG_PASSWORD
|
||||
|
||||
# NextAuth
|
||||
NEXTAUTH_URL=http://your-domain.com
|
||||
NEXTAUTH_SECRET=GENERATE_WITH_openssl_rand_base64_32
|
||||
|
||||
# App
|
||||
NODE_ENV=production
|
||||
@@ -0,0 +1,42 @@
|
||||
name: Deploy Re:Link
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Create .env.production
|
||||
run: |
|
||||
cat > .env.production << 'ENVEOF'
|
||||
DB_USER=${{ secrets.DB_USER }}
|
||||
DB_PASSWORD=${{ secrets.DB_PASSWORD }}
|
||||
DB_NAME=${{ secrets.DB_NAME }}
|
||||
REDIS_PASSWORD=${{ secrets.REDIS_PASSWORD }}
|
||||
NEXTAUTH_URL=${{ secrets.NEXTAUTH_URL }}
|
||||
NEXTAUTH_SECRET=${{ secrets.NEXTAUTH_SECRET }}
|
||||
ENVEOF
|
||||
|
||||
- name: Build and Deploy
|
||||
run: |
|
||||
docker compose -f docker-compose.prod.yml --env-file .env.production up -d --build --remove-orphans
|
||||
|
||||
- name: Run database migrations
|
||||
run: |
|
||||
docker compose -f docker-compose.prod.yml exec -T web sh -c "cd /app && npx prisma migrate deploy" || echo "Migration skipped (first deploy)"
|
||||
|
||||
- name: Health check
|
||||
run: |
|
||||
echo "Waiting for services to start..."
|
||||
sleep 10
|
||||
curl -f http://localhost/health || echo "Health check pending - services may still be starting"
|
||||
|
||||
- name: Cleanup old images
|
||||
run: |
|
||||
docker image prune -f
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
# ============================================
|
||||
# Re:Link Turborepo Multi-stage Dockerfile
|
||||
# Usage: docker build --build-arg APP_NAME=web .
|
||||
# ============================================
|
||||
|
||||
# --- Stage 1: Base ---
|
||||
FROM node:20-alpine AS base
|
||||
RUN apk add --no-cache libc6-compat
|
||||
RUN corepack enable && corepack prepare pnpm@8.10.0 --activate
|
||||
WORKDIR /app
|
||||
|
||||
# --- Stage 2: Install & Build ---
|
||||
FROM base AS builder
|
||||
ARG APP_NAME=web
|
||||
|
||||
# Copy workspace config first for better caching
|
||||
COPY pnpm-lock.yaml pnpm-workspace.yaml package.json turbo.json ./
|
||||
COPY apps/web/package.json ./apps/web/
|
||||
COPY apps/admin/package.json ./apps/admin/
|
||||
COPY packages/shared/package.json ./packages/shared/
|
||||
COPY packages/domain/package.json ./packages/domain/
|
||||
COPY packages/application/package.json ./packages/application/
|
||||
COPY packages/infrastructure/package.json ./packages/infrastructure/
|
||||
COPY packages/database/package.json ./packages/database/
|
||||
COPY packages/analytics/package.json ./packages/analytics/
|
||||
COPY packages/ui/package.json ./packages/ui/
|
||||
|
||||
RUN pnpm install --frozen-lockfile
|
||||
|
||||
# Copy all source code
|
||||
COPY . .
|
||||
|
||||
# Generate Prisma client & build
|
||||
RUN pnpm --filter @relink/database prisma generate
|
||||
RUN pnpm turbo run build --filter=@relink/${APP_NAME}
|
||||
|
||||
# --- Stage 3: Runner ---
|
||||
FROM node:20-alpine AS runner
|
||||
ARG APP_NAME=web
|
||||
ENV NODE_ENV=production
|
||||
ENV APP_NAME=${APP_NAME}
|
||||
|
||||
RUN addgroup --system --gid 1001 nodejs
|
||||
RUN adduser --system --uid 1001 nextjs
|
||||
WORKDIR /app
|
||||
|
||||
COPY --from=builder /app/apps/${APP_NAME}/public ./public
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/apps/${APP_NAME}/.next/standalone ./
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/apps/${APP_NAME}/.next/static ./apps/${APP_NAME}/.next/static
|
||||
|
||||
USER nextjs
|
||||
EXPOSE 3000
|
||||
|
||||
ENV HOSTNAME="0.0.0.0"
|
||||
ENV PORT=3000
|
||||
CMD ["sh", "-c", "node apps/${APP_NAME}/server.js"]
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { NextConfig } from 'next';
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
output: 'standalone',
|
||||
transpilePackages: ['@relink/ui', '@relink/shared'],
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { NextConfig } from 'next';
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
output: 'standalone',
|
||||
transpilePackages: ['@relink/ui', '@relink/shared'],
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
# ===========================================
|
||||
# Re:Link Auto Deploy Script
|
||||
# Triggered by Gitea webhook on push to main
|
||||
# ===========================================
|
||||
|
||||
APP_DIR="$HOME/relink"
|
||||
LOG_FILE="$APP_DIR/deploy.log"
|
||||
LOCK_FILE="$APP_DIR/deploy.lock"
|
||||
GITEA_REPO="http://39.117.244.52:3000/geonhee/Re_Link.git"
|
||||
|
||||
log() {
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
|
||||
}
|
||||
|
||||
# Prevent concurrent deploys
|
||||
if [ -f "$LOCK_FILE" ]; then
|
||||
LOCK_PID=$(cat "$LOCK_FILE" 2>/dev/null)
|
||||
if kill -0 "$LOCK_PID" 2>/dev/null; then
|
||||
log "ERROR: Deploy already running (PID: $LOCK_PID). Skipping."
|
||||
exit 1
|
||||
fi
|
||||
rm -f "$LOCK_FILE"
|
||||
fi
|
||||
echo $$ > "$LOCK_FILE"
|
||||
trap 'rm -f "$LOCK_FILE"' EXIT
|
||||
|
||||
log "=== Deploy started ==="
|
||||
|
||||
# Navigate to app directory
|
||||
cd "$APP_DIR"
|
||||
|
||||
# Clone or pull
|
||||
if [ ! -d "$APP_DIR/repo/.git" ]; then
|
||||
log "Cloning repository..."
|
||||
git clone "$GITEA_REPO" "$APP_DIR/repo"
|
||||
cd "$APP_DIR/repo"
|
||||
else
|
||||
cd "$APP_DIR/repo"
|
||||
log "Pulling latest changes..."
|
||||
git fetch origin
|
||||
git reset --hard origin/main
|
||||
fi
|
||||
|
||||
log "Current commit: $(git log --oneline -1)"
|
||||
|
||||
# Copy env file
|
||||
if [ -f "$APP_DIR/.env.production" ]; then
|
||||
cp "$APP_DIR/.env.production" "$APP_DIR/repo/.env.production"
|
||||
fi
|
||||
|
||||
# Build and deploy
|
||||
log "Building and deploying with Docker Compose..."
|
||||
docker compose -f docker-compose.prod.yml --env-file .env.production up -d --build --remove-orphans 2>&1 | tee -a "$LOG_FILE"
|
||||
|
||||
# Wait for services
|
||||
log "Waiting for services to be healthy..."
|
||||
sleep 15
|
||||
|
||||
# Health check
|
||||
if curl -sf http://localhost/health > /dev/null 2>&1; then
|
||||
log "Health check PASSED"
|
||||
else
|
||||
log "WARNING: Health check failed - services may still be starting"
|
||||
fi
|
||||
|
||||
# Cleanup old images
|
||||
docker image prune -f >> "$LOG_FILE" 2>&1
|
||||
|
||||
log "=== Deploy completed ==="
|
||||
@@ -0,0 +1,48 @@
|
||||
upstream web_app {
|
||||
server web:3000;
|
||||
}
|
||||
|
||||
upstream admin_app {
|
||||
server admin:3000;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
|
||||
client_max_body_size 50M;
|
||||
|
||||
# Web app (default)
|
||||
location / {
|
||||
proxy_pass http://web_app;
|
||||
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;
|
||||
}
|
||||
|
||||
# Admin app
|
||||
location /admin/ {
|
||||
rewrite ^/admin/(.*) /$1 break;
|
||||
proxy_pass http://admin_app;
|
||||
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 'ok';
|
||||
add_header Content-Type text/plain;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
#!/bin/bash
|
||||
# ===========================================
|
||||
# Re:Link Poll-based Auto Deploy
|
||||
# Checks Gitea for new commits every minute
|
||||
# ===========================================
|
||||
|
||||
APP_DIR="$HOME/relink"
|
||||
REPO_DIR="$APP_DIR/repo"
|
||||
LOG_FILE="$APP_DIR/deploy.log"
|
||||
LOCK_FILE="$APP_DIR/deploy.lock"
|
||||
GITEA_REPO="http://39.117.244.52:3000/geonhee/Re_Link.git"
|
||||
BRANCH="main"
|
||||
|
||||
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" >> "$LOG_FILE"; }
|
||||
|
||||
# Prevent concurrent deploys
|
||||
if [ -f "$LOCK_FILE" ]; then
|
||||
LOCK_PID=$(cat "$LOCK_FILE" 2>/dev/null)
|
||||
if kill -0 "$LOCK_PID" 2>/dev/null; then
|
||||
exit 0
|
||||
fi
|
||||
rm -f "$LOCK_FILE"
|
||||
fi
|
||||
|
||||
# First time: clone
|
||||
if [ ! -d "$REPO_DIR/.git" ]; then
|
||||
log "Initial clone..."
|
||||
echo $$ > "$LOCK_FILE"
|
||||
trap 'rm -f "$LOCK_FILE"' EXIT
|
||||
git clone "$GITEA_REPO" "$REPO_DIR"
|
||||
cd "$REPO_DIR"
|
||||
|
||||
if [ -f "$APP_DIR/.env.production" ]; then
|
||||
cp "$APP_DIR/.env.production" "$REPO_DIR/.env.production"
|
||||
fi
|
||||
|
||||
log "Building and deploying (first time)..."
|
||||
docker compose -f docker-compose.prod.yml --env-file .env.production up -d --build --remove-orphans >> "$LOG_FILE" 2>&1
|
||||
docker image prune -f >> "$LOG_FILE" 2>&1
|
||||
log "First deploy completed. Commit: $(git log --oneline -1)"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
cd "$REPO_DIR"
|
||||
|
||||
# Fetch latest
|
||||
git fetch origin "$BRANCH" --quiet 2>/dev/null
|
||||
|
||||
LOCAL=$(git rev-parse HEAD)
|
||||
REMOTE=$(git rev-parse origin/$BRANCH)
|
||||
|
||||
# No changes
|
||||
if [ "$LOCAL" = "$REMOTE" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# New commits detected - deploy
|
||||
echo $$ > "$LOCK_FILE"
|
||||
trap 'rm -f "$LOCK_FILE"' EXIT
|
||||
|
||||
log "=== New commits detected ==="
|
||||
log "Local: $LOCAL"
|
||||
log "Remote: $REMOTE"
|
||||
|
||||
git reset --hard origin/$BRANCH
|
||||
log "Updated to: $(git log --oneline -1)"
|
||||
|
||||
if [ -f "$APP_DIR/.env.production" ]; then
|
||||
cp "$APP_DIR/.env.production" "$REPO_DIR/.env.production"
|
||||
fi
|
||||
|
||||
log "Building and deploying..."
|
||||
docker compose -f docker-compose.prod.yml --env-file .env.production up -d --build --remove-orphans >> "$LOG_FILE" 2>&1
|
||||
|
||||
sleep 15
|
||||
if curl -sf http://localhost/health > /dev/null 2>&1; then
|
||||
log "Health check PASSED"
|
||||
else
|
||||
log "WARNING: Health check pending"
|
||||
fi
|
||||
|
||||
docker image prune -f >> "$LOG_FILE" 2>&1
|
||||
log "=== Deploy completed ==="
|
||||
@@ -0,0 +1,89 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
# ===========================================
|
||||
# Re:Link IDC Server Setup Script
|
||||
# Run once on the IDC server to initialize
|
||||
# ===========================================
|
||||
|
||||
echo "=== Re:Link Server Setup ==="
|
||||
|
||||
# 1. Install Docker Compose V2 plugin
|
||||
echo "[1/5] Installing Docker Compose V2 plugin..."
|
||||
DOCKER_CONFIG=${DOCKER_CONFIG:-$HOME/.docker}
|
||||
mkdir -p "$DOCKER_CONFIG/cli-plugins"
|
||||
COMPOSE_VERSION=$(curl -s https://api.github.com/repos/docker/compose/releases/latest | grep '"tag_name"' | sed -E 's/.*"([^"]+)".*/\1/')
|
||||
curl -SL "https://github.com/docker/compose/releases/download/${COMPOSE_VERSION}/docker-compose-linux-x86_64" -o "$DOCKER_CONFIG/cli-plugins/docker-compose"
|
||||
chmod +x "$DOCKER_CONFIG/cli-plugins/docker-compose"
|
||||
echo "Docker Compose $(docker compose version) installed."
|
||||
|
||||
# 2. Install Gitea Act Runner
|
||||
echo "[2/5] Installing Gitea Act Runner..."
|
||||
ACT_RUNNER_VERSION="0.2.11"
|
||||
curl -SL "https://gitea.com/gitea/act_runner/releases/download/v${ACT_RUNNER_VERSION}/act_runner-${ACT_RUNNER_VERSION}-linux-amd64" -o /usr/local/bin/act_runner || {
|
||||
# Fallback: download to home dir if no sudo
|
||||
curl -SL "https://gitea.com/gitea/act_runner/releases/download/v${ACT_RUNNER_VERSION}/act_runner-${ACT_RUNNER_VERSION}-linux-amd64" -o "$HOME/act_runner"
|
||||
chmod +x "$HOME/act_runner"
|
||||
echo "act_runner installed to $HOME/act_runner"
|
||||
}
|
||||
chmod +x /usr/local/bin/act_runner 2>/dev/null || true
|
||||
echo "Act Runner v${ACT_RUNNER_VERSION} installed."
|
||||
|
||||
# 3. Create app directory
|
||||
echo "[3/5] Creating application directory..."
|
||||
APP_DIR="$HOME/relink"
|
||||
mkdir -p "$APP_DIR"
|
||||
cd "$APP_DIR"
|
||||
|
||||
# 4. Generate runner config
|
||||
echo "[4/5] Generating runner config..."
|
||||
act_runner generate-config > "$APP_DIR/runner-config.yaml" 2>/dev/null || "$HOME/act_runner" generate-config > "$APP_DIR/runner-config.yaml" 2>/dev/null || true
|
||||
|
||||
# 5. Create systemd service for act_runner
|
||||
echo "[5/5] Creating systemd service..."
|
||||
RUNNER_BIN=$(which act_runner 2>/dev/null || echo "$HOME/act_runner")
|
||||
|
||||
sudo tee /etc/systemd/system/gitea-runner.service > /dev/null << SERVICEEOF
|
||||
[Unit]
|
||||
Description=Gitea Act Runner
|
||||
After=network.target docker.service
|
||||
Requires=docker.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=$(whoami)
|
||||
WorkingDirectory=${APP_DIR}
|
||||
ExecStart=${RUNNER_BIN} daemon --config ${APP_DIR}/runner-config.yaml
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
Environment=DOCKER_HOST=unix:///var/run/docker.sock
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
SERVICEEOF
|
||||
|
||||
echo ""
|
||||
echo "=== Setup Complete ==="
|
||||
echo ""
|
||||
echo "Next steps:"
|
||||
echo "1. Register the runner with Gitea:"
|
||||
echo " cd $APP_DIR"
|
||||
echo " act_runner register \\"
|
||||
echo " --instance http://39.117.244.52:3000 \\"
|
||||
echo " --token <REGISTRATION_TOKEN> \\"
|
||||
echo " --name relink-runner \\"
|
||||
echo " --labels ubuntu-latest:host"
|
||||
echo ""
|
||||
echo "2. Start the runner service:"
|
||||
echo " sudo systemctl daemon-reload"
|
||||
echo " sudo systemctl enable gitea-runner"
|
||||
echo " sudo systemctl start gitea-runner"
|
||||
echo ""
|
||||
echo "3. Create .env.production in $APP_DIR:"
|
||||
echo " cp .env.production.example .env.production"
|
||||
echo " # Edit with real values"
|
||||
echo ""
|
||||
echo "4. Add secrets in Gitea repository settings:"
|
||||
echo " - DB_USER, DB_PASSWORD, DB_NAME"
|
||||
echo " - REDIS_PASSWORD"
|
||||
echo " - NEXTAUTH_URL, NEXTAUTH_SECRET"
|
||||
@@ -0,0 +1,115 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Re:Link Gitea Webhook Receiver
|
||||
Listens for push events and triggers deployment.
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from http.server import HTTPServer, BaseHTTPRequestHandler
|
||||
from datetime import datetime
|
||||
|
||||
WEBHOOK_SECRET = os.environ.get("WEBHOOK_SECRET", "relink-deploy-secret")
|
||||
DEPLOY_SCRIPT = os.path.expanduser("~/relink/deploy.sh")
|
||||
DEPLOY_BRANCH = "main"
|
||||
PORT = int(os.environ.get("WEBHOOK_PORT", "9000"))
|
||||
LOG_FILE = os.path.expanduser("~/relink/webhook.log")
|
||||
|
||||
|
||||
def log(msg: str):
|
||||
line = f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] {msg}"
|
||||
print(line, flush=True)
|
||||
with open(LOG_FILE, "a") as f:
|
||||
f.write(line + "\n")
|
||||
|
||||
|
||||
def verify_signature(payload: bytes, signature: str) -> bool:
|
||||
if not signature:
|
||||
return False
|
||||
expected = hmac.new(
|
||||
WEBHOOK_SECRET.encode(), payload, hashlib.sha256
|
||||
).hexdigest()
|
||||
return hmac.compare_digest(f"sha256={expected}", signature)
|
||||
|
||||
|
||||
class WebhookHandler(BaseHTTPRequestHandler):
|
||||
def do_POST(self):
|
||||
if self.path != "/deploy":
|
||||
self.send_response(404)
|
||||
self.end_headers()
|
||||
return
|
||||
|
||||
content_length = int(self.headers.get("Content-Length", 0))
|
||||
payload = self.rfile.read(content_length)
|
||||
|
||||
# Verify signature
|
||||
signature = self.headers.get("X-Gitea-Signature", "")
|
||||
if WEBHOOK_SECRET and not verify_signature(payload, signature):
|
||||
log("REJECTED: Invalid signature")
|
||||
self.send_response(403)
|
||||
self.end_headers()
|
||||
self.wfile.write(b"Invalid signature")
|
||||
return
|
||||
|
||||
try:
|
||||
data = json.loads(payload)
|
||||
except json.JSONDecodeError:
|
||||
self.send_response(400)
|
||||
self.end_headers()
|
||||
return
|
||||
|
||||
ref = data.get("ref", "")
|
||||
if ref != f"refs/heads/{DEPLOY_BRANCH}":
|
||||
log(f"SKIPPED: Push to {ref} (not {DEPLOY_BRANCH})")
|
||||
self.send_response(200)
|
||||
self.end_headers()
|
||||
self.wfile.write(b"Skipped: not target branch")
|
||||
return
|
||||
|
||||
pusher = data.get("pusher", {}).get("login", "unknown")
|
||||
commits = len(data.get("commits", []))
|
||||
log(f"DEPLOY TRIGGERED: {pusher} pushed {commits} commit(s) to {DEPLOY_BRANCH}")
|
||||
|
||||
# Run deploy script asynchronously
|
||||
try:
|
||||
subprocess.Popen(
|
||||
["bash", DEPLOY_SCRIPT],
|
||||
stdout=open(LOG_FILE, "a"),
|
||||
stderr=subprocess.STDOUT,
|
||||
start_new_session=True,
|
||||
)
|
||||
self.send_response(200)
|
||||
self.end_headers()
|
||||
self.wfile.write(b"Deploy started")
|
||||
log("Deploy script launched")
|
||||
except Exception as e:
|
||||
log(f"ERROR: Failed to start deploy: {e}")
|
||||
self.send_response(500)
|
||||
self.end_headers()
|
||||
self.wfile.write(f"Deploy failed: {e}".encode())
|
||||
|
||||
def do_GET(self):
|
||||
if self.path == "/health":
|
||||
self.send_response(200)
|
||||
self.end_headers()
|
||||
self.wfile.write(b"ok")
|
||||
else:
|
||||
self.send_response(404)
|
||||
self.end_headers()
|
||||
|
||||
def log_message(self, format, *args):
|
||||
pass # Suppress default logging
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
log(f"Webhook receiver starting on port {PORT}")
|
||||
server = HTTPServer(("0.0.0.0", PORT), WebhookHandler)
|
||||
try:
|
||||
server.serve_forever()
|
||||
except KeyboardInterrupt:
|
||||
log("Webhook receiver stopped")
|
||||
server.server_close()
|
||||
@@ -0,0 +1,88 @@
|
||||
services:
|
||||
postgres:
|
||||
image: postgis/postgis:16-3.4-alpine
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "127.0.0.1:5432:5432"
|
||||
environment:
|
||||
POSTGRES_USER: ${DB_USER:-relink}
|
||||
POSTGRES_PASSWORD: ${DB_PASSWORD:?DB_PASSWORD is required}
|
||||
POSTGRES_DB: ${DB_NAME:-relink_prod}
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-relink} -d ${DB_NAME:-relink_prod}"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "127.0.0.1:6379:6379"
|
||||
command: redis-server --requirepass ${REDIS_PASSWORD:?REDIS_PASSWORD is required}
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
web:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
args:
|
||||
APP_NAME: web
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "127.0.0.1:3000:3000"
|
||||
environment:
|
||||
DATABASE_URL: postgresql://${DB_USER:-relink}:${DB_PASSWORD}@postgres:5432/${DB_NAME:-relink_prod}?schema=public
|
||||
REDIS_URL: redis://:${REDIS_PASSWORD}@redis:6379
|
||||
NEXTAUTH_URL: ${NEXTAUTH_URL:-http://localhost:3000}
|
||||
NEXTAUTH_SECRET: ${NEXTAUTH_SECRET:?NEXTAUTH_SECRET is required}
|
||||
NODE_ENV: production
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
|
||||
admin:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
args:
|
||||
APP_NAME: admin
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "127.0.0.1:3001:3000"
|
||||
environment:
|
||||
DATABASE_URL: postgresql://${DB_USER:-relink}:${DB_PASSWORD}@postgres:5432/${DB_NAME:-relink_prod}?schema=public
|
||||
REDIS_URL: redis://:${REDIS_PASSWORD}@redis:6379
|
||||
NODE_ENV: production
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
|
||||
nginx:
|
||||
image: nginx:alpine
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
volumes:
|
||||
- ./deploy/nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
|
||||
- ./deploy/nginx/ssl:/etc/nginx/ssl:ro
|
||||
depends_on:
|
||||
- web
|
||||
- admin
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
redis_data:
|
||||
Reference in New Issue
Block a user