Créer un logiciel open‑source avec Next.js et Supabase — guide complet

Pourquoi (et quand) ouvrir votre code

Bénéfices : transparence, relecture collective, attractivité développeur, adoption et pérennité. Risques/contraintes : charge de maintenance, exposition publique (bugs, dette), gouvernance à définir.

Quand l’ouvrir ?

Dès le MVP si vous voulez co‑construire avec la communauté et accepter l’instabilité. Après un premier jalon stable si vous voulez créer une première impression solide.

Quoi ouvrir ?

Le code de l’app, la doc, le suivi d’issues et la roadmap. Évitez d’ouvrir des secrets, clés API, données privées, ou un historique Git contenant des secrets (faites un scan + purge au besoin).

Choisir une licence et un modèle de gouvernance

Licences courantes :

Règle simple : si vous voulez maximiser l’adoption (ex. template Next.js), MIT est souvent suffisante.

Gouvernance :

Créez des documents clairs :

Arborescence cible (exemple)

.
├─ apps/
  ├─ web/           # Next.js
  ├─ api/           # Express/Nest
├─ infra/
  └─ compose/
     ├─ docker-compose.yml
     ├─ traefik/
  └─ traefik.yml
     └─ backups/
├─ docs/
├─ .github/workflows/ci.yml
├─ LICENSE, README.md, CONTRIBUTING.md, SECURITY.md
└─ .env.example

Phase 1: Bootstrap dépôt & qualité

Critères : npm run ci passe en local et sur PR.

Créer le dépôt & initialiser npm

git init
mkdir -p apps/web
npm init -y

apps/web/package.json

{
  "name": "oss-stack",
  "private": true,
  "packageManager": "pnpm@latest",
  "type": "module",
  "workspaces": ["apps/*"],
  "engines": { "node": ">=20.10.0" },
  "scripts": {
    "prepare": "husky || true",
    "lint": "eslint .",
    "format": "prettier --check .",
    "format:write": "prettier --write .",
    "typecheck": "tsc -p tsconfig.base.json --noEmit",
    "test": "vitest run",
    "test:watch": "vitest",
    "dev": "concurrently -k -n api,web -c blue,green \"pnpm -F ./apps/api dev\" \"pnpm -F ./apps/web dev\""
  }
}

Installer les dépendances “qualité”

pnpm add -D -w \
  eslint prettier typescript \
  @typescript-eslint/parser @typescript-eslint/eslint-plugin eslint-config-prettier \
  vitest @vitest/coverage-v8 \
  husky lint-staged \
  @commitlint/cli @commitlint/config-conventional \
  concurrently globals

Fichiers de base (ignore/éditeur/Prettier/TS)

apps/web/.gitignore

node_modules/
npm-debug.log*
pnpm-debug.log*
yarn-debug.log*
package-lock.json
dist/
coverage/
.env
.env.*
!.env.example
.DS_Store
Thumbs.db

.editorconfig

root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 2
trim_trailing_whitespace = true

.prettierrc

{ "semi": true, "singleQuote": false, "trailingComma": "es5", "printWidth": 100 }

tsconfig.base.json

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ES2022",
    "moduleResolution": "Bundler",
    "strict": true,
    "skipLibCheck": true,
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "jsx": "react-jsx",
    "noEmit": true,
    "types": ["vitest/globals"],
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true
  },
  "exclude": ["node_modules", "dist", "coverage"]
}

eslint.config.js

// eslint.config.js (flat config, ESM)
import js from "@eslint/js";
import tseslint from "typescript-eslint";
import prettier from "eslint-config-prettier";
import globals from "globals";

/** @type {import('eslint').Linter.FlatConfig[]} */
export default [
  {
    ignores: [
      "dist/**",
      "coverage/**",
      "node_modules/**",
      "**/.next/**",
      "**/next-env.d.ts",
      "**/*.config.js",
      "**/*.config.ts",
      "**/*.config.mjs",
      "**/.commitlintrc.cjs",
    ],
  },
  js.configs.recommended,
  ...tseslint.configs.recommended,
  {
    files: ["**/*.{ts,tsx,js,jsx}"],
    languageOptions: {
      ecmaVersion: 2022,
      sourceType: "module",
      globals: {
        ...globals.es2022,
      },
      parserOptions: {
        project: false,
      },
    },
    rules: {
      "@typescript-eslint/no-explicit-any": "off",
      "@typescript-eslint/triple-slash-reference": "off",
      "@typescript-eslint/no-empty-object-type": "off",
    },
  },
  {
    files: ["apps/api/**/*.{ts,js}"],
    languageOptions: {
      globals: {
        ...globals.node,
      },
    },
  },
  {
    files: ["apps/web/**/*.{ts,tsx,js,jsx}"],
    languageOptions: {
      globals: {
        ...globals.browser,
      },
    },
  },
  prettier,
];

.lintstagedrc

{
  "**/*.{ts,tsx,js,jsx,json,md,css,scss,html,yaml,yml}": ["prettier --write"],
  "**/*.{ts,tsx,js,jsx}": ["eslint --fix"]
}

.commitlintrc.cjs

module.exports = { extends: ["@commitlint/config-conventional"] };

vitest.config.ts

import { defineConfig } from "vitest/config";
export default defineConfig({
  test: { environment: "node", coverage: { reporter: ["text", "html"] } }
});
pnpm exec husky init

.husky/pre-commit

pnpm exec lint-staged

.husky/commit-msg

pnpm exec commitlint --edit "$1"

Installation du projet NextJS

pnpm create next-app@latest apps/web \
  --ts --eslint --app --tailwind --src-dir --import-alias "@/*" --use-pnpm

apps/web/tsconfig.json

{
  "extends": "../../tsconfig.base.json",
  "compilerOptions": {
    "baseUrl": ".",
    "paths": { "@/*": ["./src/*"] },
    "types": ["vitest/globals", "node"]
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
  "exclude": ["node_modules"]
}

apps/web/.eslintrc.json

{
  "extends": ["next/core-web-vitals", "next/typescript"]
}

apps/web/next.config.ts

import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  /* config options here */
  output: "standalone", // <-- important pour l'image Docker prod légère
};

export default nextConfig;

Test du front

pnpm --filter ./apps/web dev

API

mkdir -p apps/api/src
cat > apps/api/package.json <<'JSON'
{
  "name": "api",
  "version": "0.0.0",
  "private": true,
  "type": "module",
  "scripts": {
    "dev": "tsx watch src/main.ts",
    "build": "tsc -p tsconfig.build.json",
    "start": "node dist/main.js",
    "test": "vitest run",
    "test:watch": "vitest"
  }
}
JSON

cat > apps/api/tsconfig.json <<'JSON'
{
  "extends": "../../tsconfig.base.json",
  "compilerOptions": {
    "outDir": "dist"
  },
  "include": ["src", "test"]
}

JSON

cat > apps/api/tsconfig.build.json <<'JSON'
{
  "extends": "./tsconfig.json",
  "compilerOptions": { 
    "noEmit": false,
    "skipLibCheck": true,
    "downlevelIteration": true,
    "strict": false,
    "noImplicitAny": false,
    "noImplicitReturns": false,
    "noUnusedLocals": false,
    "noUnusedParameters": false,
    "exactOptionalPropertyTypes": false,
    "module": "CommonJS",
    "target": "ES2020",
    "moduleResolution": "Node"
  },
  "exclude": ["test", "**/*.test.ts", "**/*.spec.ts"]
}
JSON

Installation des dépendences

# depuis la racine du repo
pnpm -F ./apps/api add @nestjs/common @nestjs/core @nestjs/platform-fastify rxjs
pnpm -F ./apps/api add @nestjs/swagger swagger-ui-dist
pnpm -F ./apps/api add @fastify/helmet @fastify/cors @fastify/compress
pnpm -F ./apps/api add reflect-metadata @fastify/static @nestjs/config zod

# dev deps pour l'API
pnpm -F ./apps/api add -D typescript @types/node tsx vitest supertest @types/supertest

apps/api/src/main.ts

