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 :
- MIT : permissive, très simple. Idéale pour un starter, lib/front. => https://opensource.org/license/mit
- Apache‑2.0 : permissive + clause de brevet ; bon choix pour les libs d’entreprise. => https://www.apache.org/licenses/LICENSE-2.0.txt
- GPL‑3.0 : copyleft fort ; impose que les dérivés restent sous GPL. => https://www.gnu.org/licenses/gpl-3.0.txt
Règle simple : si vous voulez maximiser l’adoption (ex. template Next.js), MIT est souvent suffisante.
Gouvernance :
- BDFL / Maintainer principal (décide en dernier ressort).
- Core team (comité de mainteneurs avec règles de vote).
- RFCs (propositions de changements majeurs soumises à discussion).
Créez des documents clairs :
- CONTRIBUTING.md,
- CODE_OF_CONDUCT.md (Contributor Covenant),
- SECURITY.md (procédure de signalement de vulnérabilités),
- GOVERNANCE.md (optionnel, si le projet grossit).
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é
- Monorepo (ou multi-repo) avec dossiers :
apps/web,apps/api,infra/compose. - Outils qualité : ESLint, Prettier, Husky + lint-staged, commitlint (Conventional Commits), Vitest/Testing Library.
- Actions GitHub : CI de base (typecheck, lint, test, build).
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




- **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