mirror of
https://github.com/arthur-pbty/discord-weather-bot.git
synced 2026-08-01 20:28:48 +02:00
add docker
This commit is contained in:
@@ -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
@@ -1,2 +1,24 @@
|
|||||||
/.env
|
# Dépendances
|
||||||
/node_modules
|
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
@@ -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"]
|
||||||
@@ -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
|
||||||
@@ -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"
|
||||||
@@ -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"
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -7,6 +7,12 @@ const cron = require('node-cron');
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
const fs = require('fs');
|
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 ─────────────────────────────────
|
// ─── Validation des variables d'environnement ─────────────────────────────────
|
||||||
const REQUIRED_ENV = ['DISCORD_TOKEN', 'CHANNEL_ID', 'LAT', 'LON'];
|
const REQUIRED_ENV = ['DISCORD_TOKEN', 'CHANNEL_ID', 'LAT', 'LON'];
|
||||||
for (const key of REQUIRED_ENV) {
|
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(`📍 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`);
|
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 ──────────────────────────────
|
// ── Enregistrement global des commandes slash ──────────────────────────────
|
||||||
const rest = new REST({ version: '10' }).setToken(process.env.DISCORD_TOKEN);
|
const rest = new REST({ version: '10' }).setToken(process.env.DISCORD_TOKEN);
|
||||||
const payload = [...client.commands.values()].map(c => c.data.toJSON());
|
const payload = [...client.commands.values()].map(c => c.data.toJSON());
|
||||||
|
|||||||
Reference in New Issue
Block a user