SYS-18: add production deployment stack for karbe.cosmolan.fr (Stripe test)
- enable Next.js standalone output and add Docker/Caddy production stack - add production env template and deployment runbook - add healthcheck endpoint for container supervision - fix existing lint/type blockers discovered during validation Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
parent
0a366c65db
commit
c9be24a969
11 changed files with 201 additions and 5 deletions
|
|
@ -10,6 +10,12 @@ AUTH_SECRET="changeme"
|
|||
# URL publique du site, utilisée pour résoudre les URLs canoniques et
|
||||
# OpenGraph (SEO). En développement, laissez la valeur par défaut.
|
||||
NEXT_PUBLIC_SITE_URL="http://localhost:3000"
|
||||
APP_URL="http://localhost:3000"
|
||||
|
||||
# Stripe (mode test recommandé pour le MVP)
|
||||
STRIPE_SECRET_KEY="sk_test_xxx"
|
||||
STRIPE_WEBHOOK_SECRET="whsec_xxx"
|
||||
STRIPE_OWNER_SUBSCRIPTION_PRICE_ID="price_xxx"
|
||||
|
||||
# Stockage objet des médias (S3 ou MinIO). Compatible AWS S3 et MinIO.
|
||||
# Pour MinIO en local : renseignez S3_ENDPOINT (ex: http://localhost:9000)
|
||||
|
|
|
|||
31
.env.production.example
Normal file
31
.env.production.example
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
NODE_ENV=production
|
||||
PORT=3000
|
||||
|
||||
# Domain
|
||||
NEXT_PUBLIC_SITE_URL=https://karbe.cosmolan.fr
|
||||
APP_URL=https://karbe.cosmolan.fr
|
||||
|
||||
# Security
|
||||
NEXTAUTH_SECRET=replace_with_random_secret
|
||||
AUTH_SECRET=replace_with_random_secret
|
||||
|
||||
# Database (managed PostgreSQL recommended)
|
||||
DATABASE_URL=postgresql://user:password@db-host:5432/karbe?schema=public
|
||||
|
||||
# Stripe TEST
|
||||
STRIPE_SECRET_KEY=sk_test_xxx
|
||||
STRIPE_WEBHOOK_SECRET=whsec_xxx
|
||||
STRIPE_OWNER_SUBSCRIPTION_PRICE_ID=price_xxx
|
||||
|
||||
# Storage S3 / MinIO
|
||||
S3_ENDPOINT=https://s3.example.com
|
||||
S3_REGION=eu-west-3
|
||||
S3_BUCKET=karbe-medias
|
||||
S3_ACCESS_KEY_ID=replace_me
|
||||
S3_SECRET_ACCESS_KEY=replace_me
|
||||
S3_PUBLIC_URL=https://cdn.example.com/karbe-medias
|
||||
S3_FORCE_PATH_STYLE=false
|
||||
|
||||
# Recommended for stable multi-instance deploys
|
||||
NEXT_SERVER_ACTIONS_ENCRYPTION_KEY=replace_with_base64_32_bytes
|
||||
DEPLOYMENT_VERSION=manual-v1
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -33,6 +33,7 @@ yarn-error.log*
|
|||
# env files (can opt-in for committing if needed)
|
||||
.env*
|
||||
!.env.example
|
||||
!.env.production.example
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
|
|
|||
28
Dockerfile
Normal file
28
Dockerfile
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
FROM node:20-alpine AS base
|
||||
WORKDIR /app
|
||||
|
||||
FROM base AS deps
|
||||
COPY package.json package-lock.json ./
|
||||
RUN npm ci
|
||||
|
||||
FROM base AS builder
|
||||
ENV NODE_ENV=production
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
|
||||
FROM node:20-alpine AS runner
|
||||
WORKDIR /app
|
||||
ENV NODE_ENV=production
|
||||
ENV PORT=3000
|
||||
|
||||
RUN addgroup -S nextjs && adduser -S nextjs -G nextjs
|
||||
|
||||
COPY --from=builder /app/public ./public
|
||||
COPY --from=builder /app/.next/standalone ./
|
||||
COPY --from=builder /app/.next/static ./.next/static
|
||||
|
||||
USER nextjs
|
||||
EXPOSE 3000
|
||||
|
||||
CMD ["node", "server.js"]
|
||||
60
README.md
60
README.md
|
|
@ -15,6 +15,7 @@ et payer leur séjour en ligne.
|
|||
- [Prérequis](#prérequis)
|
||||
- [Installation](#installation)
|
||||
- [Variables d'environnement](#variables-denvironnement)
|
||||
- [Déploiement production (karbe.cosmolan.fr)](#déploiement-production-karbecosmolanfr)
|
||||
- [Développement](#développement)
|
||||
- [Base de données (Prisma)](#base-de-données-prisma)
|
||||
- [Scripts npm](#scripts-npm)
|
||||
|
|
@ -115,11 +116,70 @@ Copiez-le en `.env` et renseignez vos valeurs.
|
|||
| --- | --- |
|
||||
| `DATABASE_URL` | Chaîne de connexion PostgreSQL utilisée par Prisma. |
|
||||
| `AUTH_SECRET` / `NEXTAUTH_SECRET` | Secret de signature des sessions NextAuth. Générer avec `openssl rand -base64 32`. |
|
||||
| `NEXT_PUBLIC_SITE_URL` / `APP_URL` | URL publique de l'application (ex: `https://karbe.cosmolan.fr`). |
|
||||
| `STRIPE_SECRET_KEY` | Clé secrète Stripe (mode test pour MVP). |
|
||||
| `STRIPE_WEBHOOK_SECRET` | Secret du webhook Stripe. |
|
||||
| `STRIPE_OWNER_SUBSCRIPTION_PRICE_ID` | Price ID de l'abonnement loueur Stripe. |
|
||||
|
||||
> Le fichier `.env` ne doit **jamais** être commité (il est ignoré par Git).
|
||||
> Au fur et à mesure de l'ajout des intégrations (ex. Stripe), de nouvelles
|
||||
> variables seront ajoutées à `.env.example`.
|
||||
|
||||
## Déploiement production (karbe.cosmolan.fr)
|
||||
|
||||
Le repo inclut une stack de self-hosting Docker pour le MVP:
|
||||
|
||||
- `Dockerfile` (build Next.js standalone)
|
||||
- `docker-compose.prod.yml` (app + reverse proxy Caddy HTTPS)
|
||||
- `docker/Caddyfile`
|
||||
- `.env.production.example`
|
||||
|
||||
### 1) Préparer le serveur
|
||||
|
||||
- Docker Engine + Docker Compose plugin installés.
|
||||
- DNS `A`/`AAAA` de `karbe.cosmolan.fr` pointant vers le serveur.
|
||||
- Ports `80` et `443` ouverts.
|
||||
|
||||
### 2) Configurer l'environnement
|
||||
|
||||
```bash
|
||||
cp .env.production.example .env.production
|
||||
```
|
||||
|
||||
Renseigner toutes les variables, en particulier `DATABASE_URL`,
|
||||
`STRIPE_SECRET_KEY`, `STRIPE_WEBHOOK_SECRET`, `STRIPE_OWNER_SUBSCRIPTION_PRICE_ID`,
|
||||
`APP_URL` et `NEXT_PUBLIC_SITE_URL`.
|
||||
|
||||
### 3) Appliquer les migrations Prisma
|
||||
|
||||
Les migrations doivent être appliquées avant le premier démarrage :
|
||||
|
||||
```bash
|
||||
npx prisma migrate deploy
|
||||
```
|
||||
|
||||
### 4) Lancer la stack
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.prod.yml up -d --build
|
||||
```
|
||||
|
||||
Vérifier la santé:
|
||||
|
||||
```bash
|
||||
curl -fsS https://karbe.cosmolan.fr/api/health
|
||||
```
|
||||
|
||||
### 5) Connecter le webhook Stripe (TEST)
|
||||
|
||||
Depuis un poste ayant Stripe CLI:
|
||||
|
||||
```bash
|
||||
stripe listen --forward-to https://karbe.cosmolan.fr/api/stripe/webhook
|
||||
```
|
||||
|
||||
Copier le secret affiché (`whsec_...`) dans `STRIPE_WEBHOOK_SECRET`, puis redéployer.
|
||||
|
||||
## Développement
|
||||
|
||||
```bash
|
||||
|
|
|
|||
42
docker-compose.prod.yml
Normal file
42
docker-compose.prod.yml
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
services:
|
||||
app:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
container_name: karbe-app
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- .env.production
|
||||
networks:
|
||||
- karbe
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "-qO", "-", "http://127.0.0.1:3000/api/health"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 30s
|
||||
|
||||
caddy:
|
||||
image: caddy:2.10-alpine
|
||||
container_name: karbe-caddy
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
depends_on:
|
||||
app:
|
||||
condition: service_healthy
|
||||
volumes:
|
||||
- ./docker/Caddyfile:/etc/caddy/Caddyfile:ro
|
||||
- caddy_data:/data
|
||||
- caddy_config:/config
|
||||
networks:
|
||||
- karbe
|
||||
|
||||
networks:
|
||||
karbe:
|
||||
name: karbe-net
|
||||
|
||||
volumes:
|
||||
caddy_data:
|
||||
caddy_config:
|
||||
14
docker/Caddyfile
Normal file
14
docker/Caddyfile
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
email ops@cosmolan.fr
|
||||
}
|
||||
|
||||
karbe.cosmolan.fr {
|
||||
encode zstd gzip
|
||||
reverse_proxy app:3000
|
||||
|
||||
header {
|
||||
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
|
||||
X-Content-Type-Options "nosniff"
|
||||
Referrer-Policy "strict-origin-when-cross-origin"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
/* config options here */
|
||||
output: "standalone",
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
|
|
|
|||
7
src/app/api/health/route.ts
Normal file
7
src/app/api/health/route.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import { NextResponse } from "next/server";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
|
||||
export async function GET() {
|
||||
return NextResponse.json({ status: "ok" });
|
||||
}
|
||||
|
|
@ -97,11 +97,16 @@ async function handlePaymentIntentFailed(paymentIntent: Stripe.PaymentIntent) {
|
|||
}
|
||||
|
||||
async function handleSubscriptionUpdated(subscription: Stripe.Subscription) {
|
||||
const currentPeriodEnd =
|
||||
subscription.items.data[0]?.current_period_end ??
|
||||
subscription.trial_end ??
|
||||
subscription.canceled_at;
|
||||
|
||||
await prisma.subscription.upsert({
|
||||
where: { providerSubId: subscription.id },
|
||||
update: {
|
||||
status: mapStripeSubscriptionStatus(subscription.status),
|
||||
renewedAt: fromStripeTimestamp(subscription.current_period_end),
|
||||
renewedAt: fromStripeTimestamp(currentPeriodEnd),
|
||||
canceledAt: fromStripeTimestamp(subscription.canceled_at),
|
||||
},
|
||||
create: {
|
||||
|
|
@ -111,7 +116,7 @@ async function handleSubscriptionUpdated(subscription: Stripe.Subscription) {
|
|||
providerSubId: subscription.id,
|
||||
status: mapStripeSubscriptionStatus(subscription.status),
|
||||
startedAt: fromStripeTimestamp(subscription.start_date) ?? new Date(),
|
||||
renewedAt: fromStripeTimestamp(subscription.current_period_end),
|
||||
renewedAt: fromStripeTimestamp(currentPeriodEnd),
|
||||
canceledAt: fromStripeTimestamp(subscription.canceled_at),
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import Link from "next/link";
|
||||
|
||||
import type { CarbetSearchFilters } from "@/lib/carbet-search";
|
||||
|
||||
type SearchFiltersProps = {
|
||||
|
|
@ -72,12 +74,12 @@ export function SearchFilters({ filters, rivers }: SearchFiltersProps) {
|
|||
</label>
|
||||
|
||||
<div className="flex items-end gap-2 sm:col-span-2 lg:col-span-5 lg:justify-end">
|
||||
<a
|
||||
<Link
|
||||
href="/carbets"
|
||||
className="rounded-md border border-zinc-300 px-4 py-2 text-sm text-zinc-700 hover:bg-zinc-50"
|
||||
>
|
||||
Réinitialiser
|
||||
</a>
|
||||
</Link>
|
||||
<button
|
||||
type="submit"
|
||||
className="rounded-md bg-emerald-600 px-5 py-2 text-sm font-semibold text-white hover:bg-emerald-700"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue