add docker

This commit is contained in:
Puechberty Arthur
2026-06-19 07:25:00 +02:00
parent 4bc95c52b1
commit 41245c4870
8 changed files with 394 additions and 2 deletions
+46
View File
@@ -0,0 +1,46 @@
# ── Dépendances (réinstallées dans le Dockerfile) ────────────────────────────
node_modules
npm-debug.log*
yarn-error.log*
.yarn
# ── Secrets (ne jamais inclure dans l'image) ─────────────────────────────────
.env
.env.*
!.env.example
# ── Git ───────────────────────────────────────────────────────────────────────
.git
.gitignore
.gitattributes
# ── Docker (évite les boucles de contexte) ───────────────────────────────────
Dockerfile*
docker-compose*.yml
.dockerignore
# ── Documentation ─────────────────────────────────────────────────────────────
README.md
*.md
docs/
# ── Système / IDE ─────────────────────────────────────────────────────────────
.DS_Store
Thumbs.db
.vscode
.idea
*.swp
*.swo
# ── Logs & temporaires ────────────────────────────────────────────────────────
logs/
*.log
tmp/
.tmp/
# ── Tests ─────────────────────────────────────────────────────────────────────
test/
tests/
*.test.js
*.spec.js
coverage/
+24 -2
View File
@@ -1,2 +1,24 @@
/.env
/node_modules
# Dépendances
node_modules/
# Secrets — NE JAMAIS COMMITTER
.env
.env.local
.env.*.local
# Logs
*.log
logs/
# Fichiers système
.DS_Store
Thumbs.db
# IDE
.vscode/
.idea/
*.swp
*.swo
# Docker runtime
tmp/
+76
View File
@@ -0,0 +1,76 @@
# ═══════════════════════════════════════════════════════════════════════════════
# Stage 1 — Installation des dépendances npm
# ═══════════════════════════════════════════════════════════════════════════════
FROM node:20-slim AS deps
WORKDIR /app
# Libs nécessaires pour que @napi-rs/canvas télécharge/lie ses binaires
RUN apt-get update && apt-get install -y --no-install-recommends \
libfontconfig1 \
libfreetype6 \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*
COPY package*.json ./
# Installation de PROD seulement (pas devDeps)
RUN npm ci --omit=dev --no-audit --no-fund
# ═══════════════════════════════════════════════════════════════════════════════
# Stage 2 — Image finale (allégée)
# ═══════════════════════════════════════════════════════════════════════════════
FROM node:20-slim AS runtime
LABEL org.opencontainers.image.title="Discord Météo Bot"
LABEL org.opencontainers.image.description="Bot Discord météo ultra-détaillé — Open-Meteo, QuickChart, calcul astronomique"
LABEL org.opencontainers.image.source="https://github.com/ton-repo/discord-meteo-bot"
# ── Dépendances système runtime ────────────────────────────────────────────────
# libfontconfig1 + libfreetype6 : runtime @napi-rs/canvas
# fonts-dejavu-core : polices fiables pour le rendu canvas
# fonts-noto : fallback unicode large (emoji inclus si disponible)
# ca-certificates : requêtes HTTPS (Open-Meteo, Discord, QuickChart)
# tzdata : gestion correcte des fuseaux horaires
RUN apt-get update && apt-get install -y --no-install-recommends \
fontconfig \
libfontconfig1 \
libfreetype6 \
fonts-dejavu-core \
ca-certificates \
tzdata \
&& fc-cache -f \
&& rm -rf /var/lib/apt/lists/*
# ── Variables d'environnement ──────────────────────────────────────────────────
ENV NODE_ENV=production \
TZ=Europe/Paris \
NODE_OPTIONS="--max-old-space-size=384"
WORKDIR /app
# ── Copie depuis le stage deps ─────────────────────────────────────────────────
COPY --from=deps /app/node_modules ./node_modules
# ── Code source ───────────────────────────────────────────────────────────────
COPY index.js ./
COPY healthcheck.js ./
COPY commands/ ./commands/
COPY services/ ./services/
# ── Utilisateur non-root (sécurité) ───────────────────────────────────────────
RUN groupadd --gid 1001 botgroup \
&& useradd --uid 1001 --gid botgroup --shell /bin/false --no-create-home botuser \
&& chown -R botuser:botgroup /app
USER botuser
# ── Healthcheck via fichier heartbeat écrit par le bot ────────────────────────
HEALTHCHECK \
--interval=60s \
--timeout=10s \
--start-period=45s \
--retries=3 \
CMD node healthcheck.js
CMD ["node", "index.js"]
+99
View File
@@ -0,0 +1,99 @@
# ─── Discord Météo Bot — Makefile ────────────────────────────────────────────
# Usage : make <cible>
# Nécessite : docker, docker compose
.PHONY: help build up down restart logs status shell clean rebuild update
# Nom du service (doit correspondre à docker-compose.yml)
SERVICE = meteo-bot
# Couleurs
GREEN = \033[0;32m
YELLOW = \033[0;33m
CYAN = \033[0;36m
RESET = \033[0m
help: ## 📖 Afficher cette aide
@echo ""
@echo "$(CYAN)Discord Météo Bot — Commandes disponibles$(RESET)"
@echo "════════════════════════════════════════════"
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) \
| sort \
| awk 'BEGIN {FS = ":.*?## "}; {printf " $(GREEN)%-15s$(RESET) %s\n", $$1, $$2}'
@echo ""
# ── Build ──────────────────────────────────────────────────────────────────────
build: ## 🔨 Builder l'image Docker
@echo "$(YELLOW)→ Build de l'image...$(RESET)"
docker compose build
rebuild: ## 🔨 Rebuild complet sans cache
@echo "$(YELLOW)→ Rebuild sans cache...$(RESET)"
docker compose build --no-cache
# ── Cycle de vie ──────────────────────────────────────────────────────────────
up: ## 🚀 Démarrer le bot (build si nécessaire)
@echo "$(GREEN)→ Démarrage du bot...$(RESET)"
docker compose up -d --build
@echo "$(GREEN)✅ Bot démarré. Logs : make logs$(RESET)"
down: ## 🛑 Arrêter et supprimer le conteneur
@echo "$(YELLOW)→ Arrêt du bot...$(RESET)"
docker compose down
restart: ## 🔄 Redémarrer le bot
@echo "$(YELLOW)→ Redémarrage...$(RESET)"
docker compose restart $(SERVICE)
stop: ## ⏸️ Stopper sans supprimer le conteneur
docker compose stop $(SERVICE)
start: ## ▶️ Démarrer un conteneur déjà créé
docker compose start $(SERVICE)
# ── Monitoring ────────────────────────────────────────────────────────────────
logs: ## 📋 Suivre les logs en temps réel (Ctrl+C pour quitter)
docker compose logs -f --tail=50 $(SERVICE)
status: ## 📊 Statut du conteneur (santé, ressources)
@echo "$(CYAN)── Statut ──────────────────────────────────────────$(RESET)"
@docker compose ps
@echo ""
@echo "$(CYAN)── Santé ───────────────────────────────────────────$(RESET)"
@docker inspect --format='Health: {{.State.Health.Status}} | Uptime: {{.State.StartedAt}}' discord-meteo-bot 2>/dev/null || echo "Conteneur non démarré"
@echo ""
@echo "$(CYAN)── Ressources ──────────────────────────────────────$(RESET)"
@docker stats discord-meteo-bot --no-stream --format "CPU: {{.CPUPerc}} | RAM: {{.MemUsage}}" 2>/dev/null || true
health: ## 🩺 Lancer le healthcheck manuellement
docker exec discord-meteo-bot node healthcheck.js
# ── Shell ──────────────────────────────────────────────────────────────────────
shell: ## 🐚 Ouvrir un shell dans le conteneur (debug)
docker exec -it discord-meteo-bot /bin/bash
# ── Dev ───────────────────────────────────────────────────────────────────────
dev: ## 🛠️ Démarrer en mode développement (hot-reload + debug)
docker compose -f docker-compose.yml -f docker-compose.dev.yml up --build
dev-down: ## 🛑 Arrêter le mode développement
docker compose -f docker-compose.yml -f docker-compose.dev.yml down
# ── Nettoyage ─────────────────────────────────────────────────────────────────
clean: ## 🧹 Supprimer conteneur + volumes + image locale
@echo "$(YELLOW)→ Nettoyage...$(RESET)"
docker compose down -v --rmi local
@echo "$(GREEN)✅ Nettoyage terminé$(RESET)"
prune: ## 🧹 Nettoyer les images et couches Docker inutilisées (global)
docker image prune -f
docker builder prune -f
# ── Déploiement ───────────────────────────────────────────────────────────────
update: ## ⬆️ Mettre à jour le bot (pull git + rebuild + restart)
@echo "$(CYAN)→ Mise à jour...$(RESET)"
git pull
docker compose build --no-cache
docker compose up -d
@echo "$(GREEN)✅ Mise à jour terminée$(RESET)"
@make logs
+45
View File
@@ -0,0 +1,45 @@
# ─── Override développement ───────────────────────────────────────────────────
# Usage : docker compose -f docker-compose.yml -f docker-compose.dev.yml up
#
# Différences avec la prod :
# • Hot-reload via --watch (Node 20+)
# • Code source monté en volume (pas besoin de rebuild à chaque modif)
# • DEBUG_METEO activé
# • Ressources non limitées
# • Pas de healthcheck (démarre plus vite)
services:
meteo-bot:
build:
target: deps # Utilise seulement le stage deps (plus rapide)
image: discord-meteo-bot:dev
container_name: discord-meteo-bot-dev
# Monter le code source en live (hot-reload)
volumes:
- .:/app
- /app/node_modules # Conserver les node_modules du conteneur
command: node --watch index.js
environment:
NODE_ENV: development
DEBUG_METEO: "true"
TZ: Europe/Paris
# Pas de limite en dev
deploy:
resources:
limits:
memory: 1G
cpus: "2.0"
healthcheck:
disable: true
logging:
driver: json-file
options:
max-size: "5m"
max-file: "2"
+52
View File
@@ -0,0 +1,52 @@
# ─── Discord Météo Bot — Docker Compose (production) ─────────────────────────
# Usage :
# docker compose up -d --build → Build + démarrage en arrière-plan
# docker compose logs -f → Suivre les logs en temps réel
# docker compose restart meteo-bot → Redémarrer le bot
# docker compose down → Arrêter et supprimer les conteneurs
services:
meteo-bot:
build:
context: .
dockerfile: Dockerfile
# Forcer le rebuild si le code change :
# docker compose build --no-cache
image: discord-meteo-bot:latest
container_name: discord-meteo-bot
restart: unless-stopped
# ── Variables d'environnement ─────────────────────────────────────────────
# Le fichier .env est lu au démarrage du conteneur (jamais copié dans l'image)
env_file:
- .env
environment:
NODE_ENV: production
TZ: Europe/Paris
# ── Ressources ────────────────────────────────────────────────────────────
deploy:
resources:
limits:
memory: 512M # @napi-rs/canvas peut consommer ~150-200 Mo pour le canvas
cpus: "0.75"
reservations:
memory: 128M
cpus: "0.10"
# ── Healthcheck (reprend la config du Dockerfile, peut être surchargé ici) ─
healthcheck:
test: ["CMD", "node", "healthcheck.js"]
interval: 60s
timeout: 10s
start_period: 45s
retries: 3
# ── Logs ──────────────────────────────────────────────────────────────────
logging:
driver: json-file
options:
max-size: "10m"
max-file: "5"
labels: "service=discord-meteo-bot"
+42
View File
@@ -0,0 +1,42 @@
'use strict';
/**
* Script de healthcheck Docker.
* Le bot principal écrit /tmp/.bot-alive toutes les 30s.
* Ce script lit le timestamp et exit 0 (sain) ou 1 (malade).
*
* Appelé par : HEALTHCHECK CMD node healthcheck.js
*/
const { readFileSync } = require('fs');
const HEARTBEAT_FILE = '/tmp/.bot-alive';
const MAX_AGE_MS = 2 * 60 * 1000; // 2 minutes = 4 cycles de 30s manqués
try {
const raw = readFileSync(HEARTBEAT_FILE, 'utf8').trim();
const ts = parseInt(raw, 10);
if (isNaN(ts)) {
console.error('[healthcheck] ❌ Fichier heartbeat corrompu');
process.exit(1);
}
const ageMs = Date.now() - ts;
const ageSec = Math.round(ageMs / 1000);
if (ageMs > MAX_AGE_MS) {
console.error(`[healthcheck] ❌ Bot inactif depuis ${ageSec}s (seuil : ${MAX_AGE_MS / 1000}s)`);
process.exit(1);
}
console.log(`[healthcheck] ✅ Bot actif — dernier heartbeat il y a ${ageSec}s`);
process.exit(0);
} catch (err) {
if (err.code === 'ENOENT') {
console.error('[healthcheck] ❌ Fichier heartbeat absent (bot pas encore prêt ou crashé)');
} else {
console.error('[healthcheck] ❌ Erreur lecture heartbeat :', err.message);
}
process.exit(1);
}
+10
View File
@@ -7,6 +7,12 @@ const cron = require('node-cron');
const path = require('path');
const fs = require('fs');
// ─── Heartbeat Docker healthcheck ─────────────────────────────────────────────
const HEARTBEAT_FILE = '/tmp/.bot-alive';
const writeHeartbeat = () => {
try { fs.writeFileSync(HEARTBEAT_FILE, Date.now().toString()); } catch (_) {}
};
// ─── Validation des variables d'environnement ─────────────────────────────────
const REQUIRED_ENV = ['DISCORD_TOKEN', 'CHANNEL_ID', 'LAT', 'LON'];
for (const key of REQUIRED_ENV) {
@@ -38,6 +44,10 @@ client.once('ready', async () => {
console.log(`📍 Localisation : ${process.env.LOCATION_NAME || `${process.env.LAT}°, ${process.env.LON}°`}`);
console.log(`⏰ Message quotidien : ${process.env.SEND_HOUR}h${String(process.env.SEND_MINUTE || 0).padStart(2,'0')}\n`);
// Heartbeat immédiat + toutes les 30s pour Docker healthcheck
writeHeartbeat();
setInterval(writeHeartbeat, 30_000);
// ── Enregistrement global des commandes slash ──────────────────────────────
const rest = new REST({ version: '10' }).setToken(process.env.DISCORD_TOKEN);
const payload = [...client.commands.values()].map(c => c.data.toJSON());