import 'reflect-metadata';
import { NestFactory } from '@nestjs/core';
import { FastifyAdapter, NestFastifyApplication } from '@nestjs/platform-fastify';
import fastifyCors from '@fastify/cors';
import fastifyHelmet from '@fastify/helmet';
import fastifyCompress from '@fastify/compress';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
import { AppModule } from './app.module.js';

async function bootstrap() {
  const app = await NestFactory.create<NestFastifyApplication>(
    AppModule,
    new FastifyAdapter({ logger: true })
  );
  await app.register(fastifyHelmet, { contentSecurityPolicy: false });
  await app.register(fastifyCors, { origin: true, credentials: true });
  await app.register(fastifyCompress);

  app.setGlobalPrefix('api');

  const config = new DocumentBuilder().setTitle('OSS API').setVersion('0.1.0').addBearerAuth().build();
  const doc = SwaggerModule.createDocument(app, config);
  SwaggerModule.setup('api/docs', app, doc, { swaggerOptions: { persistAuthorization: true } });

  const port = Number(process.env.PORT ?? 4000);
  await app.listen({ port, host: '0.0.0.0' });
}
bootstrap();

apps/api/src/app.module.ts

import { Module } from '@nestjs/common';
import { HealthModule } from './health/health.module.js';
import { ProjectsModule } from './projects/projects.module.js';
@Module({ imports: [HealthModule, ProjectsModule] })
export class AppModule {}

apps/api/src/health/health.module.ts

import { Module } from '@nestjs/common';
import { HealthController } from './health.controller.js';
@Module({ controllers: [HealthController] })
export class HealthModule {}

apps/api/src/health/health.controller.ts

import { Controller, Get } from '@nestjs/common';
@Controller('healthz')
export class HealthController {
  @Get() get() { return { ok: true, uptime: process.uptime() }; }
}

apps/api/src/projects/projects.module.ts

import { Module } from '@nestjs/common';
import { ProjectsController } from './projects.controller.js';
import { ProjectsService } from './projects.service.js';
@Module({ controllers: [ProjectsController], providers: [ProjectsService] })
export class ProjectsModule {}

apps/api/src/projects/projects.service.ts

import { Injectable, NotFoundException } from '@nestjs/common';
type Project = { id: number; name: string; description?: string; createdAt: Date };
@Injectable()
export class ProjectsService {
  private seq = 1;
  private data = new Map<number, Project>();
  list() { return [...this.data.values()]; }
  get(id: number) { const p = this.data.get(id); if (!p) throw new NotFoundException(); return p; }
  create(dto: { name: string; description?: string }) {
    const p: Project = { id: this.seq++, name: dto.name, description: dto.description, createdAt: new Date() };
    this.data.set(p.id, p); return p;
  }
  update(id: number, dto: Partial<Omit<Project, 'id'|'createdAt'>>) {
    const p = this.get(id); const u = { ...p, ...dto }; this.data.set(id, u); return u;
  }
  remove(id: number) { if (!this.data.delete(id)) throw new NotFoundException(); }
}

apps/api/src/projects/projects.controller.ts

import { Body, Controller, Delete, Get, Param, ParseIntPipe, Patch, Post } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { ProjectsService } from './projects.service.js';
@ApiTags('projects')
@Controller('projects')
export class ProjectsController {
  constructor(private readonly svc: ProjectsService) {}
  @Get() list() { return this.svc.list(); }
  @Get(':id') get(@Param('id', ParseIntPipe) id: number) { return this.svc.get(id); }
  @Post() create(@Body() body: { name: string; description?: string }) { return this.svc.create(body); }
  @Patch(':id') update(@Param('id', ParseIntPipe) id: number, @Body() body: any) { return this.svc.update(id, body); }
  @Delete(':id') remove(@Param('id', ParseIntPipe) id: number) { this.svc.remove(id); return { ok: true }; }
}

Test

apps/api/test/healthz.test.ts

import "reflect-metadata";
import { beforeAll, afterAll, describe, it, expect } from "vitest";
import { NestFactory } from "@nestjs/core";
import { FastifyAdapter, NestFastifyApplication } from "@nestjs/platform-fastify";
import request from "supertest";
import { AppModule } from "../src/app.module.js";

let app: NestFastifyApplication;

