Compare commits
5 Commits
b58b45f412
...
a7e1ceaca0
| Author | SHA1 | Date | |
|---|---|---|---|
| a7e1ceaca0 | |||
| b0b1930b7e | |||
| 9a576f5a90 | |||
| 9302c279df | |||
| 34d242c614 |
173
.gitea/workflows/deploy.yml
Normal file
173
.gitea/workflows/deploy.yml
Normal file
@ -0,0 +1,173 @@
|
||||
name: Deploy Pounce
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
build-and-deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up environment
|
||||
run: |
|
||||
echo "REPO_PATH=/home/administrator/pounce" >> $GITHUB_ENV
|
||||
echo "BACKEND_IMAGE=pounce-backend" >> $GITHUB_ENV
|
||||
echo "FRONTEND_IMAGE=pounce-frontend" >> $GITHUB_ENV
|
||||
|
||||
- name: Sync code to deploy directory
|
||||
run: |
|
||||
mkdir -p ${{ env.REPO_PATH }}
|
||||
cp -r . ${{ env.REPO_PATH }}/
|
||||
echo "Code synced to ${{ env.REPO_PATH }}"
|
||||
|
||||
- name: Build Backend Docker Image
|
||||
run: |
|
||||
cd ${{ env.REPO_PATH }}/backend
|
||||
docker build -t ${{ env.BACKEND_IMAGE }}:${{ github.sha }} -t ${{ env.BACKEND_IMAGE }}:latest .
|
||||
echo "✅ Backend image built successfully"
|
||||
|
||||
- name: Build Frontend Docker Image
|
||||
run: |
|
||||
cd ${{ env.REPO_PATH }}/frontend
|
||||
|
||||
# Create .env.local with correct URLs
|
||||
cat > .env.local << EOF
|
||||
NEXT_PUBLIC_API_URL=https://api.pounce.ch
|
||||
BACKEND_URL=http://pounce-backend:8000
|
||||
EOF
|
||||
|
||||
docker build \
|
||||
--build-arg NEXT_PUBLIC_API_URL=https://api.pounce.ch \
|
||||
--build-arg BACKEND_URL=http://pounce-backend:8000 \
|
||||
-t ${{ env.FRONTEND_IMAGE }}:${{ github.sha }} \
|
||||
-t ${{ env.FRONTEND_IMAGE }}:latest \
|
||||
.
|
||||
echo "✅ Frontend image built successfully"
|
||||
|
||||
- name: Deploy Backend
|
||||
env:
|
||||
DATABASE_URL: ${{ secrets.DATABASE_URL }}
|
||||
SECRET_KEY: ${{ secrets.SECRET_KEY }}
|
||||
SMTP_PASSWORD: ${{ secrets.SMTP_PASSWORD }}
|
||||
STRIPE_SECRET_KEY: ${{ secrets.STRIPE_SECRET_KEY }}
|
||||
STRIPE_WEBHOOK_SECRET: ${{ secrets.STRIPE_WEBHOOK_SECRET }}
|
||||
GOOGLE_CLIENT_SECRET: ${{ secrets.GOOGLE_CLIENT_SECRET }}
|
||||
GITHUB_CLIENT_SECRET: ${{ secrets.GITHUB_CLIENT_SECRET }}
|
||||
run: |
|
||||
# Stop existing container
|
||||
docker stop pounce-backend 2>/dev/null || true
|
||||
docker rm pounce-backend 2>/dev/null || true
|
||||
|
||||
# Run new container with secrets from environment
|
||||
docker run -d \
|
||||
--name pounce-backend \
|
||||
--network n0488s44osgoow4wgo04ogg0 \
|
||||
--restart unless-stopped \
|
||||
-e DATABASE_URL="${DATABASE_URL}" \
|
||||
-e SECRET_KEY="${SECRET_KEY}" \
|
||||
-e JWT_SECRET="${SECRET_KEY}" \
|
||||
-e REDIS_URL="redis://pounce-redis:6379/0" \
|
||||
-e ENABLE_JOB_QUEUE="true" \
|
||||
-e CORS_ORIGINS="https://pounce.ch,https://www.pounce.ch" \
|
||||
-e COOKIE_SECURE="true" \
|
||||
-e SITE_URL="https://pounce.ch" \
|
||||
-e FRONTEND_URL="https://pounce.ch" \
|
||||
-e ENVIRONMENT="production" \
|
||||
-e ENABLE_SCHEDULER="true" \
|
||||
-e SMTP_HOST="smtp.zoho.eu" \
|
||||
-e SMTP_PORT="465" \
|
||||
-e SMTP_USER="hello@pounce.ch" \
|
||||
-e SMTP_PASSWORD="${SMTP_PASSWORD}" \
|
||||
-e SMTP_FROM_EMAIL="hello@pounce.ch" \
|
||||
-e SMTP_FROM_NAME="pounce" \
|
||||
-e SMTP_USE_TLS="false" \
|
||||
-e SMTP_USE_SSL="true" \
|
||||
-e STRIPE_SECRET_KEY="${STRIPE_SECRET_KEY}" \
|
||||
-e STRIPE_PUBLISHABLE_KEY="pk_live_51ScLbjCtFUamNRpNeFugrlTIYhszbo8GovSGiMnPwHpZX9p3SGtgG8iRHYRIlAtg9M9sl3mvT5r8pwXP3mOsPALG00Wk3j0wH4" \
|
||||
-e STRIPE_PRICE_TRADER="price_1ScRlzCtFUamNRpNQdMpMzxV" \
|
||||
-e STRIPE_PRICE_TYCOON="price_1SdwhSCtFUamNRpNEXTSuGUc" \
|
||||
-e STRIPE_WEBHOOK_SECRET="${STRIPE_WEBHOOK_SECRET}" \
|
||||
-e GOOGLE_CLIENT_ID="865146315769-vi7vcu91d3i7huv8ikjun52jo9ob7spk.apps.googleusercontent.com" \
|
||||
-e GOOGLE_CLIENT_SECRET="${GOOGLE_CLIENT_SECRET}" \
|
||||
-e GOOGLE_REDIRECT_URI="https://pounce.ch/api/v1/oauth/google/callback" \
|
||||
-e GITHUB_CLIENT_ID="Ov23liBjROk39vYXi3G5" \
|
||||
-e GITHUB_CLIENT_SECRET="${GITHUB_CLIENT_SECRET}" \
|
||||
-e GITHUB_REDIRECT_URI="https://pounce.ch/api/v1/oauth/github/callback" \
|
||||
-l "traefik.enable=true" \
|
||||
-l "traefik.http.routers.pounce-api.rule=Host(\`api.pounce.ch\`)" \
|
||||
-l "traefik.http.routers.pounce-api.entryPoints=https" \
|
||||
-l "traefik.http.routers.pounce-api.tls=true" \
|
||||
-l "traefik.http.routers.pounce-api.tls.certresolver=letsencrypt" \
|
||||
-l "traefik.http.services.pounce-api.loadbalancer.server.port=8000" \
|
||||
-l "traefik.http.routers.pounce-api-http.rule=Host(\`api.pounce.ch\`)" \
|
||||
-l "traefik.http.routers.pounce-api-http.entryPoints=http" \
|
||||
-l "traefik.http.routers.pounce-api-http.middlewares=redirect-to-https" \
|
||||
${{ env.BACKEND_IMAGE }}:latest
|
||||
|
||||
# Connect to coolify network for Traefik
|
||||
docker network connect coolify pounce-backend 2>/dev/null || true
|
||||
|
||||
echo "✅ Backend deployed"
|
||||
|
||||
- name: Deploy Frontend
|
||||
run: |
|
||||
# Stop existing container
|
||||
docker stop pounce-frontend 2>/dev/null || true
|
||||
docker rm pounce-frontend 2>/dev/null || true
|
||||
|
||||
# Run new container
|
||||
docker run -d \
|
||||
--name pounce-frontend \
|
||||
--network coolify \
|
||||
--restart unless-stopped \
|
||||
-l "traefik.enable=true" \
|
||||
-l "traefik.http.routers.pounce-web.rule=Host(\`pounce.ch\`) || Host(\`www.pounce.ch\`)" \
|
||||
-l "traefik.http.routers.pounce-web.entryPoints=https" \
|
||||
-l "traefik.http.routers.pounce-web.tls=true" \
|
||||
-l "traefik.http.routers.pounce-web.tls.certresolver=letsencrypt" \
|
||||
-l "traefik.http.services.pounce-web.loadbalancer.server.port=3000" \
|
||||
-l "traefik.http.routers.pounce-web-http.rule=Host(\`pounce.ch\`) || Host(\`www.pounce.ch\`)" \
|
||||
-l "traefik.http.routers.pounce-web-http.entryPoints=http" \
|
||||
-l "traefik.http.routers.pounce-web-http.middlewares=redirect-to-https" \
|
||||
${{ env.FRONTEND_IMAGE }}:latest
|
||||
|
||||
# Connect to supabase network for backend access
|
||||
docker network connect n0488s44osgoow4wgo04ogg0 pounce-frontend 2>/dev/null || true
|
||||
|
||||
echo "✅ Frontend deployed"
|
||||
|
||||
- name: Health Check
|
||||
run: |
|
||||
echo "Waiting for services to start..."
|
||||
sleep 15
|
||||
|
||||
echo "=== Backend Health Check ==="
|
||||
curl -sf http://localhost:8000/health || curl -sf http://pounce-backend:8000/health || echo "Backend starting..."
|
||||
|
||||
echo ""
|
||||
echo "=== Container Status ==="
|
||||
docker ps --filter "name=pounce" --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"
|
||||
|
||||
- name: Cleanup
|
||||
run: |
|
||||
docker image prune -f
|
||||
docker container prune -f
|
||||
echo "✅ Cleanup complete"
|
||||
|
||||
- name: Deployment Summary
|
||||
run: |
|
||||
echo "=========================================="
|
||||
echo "🎉 DEPLOYMENT SUCCESSFUL!"
|
||||
echo "=========================================="
|
||||
echo "Commit: ${{ github.sha }}"
|
||||
echo "Branch: ${{ github.ref_name }}"
|
||||
echo "Time: $(date)"
|
||||
echo ""
|
||||
echo "Services:"
|
||||
echo " - Frontend: https://pounce.ch"
|
||||
echo " - Backend: https://api.pounce.ch"
|
||||
echo "=========================================="
|
||||
@ -629,3 +629,4 @@ MIT License
|
||||
## 📧 Support
|
||||
|
||||
For issues and feature requests, please open a GitHub issue or contact support@pounce.ch
|
||||
# Pounce CI/CD
|
||||
|
||||
@ -174,14 +174,14 @@ class CZDSClient:
|
||||
return None
|
||||
|
||||
def extract_zone_file(self, gz_path: Path) -> Path:
|
||||
"""Extract gzipped zone file."""
|
||||
output_path = gz_path.with_suffix('') # Remove .gz
|
||||
"""
|
||||
Extract gzipped zone file to RAM drive for fastest access.
|
||||
Falls back to disk if RAM drive unavailable.
|
||||
"""
|
||||
from app.services.zone_file_parser import HighPerformanceZoneParser
|
||||
|
||||
logger.info(f"Extracting {gz_path.name}...")
|
||||
|
||||
with gzip.open(gz_path, 'rb') as f_in:
|
||||
with open(output_path, 'wb') as f_out:
|
||||
shutil.copyfileobj(f_in, f_out)
|
||||
parser = HighPerformanceZoneParser(use_ram_drive=True)
|
||||
output_path = parser.extract_to_ram(gz_path)
|
||||
|
||||
# Remove gz file to save space
|
||||
gz_path.unlink()
|
||||
@ -192,43 +192,21 @@ class CZDSClient:
|
||||
"""
|
||||
Parse zone file and extract unique domain names.
|
||||
|
||||
Zone files contain various record types. We extract domains from:
|
||||
- NS records (most reliable indicator of active domain)
|
||||
- A/AAAA records
|
||||
Uses high-performance parallel parser with all CPU cores
|
||||
and RAM drive for maximum speed on large zone files.
|
||||
|
||||
Returns set of domain names (without TLD suffix).
|
||||
"""
|
||||
logger.info(f"Parsing zone file for .{tld}...")
|
||||
from app.services.zone_file_parser import HighPerformanceZoneParser
|
||||
|
||||
domains = set()
|
||||
line_count = 0
|
||||
# Use parallel parser with RAM drive
|
||||
parser = HighPerformanceZoneParser(use_ram_drive=True)
|
||||
|
||||
with open(zone_path, 'r', encoding='utf-8', errors='ignore') as f:
|
||||
for line in f:
|
||||
line_count += 1
|
||||
|
||||
# Skip comments and empty lines
|
||||
if line.startswith(';') or not line.strip():
|
||||
continue
|
||||
|
||||
# Look for NS records which indicate delegated domains
|
||||
# Format: example.tld. 86400 IN NS ns1.registrar.com.
|
||||
parts = line.split()
|
||||
if len(parts) >= 4:
|
||||
# First column is the domain name
|
||||
name = parts[0].rstrip('.')
|
||||
|
||||
# Must end with our TLD
|
||||
if name.lower().endswith(f'.{tld}'):
|
||||
# Extract just the domain name part
|
||||
domain_name = name[:-(len(tld) + 1)]
|
||||
|
||||
# Skip the TLD itself and subdomains
|
||||
if domain_name and '.' not in domain_name:
|
||||
domains.add(domain_name.lower())
|
||||
|
||||
logger.info(f"Parsed .{tld}: {len(domains):,} unique domains from {line_count:,} lines")
|
||||
return domains
|
||||
try:
|
||||
domains = parser.parse_zone_file_parallel(zone_path, tld)
|
||||
return domains
|
||||
finally:
|
||||
parser.cleanup_ram_drive()
|
||||
|
||||
def compute_checksum(self, domains: set[str]) -> str:
|
||||
"""Compute SHA256 checksum of sorted domain list."""
|
||||
|
||||
300
backend/app/services/zone_file_parser.py
Normal file
300
backend/app/services/zone_file_parser.py
Normal file
@ -0,0 +1,300 @@
|
||||
"""
|
||||
High-Performance Zone File Parser with Multiprocessing
|
||||
=======================================================
|
||||
Optimized for servers with many CPU cores (e.g., Ryzen 9 with 32 threads).
|
||||
|
||||
Uses:
|
||||
- multiprocessing.Pool for parallel chunk processing
|
||||
- Memory-mapped files for fast I/O
|
||||
- RAM drive (/dev/shm) for temporary files
|
||||
- Batch operations for maximum throughput
|
||||
|
||||
This can parse 150+ million domain records in minutes instead of hours.
|
||||
"""
|
||||
|
||||
import gzip
|
||||
import hashlib
|
||||
import logging
|
||||
import mmap
|
||||
import os
|
||||
import shutil
|
||||
import tempfile
|
||||
from concurrent.futures import ProcessPoolExecutor, as_completed
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ParseResult:
|
||||
"""Result from parsing a zone file chunk."""
|
||||
domains: set[str]
|
||||
line_count: int
|
||||
error: Optional[str] = None
|
||||
|
||||
|
||||
def get_optimal_workers() -> int:
|
||||
"""Get optimal number of worker processes based on CPU count."""
|
||||
cpu_count = os.cpu_count() or 4
|
||||
# Use 75% of available cores to leave some for other tasks
|
||||
return max(4, int(cpu_count * 0.75))
|
||||
|
||||
|
||||
def get_ram_drive_path() -> Optional[Path]:
|
||||
"""
|
||||
Get path to RAM drive if available.
|
||||
Linux: /dev/shm (typically 50% of RAM)
|
||||
macOS: /tmp is often memory-backed
|
||||
"""
|
||||
# Linux RAM drive
|
||||
if os.path.exists("/dev/shm"):
|
||||
shm_path = Path("/dev/shm/pounce_zones")
|
||||
try:
|
||||
shm_path.mkdir(parents=True, exist_ok=True)
|
||||
return shm_path
|
||||
except PermissionError:
|
||||
pass
|
||||
|
||||
# Fall back to temp directory
|
||||
tmp_path = Path(tempfile.gettempdir()) / "pounce_zones"
|
||||
tmp_path.mkdir(parents=True, exist_ok=True)
|
||||
return tmp_path
|
||||
|
||||
|
||||
def parse_chunk(args: tuple) -> ParseResult:
|
||||
"""
|
||||
Parse a chunk of zone file content.
|
||||
|
||||
This function runs in a separate process for parallelization.
|
||||
|
||||
Args:
|
||||
args: Tuple of (chunk_content, tld, chunk_id)
|
||||
|
||||
Returns:
|
||||
ParseResult with extracted domains
|
||||
"""
|
||||
chunk_content, tld, chunk_id = args
|
||||
domains = set()
|
||||
line_count = 0
|
||||
tld_suffix = f".{tld}"
|
||||
tld_suffix_len = len(tld_suffix) + 1 # +1 for the dot before TLD
|
||||
|
||||
try:
|
||||
for line in chunk_content.split('\n'):
|
||||
line_count += 1
|
||||
|
||||
# Skip comments and empty lines
|
||||
if not line or line.startswith(';'):
|
||||
continue
|
||||
|
||||
# Fast parsing: split on whitespace and check first column
|
||||
# Zone file format: example.tld. 86400 IN NS ns1.example.com.
|
||||
space_idx = line.find('\t')
|
||||
if space_idx == -1:
|
||||
space_idx = line.find(' ')
|
||||
if space_idx == -1:
|
||||
continue
|
||||
|
||||
name = line[:space_idx].rstrip('.')
|
||||
|
||||
# Must end with our TLD
|
||||
name_lower = name.lower()
|
||||
if not name_lower.endswith(tld_suffix):
|
||||
continue
|
||||
|
||||
# Extract domain name (without TLD)
|
||||
domain_name = name_lower[:-len(tld_suffix)]
|
||||
|
||||
# Skip TLD itself and subdomains
|
||||
if domain_name and '.' not in domain_name:
|
||||
domains.add(domain_name)
|
||||
|
||||
return ParseResult(domains=domains, line_count=line_count)
|
||||
|
||||
except Exception as e:
|
||||
return ParseResult(domains=set(), line_count=line_count, error=str(e))
|
||||
|
||||
|
||||
class HighPerformanceZoneParser:
|
||||
"""
|
||||
High-performance zone file parser using multiprocessing.
|
||||
|
||||
Features:
|
||||
- Parallel chunk processing using all CPU cores
|
||||
- RAM drive utilization for faster I/O
|
||||
- Memory-efficient streaming for huge files
|
||||
- Progress logging for long operations
|
||||
"""
|
||||
|
||||
def __init__(self, use_ram_drive: bool = True, workers: Optional[int] = None):
|
||||
self.use_ram_drive = use_ram_drive
|
||||
self.workers = workers or get_optimal_workers()
|
||||
self.ram_drive_path = get_ram_drive_path() if use_ram_drive else None
|
||||
|
||||
logger.info(
|
||||
f"Zone parser initialized: {self.workers} workers, "
|
||||
f"RAM drive: {self.ram_drive_path or 'disabled'}"
|
||||
)
|
||||
|
||||
def extract_to_ram(self, gz_path: Path) -> Path:
|
||||
"""
|
||||
Extract gzipped zone file to RAM drive for fastest access.
|
||||
|
||||
Args:
|
||||
gz_path: Path to .gz file
|
||||
|
||||
Returns:
|
||||
Path to extracted file (in RAM drive if available)
|
||||
"""
|
||||
# Determine output path
|
||||
if self.ram_drive_path:
|
||||
output_path = self.ram_drive_path / gz_path.stem
|
||||
else:
|
||||
output_path = gz_path.with_suffix('')
|
||||
|
||||
logger.info(f"Extracting {gz_path.name} to {output_path}...")
|
||||
|
||||
# Stream extraction to handle large files
|
||||
with gzip.open(gz_path, 'rb') as f_in:
|
||||
with open(output_path, 'wb') as f_out:
|
||||
shutil.copyfileobj(f_in, f_out, length=64 * 1024 * 1024) # 64MB buffer
|
||||
|
||||
file_size_mb = output_path.stat().st_size / (1024 * 1024)
|
||||
logger.info(f"Extracted: {file_size_mb:.1f} MB")
|
||||
|
||||
return output_path
|
||||
|
||||
def split_file_into_chunks(self, file_path: Path, num_chunks: int) -> list[tuple[int, int]]:
|
||||
"""
|
||||
Calculate byte offsets to split file into roughly equal chunks.
|
||||
|
||||
Returns list of (start_offset, end_offset) tuples.
|
||||
"""
|
||||
file_size = file_path.stat().st_size
|
||||
chunk_size = file_size // num_chunks
|
||||
|
||||
offsets = []
|
||||
start = 0
|
||||
|
||||
with open(file_path, 'rb') as f:
|
||||
for i in range(num_chunks):
|
||||
if i == num_chunks - 1:
|
||||
# Last chunk goes to end
|
||||
offsets.append((start, file_size))
|
||||
else:
|
||||
# Seek to approximate chunk boundary
|
||||
end = start + chunk_size
|
||||
f.seek(end)
|
||||
|
||||
# Find next newline to avoid cutting lines
|
||||
f.readline()
|
||||
end = f.tell()
|
||||
|
||||
offsets.append((start, end))
|
||||
start = end
|
||||
|
||||
return offsets
|
||||
|
||||
def read_chunk(self, file_path: Path, start: int, end: int) -> str:
|
||||
"""Read a chunk of file between byte offsets."""
|
||||
with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
|
||||
f.seek(start)
|
||||
return f.read(end - start)
|
||||
|
||||
def parse_zone_file_parallel(self, zone_path: Path, tld: str) -> set[str]:
|
||||
"""
|
||||
Parse zone file using parallel processing.
|
||||
|
||||
Args:
|
||||
zone_path: Path to extracted zone file
|
||||
tld: TLD being parsed
|
||||
|
||||
Returns:
|
||||
Set of domain names (without TLD)
|
||||
"""
|
||||
file_size_mb = zone_path.stat().st_size / (1024 * 1024)
|
||||
logger.info(f"Parsing .{tld} zone file ({file_size_mb:.1f} MB) with {self.workers} workers...")
|
||||
|
||||
# Split file into chunks
|
||||
chunk_offsets = self.split_file_into_chunks(zone_path, self.workers)
|
||||
|
||||
# Read chunks and prepare for parallel processing
|
||||
chunks = []
|
||||
for i, (start, end) in enumerate(chunk_offsets):
|
||||
chunk_content = self.read_chunk(zone_path, start, end)
|
||||
chunks.append((chunk_content, tld, i))
|
||||
|
||||
# Process chunks in parallel
|
||||
all_domains = set()
|
||||
total_lines = 0
|
||||
|
||||
with ProcessPoolExecutor(max_workers=self.workers) as executor:
|
||||
futures = [executor.submit(parse_chunk, chunk) for chunk in chunks]
|
||||
|
||||
for future in as_completed(futures):
|
||||
result = future.result()
|
||||
all_domains.update(result.domains)
|
||||
total_lines += result.line_count
|
||||
|
||||
if result.error:
|
||||
logger.warning(f"Chunk error: {result.error}")
|
||||
|
||||
logger.info(
|
||||
f"Parsed .{tld}: {len(all_domains):,} unique domains "
|
||||
f"from {total_lines:,} lines using {self.workers} workers"
|
||||
)
|
||||
|
||||
return all_domains
|
||||
|
||||
def cleanup_ram_drive(self):
|
||||
"""Clean up temporary files from RAM drive."""
|
||||
if self.ram_drive_path and self.ram_drive_path.exists():
|
||||
for file in self.ram_drive_path.glob("*"):
|
||||
try:
|
||||
file.unlink()
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to delete {file}: {e}")
|
||||
|
||||
|
||||
def compute_checksum(domains: set[str]) -> str:
|
||||
"""Compute SHA256 checksum of sorted domain list."""
|
||||
sorted_domains = "\n".join(sorted(domains))
|
||||
return hashlib.sha256(sorted_domains.encode()).hexdigest()
|
||||
|
||||
|
||||
def parse_zone_file_fast(
|
||||
zone_path: Path,
|
||||
tld: str,
|
||||
use_ram_drive: bool = True,
|
||||
workers: Optional[int] = None
|
||||
) -> set[str]:
|
||||
"""
|
||||
Convenience function to parse a zone file with optimal settings.
|
||||
|
||||
Args:
|
||||
zone_path: Path to zone file (can be .gz)
|
||||
tld: TLD being parsed
|
||||
use_ram_drive: Whether to use RAM drive for extraction
|
||||
workers: Number of worker processes (auto-detected if None)
|
||||
|
||||
Returns:
|
||||
Set of domain names
|
||||
"""
|
||||
parser = HighPerformanceZoneParser(use_ram_drive=use_ram_drive, workers=workers)
|
||||
|
||||
try:
|
||||
# Extract if gzipped
|
||||
if str(zone_path).endswith('.gz'):
|
||||
extracted_path = parser.extract_to_ram(zone_path)
|
||||
result = parser.parse_zone_file_parallel(extracted_path, tld)
|
||||
# Clean up extracted file
|
||||
extracted_path.unlink()
|
||||
else:
|
||||
result = parser.parse_zone_file_parallel(zone_path, tld)
|
||||
|
||||
return result
|
||||
|
||||
finally:
|
||||
parser.cleanup_ram_drive()
|
||||
65
docker-compose.prod.yml
Normal file
65
docker-compose.prod.yml
Normal file
@ -0,0 +1,65 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
backend:
|
||||
build:
|
||||
context: ./backend
|
||||
dockerfile: Dockerfile
|
||||
container_name: pounce-backend
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- pounce-network
|
||||
- supabase-network
|
||||
environment:
|
||||
- DATABASE_URL=postgresql+asyncpg://pounce:PounceDB2024!@supabase-db-n0488s44osgoow4wgo04ogg0:5432/pounce
|
||||
- JWT_SECRET=${JWT_SECRET:-pounce-super-secret-jwt-key-2024-production}
|
||||
- FRONTEND_URL=http://pounce.185-142-213-170.sslip.io
|
||||
- ENVIRONMENT=production
|
||||
- ENABLE_SCHEDULER=true
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.pounce-backend.rule=Host(`backend.185-142-213-170.sslip.io`)"
|
||||
- "traefik.http.routers.pounce-backend.entryPoints=http"
|
||||
- "traefik.http.services.pounce-backend.loadbalancer.server.port=8000"
|
||||
- "coolify.managed=true"
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 40s
|
||||
|
||||
frontend:
|
||||
build:
|
||||
context: ./frontend
|
||||
dockerfile: Dockerfile
|
||||
args:
|
||||
- NEXT_PUBLIC_API_URL=http://backend.185-142-213-170.sslip.io
|
||||
container_name: pounce-frontend
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- pounce-network
|
||||
environment:
|
||||
- NEXT_PUBLIC_API_URL=http://backend.185-142-213-170.sslip.io
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.pounce-frontend.rule=Host(`pounce.185-142-213-170.sslip.io`)"
|
||||
- "traefik.http.routers.pounce-frontend.entryPoints=http"
|
||||
- "traefik.http.services.pounce-frontend.loadbalancer.server.port=3000"
|
||||
- "coolify.managed=true"
|
||||
depends_on:
|
||||
- backend
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "-q", "--spider", "http://localhost:3000"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 30s
|
||||
|
||||
networks:
|
||||
pounce-network:
|
||||
name: coolify
|
||||
external: true
|
||||
supabase-network:
|
||||
name: n0488s44osgoow4wgo04ogg0
|
||||
external: true
|
||||
@ -1,37 +1,38 @@
|
||||
# pounce Frontend Dockerfile
|
||||
FROM node:18-alpine AS base
|
||||
|
||||
# Install dependencies only when needed
|
||||
FROM base AS deps
|
||||
RUN apk add --no-cache libc6-compat
|
||||
# Multi-stage build for optimized production image
|
||||
FROM node:20-alpine AS deps
|
||||
WORKDIR /app
|
||||
|
||||
# Copy package files
|
||||
COPY package.json package-lock.json ./
|
||||
RUN npm ci
|
||||
# Install dependencies
|
||||
COPY package.json package-lock.json* ./
|
||||
RUN npm ci --prefer-offline
|
||||
|
||||
# Rebuild source code only when needed
|
||||
FROM base AS builder
|
||||
# Builder stage
|
||||
FROM node:20-alpine AS builder
|
||||
WORKDIR /app
|
||||
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY . .
|
||||
|
||||
# Build the application
|
||||
ENV NEXT_TELEMETRY_DISABLED 1
|
||||
# Build arguments
|
||||
ARG NEXT_PUBLIC_API_URL
|
||||
ENV NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL}
|
||||
ENV NODE_OPTIONS="--max-old-space-size=2048"
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
|
||||
RUN npm run build
|
||||
|
||||
# Production image
|
||||
FROM base AS runner
|
||||
# Production stage
|
||||
FROM node:20-alpine AS runner
|
||||
WORKDIR /app
|
||||
|
||||
ENV NODE_ENV production
|
||||
ENV NEXT_TELEMETRY_DISABLED 1
|
||||
ENV NODE_ENV=production
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
|
||||
# Create non-root user
|
||||
RUN addgroup --system --gid 1001 nodejs
|
||||
RUN adduser --system --uid 1001 nextjs
|
||||
|
||||
# Copy built application
|
||||
# Copy built assets
|
||||
COPY --from=builder /app/public ./public
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
||||
@ -40,8 +41,7 @@ USER nextjs
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
ENV PORT 3000
|
||||
ENV HOSTNAME "0.0.0.0"
|
||||
ENV PORT=3000
|
||||
ENV HOSTNAME="0.0.0.0"
|
||||
|
||||
CMD ["node", "server.js"]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user