From 6c020e7ee3b88c03e8025bcbbc33339a958c0d56 Mon Sep 17 00:00:00 2001 From: Mathieu Date: Fri, 15 May 2026 14:03:46 +0200 Subject: [PATCH] =?UTF-8?q?feat(docker):=20finalise=20le=20d=C3=A9ploiemen?= =?UTF-8?q?t=20compose=20(sqlite=20+=20postgres)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Compose nettoyé en deux profils isolés (sqlite, postgres) avec healthcheck HTTP et network_mode host pour la découverte LAN. - Override docker-compose.bridge.yml pour les environnements où host mode n'est pas disponible (macOS/Windows). - Entrypoint tolérant : fallback prisma db push quand aucune migration n'existe encore. - Dockerfile robuste sans package-lock.json (npm install fallback). - .env.docker.example et docker/README.md pour un démarrage en une commande. Co-Authored-By: Claude Opus 4.7 --- .dockerignore | 67 +++++++++++++++++-- .env.docker.example | 52 +++++++++++++++ docker/Dockerfile | 81 ++++++++++++++++++----- docker/README.md | 109 +++++++++++++++++++++++++++++++ docker/docker-compose.bridge.yml | 43 ++++++++++++ docker/docker-compose.yml | 83 +++++++++++++++++++---- docker/entrypoint.sh | 11 +++- 7 files changed, 411 insertions(+), 35 deletions(-) create mode 100644 .env.docker.example create mode 100644 docker/README.md create mode 100644 docker/docker-compose.bridge.yml diff --git a/.dockerignore b/.dockerignore index 07c1bc1..7614b1d 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,15 +1,74 @@ +# --------------------------------------------------------------- +# Docker build context — exclusions +# --------------------------------------------------------------- + +# Dependencies (réinstallés dans l'image) node_modules +.pnp +.pnp.js + +# Build artifacts .next -.git +out +dist +build +*.tsbuildinfo +next-env.d.ts + +# Env (les secrets ne doivent jamais être copiés dans l'image) .env .env.local .env.*.local -npm-debug.log -Dockerfile -.dockerignore +.env.production +.env.development.local +.env.test.local + +# Logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Git +.git +.gitignore +.gitattributes + +# IDE .vscode .idea +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db + +# Docker (évite la récursion). On NE PEUT PAS exclure docker/ car +# l'image a besoin de docker/entrypoint.sh ; on se contente d'exclure +# le compose et le dockerignore qui sont inutiles dans l'image. +docker/docker-compose.yml +docker/docker-compose.bridge.yml +.dockerignore + +# Bases locales (SQLite dev) +data +*.db +*.db-journal +prisma/data +prisma/*.db +prisma/*.db-journal + +# Tests & coverage tests coverage +.nyc_output +__tests__ + +# Docs (sauf README) *.md !README.md + +# CI / Misc +.github +.husky diff --git a/.env.docker.example b/.env.docker.example new file mode 100644 index 0000000..61c6aef --- /dev/null +++ b/.env.docker.example @@ -0,0 +1,52 @@ +# --------------------------------------------------------------- +# IPAM — Variables pour un déploiement Docker +# --------------------------------------------------------------- +# Copie ce fichier en `.env` à la racine du projet avant de lancer +# `docker compose`. Le compose lit `../.env` (env_file). +# --------------------------------------------------------------- + +NODE_ENV=production + +# --------------------------------------------------------------- +# Base de données +# --------------------------------------------------------------- +# Choix par défaut : SQLite — aucune dépendance externe. +# Pour basculer sur PostgreSQL : +# 1) Lance le profil postgres : `docker compose --profile postgres up -d` +# 2) Décommente la section PG ci-dessous et commente celle SQLite. +DATABASE_PROVIDER=sqlite +DATABASE_URL="file:/app/data/ipam.db" + +# --- Variante PostgreSQL -------------------------------------- +# DATABASE_PROVIDER=postgresql +# DATABASE_URL="postgresql://ipam:ipam@127.0.0.1:5432/ipam?schema=public" +# POSTGRES_PASSWORD=ipam + +# --------------------------------------------------------------- +# Application +# --------------------------------------------------------------- +PORT=3000 +APP_URL=http://localhost:3000 + +# Seed initial au premier démarrage (catégories d'apps, etc.) +RUN_SEED=true + +# --------------------------------------------------------------- +# Découverte réseau +# --------------------------------------------------------------- +# Adapte la plage à ton LAN (séparées par des virgules) +DISCOVERY_DEFAULT_CIDRS=192.168.1.0/24 + +DISCOVERY_DEFAULT_PORTS=22,53,80,81,443,445,3000,3306,5432,6379,7878,8080,8081,8096,8123,8443,8989,9000,9090,9091,9117,9443,32400 +DISCOVERY_PING_TIMEOUT=1000 +DISCOVERY_PORT_TIMEOUT=800 +DISCOVERY_CONCURRENCY=32 +DISCOVERY_ENABLE_PING=true +DISCOVERY_ENABLE_PORT_SCAN=true +DISCOVERY_ENABLE_ARP=true +DISCOVERY_ENABLE_MDNS=true + +# --------------------------------------------------------------- +# Logs +# --------------------------------------------------------------- +LOG_LEVEL=info diff --git a/docker/Dockerfile b/docker/Dockerfile index a709ce2..f1322b8 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,44 +1,95 @@ # syntax=docker/dockerfile:1.6 +# --------------------------------------------------------------- +# IPAM — Dockerfile multi-stage +# --------------------------------------------------------------- +# Build arg : +# DATABASE_PROVIDER = sqlite (défaut) | postgresql +# Rappel : le provider Prisma est statique dans schema.prisma, on +# le bascule via scripts/switch-db-provider.mjs avant `prisma generate`. +# --------------------------------------------------------------- + +ARG DATABASE_PROVIDER=sqlite + # ---------- Stage 1 : deps ---------- FROM node:20-alpine AS deps RUN apk add --no-cache libc6-compat WORKDIR /app COPY package.json package-lock.json* ./ -RUN npm ci +# `npm ci` exige un package-lock.json ; sinon on retombe sur `npm install` +# qui en générera un. Cela permet un build « out of the box » même sans lock. +RUN if [ -f package-lock.json ]; then \ + npm ci; \ + else \ + echo "package-lock.json absent -> npm install" && \ + npm install --no-audit --no-fund; \ + fi # ---------- Stage 2 : build ---------- FROM node:20-alpine AS builder +ARG DATABASE_PROVIDER +ENV DATABASE_PROVIDER=${DATABASE_PROVIDER} + +# Valeur factice pour que Zod ne rejette pas la config à la génération +ENV DATABASE_URL="file:./data/build-placeholder.db" + WORKDIR /app COPY --from=deps /app/node_modules ./node_modules COPY . . -RUN npx prisma generate -RUN npm run build + +# Bascule le provider Prisma si nécessaire, puis génère le client et build +RUN node scripts/switch-db-provider.mjs "$DATABASE_PROVIDER" \ + && npx prisma generate \ + && npm run build # ---------- Stage 3 : runner ---------- FROM node:20-alpine AS runner -WORKDIR /app - +ARG DATABASE_PROVIDER +ENV DATABASE_PROVIDER=${DATABASE_PROVIDER} ENV NODE_ENV=production ENV PORT=3000 +ENV HOSTNAME=0.0.0.0 -# Outils système utiles à la découverte réseau -# - iputils : ping ICMP (privileged / cap_net_raw) -# - iproute2 : ip neigh (ARP) -# - nmap : fallback scan ports (optionnel, décommente si besoin) -RUN apk add --no-cache iputils iproute2 +WORKDIR /app -RUN addgroup --system --gid 1001 nodejs -RUN adduser --system --uid 1001 nextjs +# Outils système utiles à la découverte et à l'entrypoint : +# - iputils : ping ICMP (cap_net_raw requis côté compose) +# - iproute2 : `ip neigh` pour le scanner ARP +# - netcat-openbsd : attente de PostgreSQL (entrypoint) +# - openssl, tini, dumb-init : init + signaux propres +RUN apk add --no-cache iputils iproute2 netcat-openbsd tini openssl -COPY --from=builder /app/public ./public +# User non-root pour la sécurité +RUN addgroup --system --gid 1001 nodejs \ + && adduser --system --uid 1001 nextjs + +# Binaires app (mode standalone Next) +COPY --from=builder --chown=nextjs:nodejs /app/public ./public COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static + +# Prisma : schema + client généré + migrations (pour `migrate deploy` au boot) COPY --from=builder --chown=nextjs:nodejs /app/prisma ./prisma COPY --from=builder --chown=nextjs:nodejs /app/node_modules/.prisma ./node_modules/.prisma COPY --from=builder --chown=nextjs:nodejs /app/node_modules/@prisma ./node_modules/@prisma +COPY --from=builder --chown=nextjs:nodejs /app/node_modules/prisma ./node_modules/prisma +# tsx + dépendances pour pouvoir exécuter prisma/seed.ts si RUN_SEED=true +COPY --from=builder --chown=nextjs:nodejs /app/node_modules/tsx ./node_modules/tsx +COPY --from=builder --chown=nextjs:nodejs /app/node_modules/esbuild ./node_modules/esbuild + +# Entrypoint +COPY --chown=nextjs:nodejs docker/entrypoint.sh /app/entrypoint.sh +RUN chmod +x /app/entrypoint.sh + +# Volume SQLite (inutile en PG mais inoffensif) +RUN mkdir -p /app/data && chown -R nextjs:nodejs /app/data +VOLUME ["/app/data"] USER nextjs - EXPOSE 3000 -CMD ["node", "server.js"] +# Healthcheck simple — retourne 200 dès que Next répond +HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \ + CMD wget --no-verbose --tries=1 --spider http://localhost:3000/ || exit 1 + +# tini pour gérer SIGTERM proprement +ENTRYPOINT ["/sbin/tini", "--", "/app/entrypoint.sh"] diff --git a/docker/README.md b/docker/README.md new file mode 100644 index 0000000..b38b7ae --- /dev/null +++ b/docker/README.md @@ -0,0 +1,109 @@ +# IPAM — Déploiement Docker + +Tout ce qu'il faut pour lancer l'application en une commande. + +## Pré-requis + +- Docker Engine ≥ 24 +- Docker Compose v2 (`docker compose` et non `docker-compose`) +- Linux conseillé (le mode `network_mode: host` n'est pas pleinement supporté sur Docker Desktop macOS/Windows ; voir la section *Bridge mode* plus bas) + +## Fichiers fournis + +| Fichier | Rôle | +|---|---| +| `Dockerfile` | Image multi-stage (deps → build → runner) basée sur `node:20-alpine` | +| `entrypoint.sh` | Attente PostgreSQL, migrations Prisma, seed optionnel, lancement Next | +| `docker-compose.yml` | Compose principal — profils `sqlite` et `postgres`, mode host | +| `docker-compose.bridge.yml` | Override pour passer en mode bridge (sans découverte LAN) | + +## Démarrage rapide — profil SQLite + +```bash +# 1. Prépare la config +cp .env.docker.example .env +# (édite .env si besoin — surtout DISCOVERY_DEFAULT_CIDRS) + +# 2. Build + run +docker compose -f docker/docker-compose.yml --profile sqlite up -d --build + +# 3. Logs +docker compose -f docker/docker-compose.yml logs -f ipam +``` + +L'UI est disponible sur . + +## Profil PostgreSQL + +```bash +cp .env.docker.example .env +# Dans .env, bascule sur PostgreSQL : +# DATABASE_PROVIDER=postgresql +# DATABASE_URL="postgresql://ipam:ipam@127.0.0.1:5432/ipam?schema=public" + +docker compose -f docker/docker-compose.yml --profile postgres up -d --build +``` + +Le container `ipam-postgres` expose le port `5432` sur l'hôte (utile pour `prisma studio` ou un client SQL externe). + +## Bridge mode (sans découverte LAN) + +Utile sur macOS / Windows ou pour un test rapide de l'UI : + +```bash +docker compose \ + -f docker/docker-compose.yml \ + -f docker/docker-compose.bridge.yml \ + --profile sqlite up -d --build +``` + +⚠️ En bridge, **ARP / mDNS / ping sweep ne fonctionneront pas** sur ton LAN — seule la saisie manuelle reste pleinement utilisable. + +## Commandes utiles + +```bash +# Arrêter +docker compose -f docker/docker-compose.yml down + +# Tout supprimer (y compris les volumes — perte des données !) +docker compose -f docker/docker-compose.yml down -v + +# Re-seeder manuellement +docker exec -it ipam npx tsx prisma/seed.ts + +# Prisma studio (depuis l'hôte, profil postgres) +DATABASE_URL="postgresql://ipam:ipam@localhost:5432/ipam?schema=public" npx prisma studio + +# Shell dans le container +docker exec -it ipam sh +``` + +## Variables d'environnement clés + +Toutes les variables sont documentées dans [`.env.docker.example`](../.env.docker.example). +Les plus importantes pour Docker : + +- `DATABASE_PROVIDER` — `sqlite` (défaut) ou `postgresql` +- `DATABASE_URL` — fichier `file:/app/data/ipam.db` ou DSN PostgreSQL +- `RUN_SEED` — `true` pour exécuter `prisma/seed.ts` au premier boot +- `DISCOVERY_DEFAULT_CIDRS` — plages scannées par défaut, à adapter à ton LAN + +## Persistance + +| Volume | Contenu | +|---|---| +| `ipam-data` | Base SQLite (`/app/data/ipam.db`) | +| `ipam-postgres` | Données PostgreSQL | + +Les deux survivent à `docker compose down`, mais pas à `down -v`. + +## Pourquoi `network_mode: host` ? + +Pour qu'un container puisse : +- envoyer des paquets ICMP (ping sweep) +- lire la table ARP (`ip neigh`) du réseau local +- recevoir les annonces mDNS (port 5353 UDP multicast) + +…il doit partager la pile réseau de l'hôte. Sans `host`, ces fonctionnalités sont aveugles au LAN. + +Les `cap_add: [NET_RAW, NET_ADMIN]` permettent par ailleurs au binaire `ping` du container d'ouvrir un socket ICMP en non-root. diff --git a/docker/docker-compose.bridge.yml b/docker/docker-compose.bridge.yml new file mode 100644 index 0000000..9cff003 --- /dev/null +++ b/docker/docker-compose.bridge.yml @@ -0,0 +1,43 @@ +# --------------------------------------------------------------- +# IPAM — Override : bridge network (sans host mode) +# --------------------------------------------------------------- +# À combiner avec docker-compose.yml : +# docker compose \ +# -f docker/docker-compose.yml \ +# -f docker/docker-compose.bridge.yml \ +# --profile sqlite up -d +# +# ATTENTION : en mode bridge la découverte ARP / mDNS / ping sweep +# ne fonctionnera PAS sur le LAN (le container n'a pas accès direct +# au réseau de l'hôte). À utiliser uniquement pour la saisie manuelle +# ou un test rapide de l'UI. +# --------------------------------------------------------------- + +services: + ipam: + network_mode: bridge + ports: + - "3000:3000" + networks: + - ipam-net + + ipam-pg: + network_mode: bridge + ports: + - "3000:3000" + environment: + # En bridge, on cible le service postgres par son nom DNS Docker + DATABASE_URL: "postgresql://ipam:${POSTGRES_PASSWORD:-ipam}@postgres:5432/ipam?schema=public" + networks: + - ipam-net + + postgres: + network_mode: bridge + ports: + - "5432:5432" + networks: + - ipam-net + +networks: + ipam-net: + driver: bridge diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index d1d8b08..2ad9d09 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -2,60 +2,115 @@ # IPAM — Docker Compose # --------------------------------------------------------------- # Deux profils : -# - `sqlite` → ipam seul, base dans un volume -# - `postgres` → ipam + postgres +# - `sqlite` → ipam seul, base SQLite dans un volume +# - `postgres` → ipam + postgres # # Usage : -# docker compose --profile sqlite up -d -# docker compose --profile postgres up -d +# docker compose -f docker/docker-compose.yml --profile sqlite up -d +# docker compose -f docker/docker-compose.yml --profile postgres up -d +# +# Le mode `network_mode: host` est indispensable pour que la découverte +# (ARP, mDNS, ping sweep, port scan) atteigne le LAN. Sans lui, le +# container ne voit que le bridge Docker. # --------------------------------------------------------------- services: + # ========================================================= + # Service IPAM — variante SQLite (autonome, sans PostgreSQL) + # ========================================================= ipam: build: context: .. dockerfile: docker/Dockerfile + args: + DATABASE_PROVIDER: sqlite + image: ipam-homelab:sqlite container_name: ipam restart: unless-stopped - # Network mode host : indispensable pour ARP / mDNS / ping sweep - # sur le réseau local. Commenter cette ligne si non nécessaire. network_mode: host environment: NODE_ENV: production PORT: 3000 + HOSTNAME: 0.0.0.0 + DATABASE_PROVIDER: sqlite + DATABASE_URL: "file:/app/data/ipam.db" + RUN_SEED: "${RUN_SEED:-false}" env_file: - ../.env volumes: - ipam-data:/app/data - # Capabilities requises pour le ping ICMP raw cap_add: - NET_RAW - NET_ADMIN - profiles: [sqlite, postgres] + healthcheck: + test: ["CMD-SHELL", "wget -q --spider http://localhost:3000/ || exit 1"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 30s + profiles: ["sqlite"] + + # ========================================================= + # Service IPAM — variante PostgreSQL + # ========================================================= + ipam-pg: + build: + context: .. + dockerfile: docker/Dockerfile + args: + DATABASE_PROVIDER: postgresql + image: ipam-homelab:postgres + container_name: ipam + restart: unless-stopped + network_mode: host + environment: + NODE_ENV: production + PORT: 3000 + HOSTNAME: 0.0.0.0 + DATABASE_PROVIDER: postgresql + DATABASE_URL: "postgresql://ipam:${POSTGRES_PASSWORD:-ipam}@127.0.0.1:5432/ipam?schema=public" + RUN_SEED: "${RUN_SEED:-false}" + env_file: + - ../.env + cap_add: + - NET_RAW + - NET_ADMIN depends_on: postgres: condition: service_healthy - required: false + healthcheck: + test: ["CMD-SHELL", "wget -q --spider http://localhost:3000/ || exit 1"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 30s + profiles: ["postgres"] + # ========================================================= + # PostgreSQL (profil "postgres" uniquement) + # ========================================================= postgres: image: postgres:16-alpine container_name: ipam-postgres restart: unless-stopped + # network_mode host → expose le port 5432 directement sur l'hôte. + network_mode: host environment: POSTGRES_USER: ipam - POSTGRES_PASSWORD: ipam + POSTGRES_PASSWORD: "${POSTGRES_PASSWORD:-ipam}" POSTGRES_DB: ipam + PGPORT: 5432 volumes: - ipam-postgres:/var/lib/postgresql/data - ports: - - '5432:5432' healthcheck: - test: ['CMD-SHELL', 'pg_isready -U ipam'] + test: ["CMD-SHELL", "pg_isready -U ipam -d ipam"] interval: 5s timeout: 3s retries: 10 - profiles: [postgres] + profiles: ["postgres"] volumes: ipam-data: + driver: local ipam-postgres: + driver: local diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index 6122347..de9a534 100755 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -44,8 +44,15 @@ if [ "${DATABASE_PROVIDER:-sqlite}" = "sqlite" ]; then fi # --- 3. Migrations Prisma ----------------------------------------- -log "Application des migrations Prisma…" -npx prisma migrate deploy +# Si des migrations existent → migrate deploy (production-safe) +# Sinon (premier démarrage sans baseline) → db push pour initialiser le schéma +if [ -d /app/prisma/migrations ] && [ "$(find /app/prisma/migrations -mindepth 1 -maxdepth 1 -type d 2>/dev/null | head -n 1)" != "" ]; then + log "Application des migrations Prisma…" + npx prisma migrate deploy +else + log "Aucune migration trouvée — initialisation du schéma via 'prisma db push'…" + npx prisma db push --skip-generate --accept-data-loss +fi # --- 4. Seed optionnel -------------------------------------------- if [ "${RUN_SEED:-false}" = "true" ]; then