beforeAll(async () => {
  app = await NestFactory.create<NestFastifyApplication>(
    AppModule,
    new FastifyAdapter()
  );
  await app.init();
  // Fastify doit être "ready" avant d'accepter les requêtes
  await app.getHttpAdapter().getInstance().ready();
});

afterAll(async () => {
  await app.close();
});

describe("Healthcheck", () => {
  it("GET /api/healthz → 200 + { ok: true, uptime: number }", async () => {
    const res = await request(app.getHttpServer()).get("/api/healthz");

    expect(res.status).toBe(200);
    expect(res.headers["content-type"]).toMatch(/application\/json/);
    expect(res.body).toHaveProperty("ok", true);
    expect(typeof res.body.uptime).toBe("number");
  });
});
pnpm -F ./apps/api dev
# Swagger ⇒ http://localhost:4000/api/docs
# Health  ⇒ http://localhost:4000/api/healthz

Fichiers open-source & CI

.github/workflows/ci.yml

name: CI
on:
  pull_request:
  push:
    branches: [main]
jobs:
  ci:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: corepack enable
      - run: corepack prepare pnpm@10.15.1 --activate
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: "pnpm"
      - run: pnpm install --frozen-lockfile
      - run: pnpm run typecheck
      - run: pnpm run lint
      - run: pnpm test

README.md


![CI Status](https://github.com/ldandoy/open-source-projet-template/workflows/CI/badge.svg)
![License](https://img.shields.io/badge/license-MIT-blue.svg)
![Node Version](https://img.shields.io/badge/node-%3E%3D20.10.0-green.svg)
![pnpm](https://img.shields.io/badge/pnpm-10.15.1-orange.svg)

- **apps/web** : Next.js/React opérationnel.
- **apps/api** : Express TypeScript sur **:4000** (health + hello).
- **Qualité** : ESLint, Prettier, TypeScript strict, Vitest, Husky + lint-staged, commitlint. 
- **Dev** : `pnpm dev` lance Web + API ensemble.

SECURITY.md

# Sécurité
Merci de signaler les vulnérabilités par email à security@exemple.org.
Ne créez pas d’issue publique pour des failles non corrigées.

CODE_OF_CONDUCT.md

# Code de conduite
Nous adoptons le Contributor Covenant v2.1. Voir https://www.contributor-covenant.org/

CONTRIBUTING.md

Ajout des .env

apps/api/src/config/env.ts

import { z } from "zod";

/** Schéma de validation des variables d'env */
export const EnvSchema = z.object({
  NODE_ENV: z.enum(["development", "test", "production"]).default("development"),
  PORT: z.coerce.number().int().positive().default(4000),

  // Placeholders pour la suite (Phase 2+)
  DATABASE_URL: z.string().url().optional(),
  KEYCLOAK_URL: z.string().url().optional(),
  REDIS_URL: z.string().url().optional(),
  MINIO_ENDPOINT: z.string().optional(),
});

export type Env = z.infer<typeof EnvSchema>;

apps/api/src/app.module.ts

import { Module } from "@nestjs/common";
import { ConfigModule } from "@nestjs/config";
import { EnvSchema } from "./config/env.js";
import { HealthModule } from "./health/health.module.js";
import { ProjectsModule } from "./projects/projects.module.js";

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
      // ordre de priorité (du + spécifique au + générique)
      envFilePath: [
        `.env.${process.env.NODE_ENV}.local`,
        `.env.${process.env.NODE_ENV}`,
        `.env.local`,
        `.env`,
      ],
      validate: (raw) => EnvSchema.parse(raw), // ❗ fail fast si invalide/manquant
    }),
    HealthModule,
    ProjectsModule,
  ],
})
export class AppModule {}

On teste tout !

pnpm lint
git add . && git commit -m 'build: inital commit' && git push --set-upstram

Phase 2: Phase 2 — Infra locale Docker Compose

.dockerignore

# Dependencies
node_modules/
.pnpm-store/

# Git
.git/
.gitignore

# Development tools
.husky/
.vscode/
.cursor/

# Logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*

# Runtime data
pids
*.pid
*.seed
*.pid.lock

