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
|
||||
/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
@@ -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 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());
|
||||
|
||||
Reference in New Issue
Block a user