diff --git a/.env.example b/.env.example index 9897eac..beceb0c 100644 --- a/.env.example +++ b/.env.example @@ -1,13 +1,14 @@ -# Database -DATABASE_URI=postgresql://postgres:postgres@localhost:5432/letsbe +# ── PostgreSQL ── +POSTGRES_USER=letsbe +POSTGRES_PASSWORD=your-secure-postgres-password -# Payload CMS -PAYLOAD_SECRET=your-secret-key-here-change-in-production +# ── Payload CMS ── +PAYLOAD_SECRET=generate-a-random-32-char-secret-here -# OpenRouter (for AI brief generation) +# ── OpenRouter (AI brief generation) ── OPENROUTER_API_KEY=your-openrouter-api-key -# Email (Poste.io SMTP) +# ── Email (Poste.io SMTP) ── SMTP_HOST=mail.letsbe.biz SMTP_PORT=587 SMTP_USER=hello@letsbe.biz @@ -15,8 +16,8 @@ SMTP_PASS=your-smtp-password SMTP_FROM=hello@letsbe.biz ADMIN_EMAIL=hello@letsbe.biz -# Cal.com +# ── Cal.com ── NEXT_PUBLIC_CALCOM_URL=https://cal.letsbe.biz -# Site -NEXT_PUBLIC_SITE_URL=https://letsbe.biz +# ── Site URL ── +NEXT_PUBLIC_SITE_URL=https://staging.letsbe.biz diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..befc03c --- /dev/null +++ b/Dockerfile @@ -0,0 +1,49 @@ +FROM node:20-alpine AS base + +# ── Dependencies ── +FROM base AS deps +WORKDIR /app +COPY package.json package-lock.json ./ +RUN npm ci --ignore-scripts +RUN npm rebuild sharp + +# ── Builder ── +FROM base AS builder +WORKDIR /app +COPY --from=deps /app/node_modules ./node_modules +COPY . . + +# Build args become env vars at build time +ARG DATABASE_URI +ARG PAYLOAD_SECRET +ARG NEXT_PUBLIC_SITE_URL +ARG NEXT_PUBLIC_CALCOM_URL + +ENV DATABASE_URI=${DATABASE_URI} +ENV PAYLOAD_SECRET=${PAYLOAD_SECRET} +ENV NEXT_PUBLIC_SITE_URL=${NEXT_PUBLIC_SITE_URL} +ENV NEXT_PUBLIC_CALCOM_URL=${NEXT_PUBLIC_CALCOM_URL} + +RUN npm run build + +# ── Runner ── +FROM base AS runner +WORKDIR /app + +ENV NODE_ENV=production +ENV HOSTNAME=0.0.0.0 +ENV PORT=3000 + +RUN addgroup --system --gid 1001 nodejs +RUN adduser --system --uid 1001 nextjs + +# Copy standalone output +COPY --from=builder /app/.next/standalone ./ +COPY --from=builder /app/.next/static ./.next/static +COPY --from=builder /app/public ./public + +USER nextjs + +EXPOSE 3000 + +CMD ["node", "server.js"] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..02b2a78 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,45 @@ +services: + db: + image: postgres:16-alpine + restart: unless-stopped + environment: + POSTGRES_DB: letsbe + POSTGRES_USER: ${POSTGRES_USER:-letsbe} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD in .env} + volumes: + - pgdata:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-letsbe} -d letsbe"] + interval: 5s + timeout: 5s + retries: 5 + + app: + build: + context: . + args: + DATABASE_URI: postgresql://${POSTGRES_USER:-letsbe}:${POSTGRES_PASSWORD}@db:5432/letsbe + PAYLOAD_SECRET: ${PAYLOAD_SECRET:?Set PAYLOAD_SECRET in .env} + NEXT_PUBLIC_SITE_URL: ${NEXT_PUBLIC_SITE_URL:-https://staging.letsbe.biz} + NEXT_PUBLIC_CALCOM_URL: ${NEXT_PUBLIC_CALCOM_URL:-} + restart: unless-stopped + ports: + - "127.0.0.1:3000:3000" + environment: + DATABASE_URI: postgresql://${POSTGRES_USER:-letsbe}:${POSTGRES_PASSWORD}@db:5432/letsbe + PAYLOAD_SECRET: ${PAYLOAD_SECRET} + NEXT_PUBLIC_SITE_URL: ${NEXT_PUBLIC_SITE_URL:-https://staging.letsbe.biz} + NEXT_PUBLIC_CALCOM_URL: ${NEXT_PUBLIC_CALCOM_URL:-} + SMTP_HOST: ${SMTP_HOST:-} + SMTP_PORT: ${SMTP_PORT:-587} + SMTP_USER: ${SMTP_USER:-} + SMTP_PASS: ${SMTP_PASS:-} + SMTP_FROM: ${SMTP_FROM:-hello@letsbe.biz} + ADMIN_EMAIL: ${ADMIN_EMAIL:-hello@letsbe.biz} + OPENROUTER_API_KEY: ${OPENROUTER_API_KEY:-} + depends_on: + db: + condition: service_healthy + +volumes: + pgdata: diff --git a/nginx/staging.letsbe.biz.conf b/nginx/staging.letsbe.biz.conf new file mode 100644 index 0000000..c00a99f --- /dev/null +++ b/nginx/staging.letsbe.biz.conf @@ -0,0 +1,48 @@ +server { + listen 80; + listen [::]:80; + server_name staging.letsbe.biz; + + # Certbot will add SSL config after: sudo certbot --nginx -d staging.letsbe.biz + + location / { + proxy_pass http://127.0.0.1:3000; + 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 86400; + } + + # Static assets — long cache + location /_next/static/ { + proxy_pass http://127.0.0.1:3000; + proxy_cache_valid 200 365d; + add_header Cache-Control "public, max-age=31536000, immutable"; + } + + # Public assets + location /images/ { + proxy_pass http://127.0.0.1:3000; + proxy_cache_valid 200 30d; + add_header Cache-Control "public, max-age=2592000"; + } + + # Payload media uploads + location /media/ { + proxy_pass http://127.0.0.1:3000; + proxy_cache_valid 200 30d; + add_header Cache-Control "public, max-age=2592000"; + } + + # Security headers + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + + client_max_body_size 50M; +}