# Coverage directory used by tools like istanbul
coverage/
*.lcov

# Environment files
.env
.env.local
.env.development.local
.env.test.local
.env.production.local

# Build outputs
dist/
build/
.next/
out/

# Tests
**/__tests__/
**/*.test.js
**/*.test.ts
**/*.spec.js
**/*.spec.ts

# Documentation
README.md
CHANGELOG.md
*.md

# CI/CD
.github/

# Others
.DS_Store
Thumbs.db

infra/compsoe/docker-compose.yml

services:
  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: dev
      POSTGRES_PASSWORD: devpass
      POSTGRES_DB: ossdb
    ports: 
      - "5432:5432"
    volumes: 
      - pg_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U dev -d ossdb"]
      interval: 30s
      timeout: 10s
      retries: 3
    restart: unless-stopped

  pgadmin:
    image: dpage/pgadmin4:8.8
    depends_on:
      db:
        condition: service_healthy
    environment:
      PGADMIN_DEFAULT_EMAIL: ${PGADMIN_DEFAULT_EMAIL:-admin@example.com}
      PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_DEFAULT_PASSWORD:-admin}
    ports:
      - "8081:80"        # UI: http://localhost:8081
    volumes:
      - pgadmin_data:/var/lib/pgadmin   # (persistant)
    restart: unless-stopped

# Mail de dev (SMTP + UI)
  mailpit:
    image: axllent/mailpit:latest
    environment:
      MP_MAX_MESSAGES: 10000
    ports:
      - "1025:1025"      # SMTP
      - "8025:8025"      # Web UI
    restart: unless-stopped

  api:
    build:
      context: ../..
      dockerfile: apps/api/Dockerfile
    environment:
      PORT: 5000
      DATABASE_URL: postgres://dev:devpass@db:5432/ossdb
    depends_on: [ db ]
    ports: ["5000:5000"]   # http://localhost:5000
    restart: unless-stopped

  web:
    build:
      context: ../..
      dockerfile: apps/web/Dockerfile
    environment:
      NEXT_PUBLIC_API_BASE_URL: http://localhost:5000
    depends_on: [ api ]
    ports: ["3000:3000"]   # http://localhost:3000
    restart: unless-stopped

volumes:
  pg_data:
  pgadmin_data:

infra/compsoe/Makefile

SHELL := /bin/bash
PROJECT := oss-local

.PHONY: help up down logs ps restart clean build rebuild status dev

help: ## Affiche cette aide
	@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}'

up: ## Démarre tous les services
	docker compose -p $(PROJECT) -f docker-compose.yml up -d

dev: ## Démarre uniquement les services d'infrastructure pour le développement (DB, pgAdmin, Mailpit)
	@echo "🚀 Démarrage des services d'infrastructure pour le développement..."
	docker compose -p $(PROJECT) -f docker-compose.yml up db pgadmin mailpit -d
	@echo ""
	@echo "✅ Services d'infrastructure démarrés !"
	@echo ""
	@echo "📋 Services disponibles :"
	@echo "   • PostgreSQL  : localhost:5432"
	@echo "   • pgAdmin     : http://localhost:8081 (admin@example.com / admin)"
	@echo "   • Mailpit     : http://localhost:8025"
	@echo ""
	@echo "🏃 Pour lancer les apps en mode développement :"
	@echo "   cd ../../ && pnpm dev"
	@echo ""

down: ## Arrête tous les services
	docker compose -p $(PROJECT) -f docker-compose.yml down

logs: ## Affiche les logs en temps réel
	docker compose -p $(PROJECT) -f docker-compose.yml logs -f

ps: ## Liste les conteneurs
	docker compose -p $(PROJECT) -f docker-compose.yml ps

restart: ## Redémarre tous les services
	docker compose -p $(PROJECT) -f docker-compose.yml restart

clean: ## Arrête et supprime les volumes
	docker compose -p $(PROJECT) -f docker-compose.yml down -v

build: ## Construit les images
	docker compose -p $(PROJECT) -f docker-compose.yml build

rebuild: ## Reconstruit les images sans cache
	docker compose -p $(PROJECT) -f docker-compose.yml build --no-cache


status: ## Affiche le statut des services
	@echo "=== Statut des services ==="
	@docker compose -p $(PROJECT) -f docker-compose.yml ps --format "table {{.Name}}\t{{.Status}}\t{{.Ports}}"

apps/web/Dockerfile

# --- Base commune ---
    FROM node:20-alpine AS base
    WORKDIR /workspace
    ENV PNPM_HOME="/root/.local/share/pnpm"
    ENV PATH="$PNPM_HOME:$PATH"
    RUN corepack enable
    
    # --- Dépendances pour builder ---
    FROM base AS builder-deps
    # Désactive Husky dans Docker
    ENV HUSKY=0
    COPY package.json pnpm-lock.yaml pnpm-workspace.yaml tsconfig.base.json ./
    COPY apps/web/package.json apps/web/tsconfig.json apps/web/next.config.ts ./apps/web/
    RUN pnpm -r --filter ./apps/web... install --frozen-lockfile
    
    # --- Build ---
    FROM builder-deps AS build
    ENV NEXT_TELEMETRY_DISABLED=1
    COPY apps/web ./apps/web
    # Si tu as un dossier public/, il sera inclus automatiquement par Next
    RUN pnpm -F ./apps/web build
    
    # --- Runner (standalone) ---
    FROM node:20-alpine AS runner
    WORKDIR /app
    ENV NODE_ENV=production
    ENV NEXT_TELEMETRY_DISABLED=1
    # Copie la sortie standalone générée par Next
    COPY --from=build /workspace/apps/web/.next/standalone ./
    # Copie les assets statiques et public
    COPY --from=build /workspace/apps/web/.next/static ./apps/web/.next/static
    COPY --from=build /workspace/apps/web/public ./apps/web/public
    # Port par défaut Next
    ENV PORT=3000
    EXPOSE 3000
    CMD ["node", "apps/web/server.js"]
    

apps/web/.dockerignore

# Dependencies
node_modules/

# Build outputs
.next/
out/
coverage/

# Environment files
.env
.env.*

# Development files
.husky/
*.log

apps/api/Dockerfile

# --- Base commune ---
    FROM node:20-alpine AS base
    WORKDIR /workspace
    ENV PNPM_HOME="/root/.local/share/pnpm"
    ENV PATH="$PNPM_HOME:$PATH"
    RUN corepack enable
    
    # --- Dépendances pour builder (avec dev deps) ---
    FROM base AS builder-deps
    # Désactive Husky dans Docker
    ENV HUSKY=0
    # Copie fichiers du monorepo nécessaires à l'install
    COPY package.json pnpm-lock.yaml pnpm-workspace.yaml tsconfig.base.json ./
    # Copie les manifests de l'app API
    COPY apps/api/package.json apps/api/tsconfig.json apps/api/tsconfig.build.json ./apps/api/
    # Installe seulement ce qui est nécessaire pour l'app API
    RUN pnpm -r --filter ./apps/api... install --frozen-lockfile
    
    # --- Build ---
    FROM builder-deps AS build
    # Copie le code source de l'API
    COPY apps/api ./apps/api
    # Build (génère apps/api/dist)
    RUN pnpm -F ./apps/api build
    
    # --- Dépendances prod minimalistes ---
    FROM base AS prod-deps
    # Désactive Husky dans Docker
    ENV HUSKY=0
    COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
    COPY apps/api/package.json ./apps/api/
    RUN pnpm -r --filter ./apps/api... install --frozen-lockfile --prod
    
    # --- Runner ---
    FROM node:20-alpine AS runner
    ENV NODE_ENV=production
    # Copie TOUT l'environnement de build - approche simple mais efficace
    COPY --from=build /workspace /workspace
    WORKDIR /workspace/apps/api
    # Port interne Nest
    ENV PORT=4000
    EXPOSE 4000
    CMD ["node", "dist/main.js"]
    

apps/api/.dockerignore

# Dependencies
node_modules/

# Build outputs
dist/
coverage/

# Environment files
.env
.env.*

# Tests
test/
**/*.test.ts
**/*.spec.ts

# Development files
.husky/
*.log
Loïc DANDOY,