feat(docker): finalise le déploiement compose (sqlite + postgres)

- 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 <noreply@anthropic.com>
This commit is contained in:
2026-05-15 14:03:46 +02:00
parent c18d831d89
commit 6c020e7ee3
7 changed files with 411 additions and 35 deletions

View File

@@ -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"]

109
docker/README.md Normal file
View File

@@ -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 <http://localhost:3000>.
## 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.

View File

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

View File

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

View File

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