production: docker fixes, nginx SSL config, generateStaticParams fallback
Deploy to VPS / deploy (push) Has been cancelled

This commit is contained in:
2026-04-01 16:57:22 +00:00
parent fc24313f15
commit 0118774e31
6 changed files with 64 additions and 128 deletions
+1
View File
@@ -41,3 +41,4 @@ yarn-error.log*
next-env.d.ts next-env.d.ts
/src/generated/prisma /src/generated/prisma
certbot/
+1
View File
@@ -25,6 +25,7 @@ RUN DATABASE_URL="postgresql://dummy:dummy@localhost:5432/dummy" npx prisma gene
# Disable telemetry during build # Disable telemetry during build
ENV NEXT_TELEMETRY_DISABLED=1 ENV NEXT_TELEMETRY_DISABLED=1
ENV DATABASE_URL="postgresql://dummy:dummy@localhost:5432/dummy"
RUN npm run build RUN npm run build
-107
View File
@@ -1,107 +0,0 @@
# ═══════════════════════════════════════════════════════════════
# FLUX SRL — Nginx SSL Configuration (rf-flux.com)
# Swap this in after running certbot:
# cp nginx/conf.d/flux-ssl.conf nginx/conf.d/flux.conf
# docker compose restart nginx
# ═══════════════════════════════════════════════════════════════
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=login:10m rate=3r/m;
upstream nextjs {
server app:3000;
keepalive 32;
}
# HTTP → HTTPS redirect
server {
listen 80;
server_name rf-flux.com www.rf-flux.com;
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
location / {
return 301 https://$host$request_uri;
}
}
# HTTPS
server {
listen 443 ssl http2;
server_name rf-flux.com www.rf-flux.com;
# SSL Certificates
ssl_certificate /etc/letsencrypt/live/rf-flux.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/rf-flux.com/privkey.pem;
# SSL Hardening (A+ on SSL Labs)
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off;
ssl_stapling on;
ssl_stapling_verify on;
# HSTS (2 years)
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
# Content Security Policy
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: blob:; connect-src 'self' https://api.openai.com; media-src 'self'; object-src 'none'; frame-ancestors 'self';" always;
location /_next/static/ {
proxy_pass http://nextjs;
expires 365d;
add_header Cache-Control "public, immutable";
access_log off;
}
location /footage/ {
proxy_pass http://nextjs;
expires 30d;
add_header Cache-Control "public";
access_log off;
}
location /hq-command/ {
limit_req zone=login burst=5 nodelay;
proxy_pass http://nextjs;
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 /api/chat {
limit_req zone=api burst=20 nodelay;
proxy_pass http://nextjs;
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_http_version 1.1;
proxy_set_header Connection '';
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 120s;
}
location /api/health {
proxy_pass http://nextjs;
access_log off;
}
location / {
proxy_pass http://nextjs;
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_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
+32 -15
View File
@@ -1,10 +1,5 @@
# ═══════════════════════════════════════════════════════════════
# FLUX SRL — Nginx Site Configuration
# Phase 1: HTTP only (SSL added after certbot runs)
# ═══════════════════════════════════════════════════════════════
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s; limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=login:10m rate=3r/m; limit_req_zone $binary_remote_addr zone=login:10m rate=5r/s;
upstream nextjs { upstream nextjs {
server app:3000; server app:3000;
@@ -15,12 +10,31 @@ server {
listen 80; listen 80;
server_name rf-flux.com www.rf-flux.com; server_name rf-flux.com www.rf-flux.com;
# Let's Encrypt challenge (keep this even after SSL)
location /.well-known/acme-challenge/ { location /.well-known/acme-challenge/ {
root /var/www/certbot; root /var/www/certbot;
} }
# Static assets: Next.js hashed (immutable cache) location / {
return 301 https://$host$request_uri;
}
}
server {
listen 443 ssl;
http2 on;
server_name rf-flux.com www.rf-flux.com;
ssl_certificate /etc/letsencrypt/live/rf-flux.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/rf-flux.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off;
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
location /_next/static/ { location /_next/static/ {
proxy_pass http://nextjs; proxy_pass http://nextjs;
expires 365d; expires 365d;
@@ -28,7 +42,6 @@ server {
access_log off; access_log off;
} }
# Static assets: footage, GLB models
location /footage/ { location /footage/ {
proxy_pass http://nextjs; proxy_pass http://nextjs;
expires 30d; expires 30d;
@@ -36,9 +49,16 @@ server {
access_log off; access_log off;
} }
# CMS Admin (strict rate limiting) location /hq-command/login {
location /hq-command/ { limit_req zone=login burst=10 nodelay;
limit_req zone=login burst=5 nodelay; proxy_pass http://nextjs;
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 /hq-command/ {
proxy_pass http://nextjs; proxy_pass http://nextjs;
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
@@ -46,7 +66,6 @@ server {
proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Proto $scheme;
} }
# FluxAI Chat API (streaming + rate limited)
location /api/chat { location /api/chat {
limit_req zone=api burst=20 nodelay; limit_req zone=api burst=20 nodelay;
proxy_pass http://nextjs; proxy_pass http://nextjs;
@@ -61,13 +80,11 @@ server {
proxy_read_timeout 120s; proxy_read_timeout 120s;
} }
# Health check
location /api/health { location /api/health {
proxy_pass http://nextjs; proxy_pass http://nextjs;
access_log off; access_log off;
} }
# Default
location / { location / {
proxy_pass http://nextjs; proxy_pass http://nextjs;
proxy_set_header Host $host; proxy_set_header Host $host;
+14 -2
View File
@@ -30,8 +30,20 @@ function getApplicationImages(slug: string) {
// GENERACIÓN DE RUTAS ESTÁTICAS DESDE LA BD // GENERACIÓN DE RUTAS ESTÁTICAS DESDE LA BD
export async function generateStaticParams() { export async function generateStaticParams() {
const apps = await prisma.application.findMany({ select: { slug: true } }); // In production Docker build, DB is not available.
return apps.map((app: { slug: string }) => ({ slug: app.slug })); // Pages are generated on-demand via SSR instead.
if (process.env.NODE_ENV === 'production' && !process.env.VERCEL) {
return [];
}
try {
const apps = await prisma.application.findMany({
select: { slug: true },
});
return apps.map((app: any) => ({ slug: app.slug }));
} catch {
return [];
}
} }
export const revalidate = 60; export const revalidate = 60;
+13 -1
View File
@@ -10,8 +10,20 @@ import BreathingField from "@/components/visuals/BreathingField";
import { getLocalizedData } from "@/lib/i18nHelper"; import { getLocalizedData } from "@/lib/i18nHelper";
export async function generateStaticParams() { export async function generateStaticParams() {
const articles = await prisma.newsArticle.findMany({ select: { slug: true } }); // In production Docker build, DB is not available.
// Pages are generated on-demand via SSR instead.
if (process.env.NODE_ENV === 'production' && !process.env.VERCEL) {
return [];
}
try {
const articles = await prisma.newsArticle.findMany({
select: { slug: true },
});
return articles.map((a: any) => ({ slug: a.slug })); return articles.map((a: any) => ({ slug: a.slug }));
} catch {
return [];
}
} }
// ── SÚPER PARSER MARKDOWN (Con Tablas, Imágenes y Dark/Light Mode) ── // ── SÚPER PARSER MARKDOWN (Con Tablas, Imágenes y Dark/Light Mode) ──