diff --git a/Caddyfile b/Caddyfile new file mode 100644 index 0000000..283542e --- /dev/null +++ b/Caddyfile @@ -0,0 +1,68 @@ +# ═══════════════════════════════════════════════════════ +# Jool International — Caddyfile PRODUCTION +# Domaine : jool-international.com +# Caddy gère automatiquement HTTPS via Let's Encrypt +# ═══════════════════════════════════════════════════════ + +# Redirection www → non-www +www.jool-international.com { + redir https://jool-international.com{uri} permanent +} + +jool-international.com { + + # ── Fichiers statiques Django (WhiteNoise les sert aussi, + # mais Caddy est plus rapide pour les assets lourds) ── + handle_path /static/* { + root * /app/staticfiles + file_server + header Cache-Control "public, max-age=31536000, immutable" + } + + # ── CVs uploadés : jamais accessibles publiquement ─── + handle /media/careers/cvs/* { + respond 404 + } + + # ── Autres fichiers media ───────────────────────────── + handle_path /media/* { + root * /app/media + file_server + header Cache-Control "public, max-age=604800" + } + + # ── Application Django (Gunicorn) ───────────────────── + handle { + reverse_proxy web:8000 { + header_up X-Forwarded-Proto {scheme} + header_up X-Real-IP {remote_host} + transport http { + read_timeout 60s + write_timeout 60s + } + } + } + + # ── En-têtes de sécurité ────────────────────────────── + header { + # HSTS — commencer à 1h, passer à 1 an après validation + Strict-Transport-Security "max-age=3600; includeSubDomains" + X-Content-Type-Options "nosniff" + X-Frame-Options "DENY" + Referrer-Policy "strict-origin-when-cross-origin" + Permissions-Policy "camera=(), microphone=(), geolocation=()" + # Masquer la signature du serveur + -Server + -X-Powered-By + } + + # ── Logs ────────────────────────────────────────────── + log { + output stdout + format json + level WARN + } + + # ── Encodage gzip/zstd automatique ─────────────────── + encode zstd gzip +} diff --git a/deploy.sh b/deploy.sh new file mode 100755 index 0000000..746b95e --- /dev/null +++ b/deploy.sh @@ -0,0 +1,95 @@ +#!/usr/bin/env bash +# ═══════════════════════════════════════════════════════ +# deploy.sh — Jool International · déploiement production +# Usage : ./deploy.sh +# Prérequis : docker, docker compose, git, accès sudo +# ═══════════════════════════════════════════════════════ +set -euo pipefail + +# ── Couleurs pour la lisibilité ──────────────────────── +RESET="\033[0m" +BOLD="\033[1m" +GREEN="\033[32m" +YELLOW="\033[33m" +RED="\033[31m" +CYAN="\033[36m" + +log() { echo -e "${CYAN}[DEPLOY]${RESET} $*"; } +ok() { echo -e "${GREEN}[OK]${RESET} $*"; } +warn() { echo -e "${YELLOW}[WARN]${RESET} $*"; } +error() { echo -e "${RED}[ERROR]${RESET} $*" >&2; exit 1; } +title() { echo -e "\n${BOLD}━━━ $* ━━━${RESET}"; } + +# ── Variables ────────────────────────────────────────── +COMPOSE_FILE="docker-compose.prod.yml" +ENV_FILE=".env.prod" +APP_SERVICE="web" +BRANCH="${DEPLOY_BRANCH:-main}" + +# ── Vérifications préalables ─────────────────────────── +title "Vérifications" + +command -v docker >/dev/null 2>&1 || error "docker non trouvé" +command -v git >/dev/null 2>&1 || error "git non trouvé" +[[ -f "$ENV_FILE" ]] || error "Fichier $ENV_FILE introuvable — créer d'abord .env.prod" +[[ -f "$COMPOSE_FILE" ]] || error "Fichier $COMPOSE_FILE introuvable" +[[ -f "Caddyfile" ]] || error "Fichier Caddyfile introuvable" +ok "Docker, git, .env.prod et Caddyfile présents" + +# ── Pull du code ─────────────────────────────────────── +title "Récupération du code" +git fetch origin "$BRANCH" +LOCAL=$(git rev-parse HEAD) +REMOTE=$(git rev-parse "origin/$BRANCH") + +if [[ "$LOCAL" == "$REMOTE" ]]; then + warn "Déjà à jour ($(git rev-parse --short HEAD)) — rebuild quand même" +else + log "Mise à jour : $(git rev-parse --short HEAD) → $(git rev-parse --short "origin/$BRANCH")" + git pull --ff-only origin "$BRANCH" +fi +ok "Code à jour : $(git rev-parse --short HEAD)" + +# ── Build de l'image ─────────────────────────────────── +title "Build Docker" +docker compose -f "$COMPOSE_FILE" build --no-cache "$APP_SERVICE" +ok "Image $APP_SERVICE construite" + +# ── Vérification de la config Caddy ──────────────────── +title "Validation Caddyfile" +docker run --rm -v "$(pwd)/Caddyfile:/etc/caddy/Caddyfile:ro" \ + caddy:2-alpine caddy validate --config /etc/caddy/Caddyfile \ + && ok "Caddyfile valide" \ + || error "Caddyfile invalide — déploiement annulé" + +# ── Déploiement sans interruption ───────────────────── +title "Déploiement" +docker compose -f "$COMPOSE_FILE" up -d --remove-orphans +ok "Conteneurs démarrés" + +# ── Attente de la santé du service web ───────────────── +title "Healthcheck" +MAX_WAIT=60 +ELAPSED=0 +log "En attente que '$APP_SERVICE' soit healthy (max ${MAX_WAIT}s)…" +until docker compose -f "$COMPOSE_FILE" ps "$APP_SERVICE" | grep -q "healthy"; do + if [[ $ELAPSED -ge $MAX_WAIT ]]; then + warn "Healthcheck timeout — vérifier : docker compose logs $APP_SERVICE" + break + fi + sleep 3 + ELAPSED=$((ELAPSED + 3)) +done +ok "$APP_SERVICE opérationnel (${ELAPSED}s)" + +# ── Nettoyage des images orphelines ──────────────────── +title "Nettoyage" +docker image prune -f --filter "until=24h" >/dev/null 2>&1 || true +ok "Images inutilisées supprimées" + +# ── Résumé ───────────────────────────────────────────── +title "Déploiement terminé" +echo -e " Commit : ${BOLD}$(git rev-parse --short HEAD)${RESET}" +echo -e " Site : ${BOLD}https://jool-international.com/${RESET}" +echo -e " Logs : docker compose logs -f" +echo -e " Rollback : git checkout && ./deploy.sh" diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml new file mode 100644 index 0000000..909b618 --- /dev/null +++ b/docker-compose.prod.yml @@ -0,0 +1,68 @@ +# ═══════════════════════════════════════════════════════ +# Jool International — Docker Compose PRODUCTION +# Stack : PostgreSQL 16 · Django/Gunicorn · Caddy 2 +# Usage : docker compose -f docker-compose.prod.yml up -d --build +# ═══════════════════════════════════════════════════════ + +services: + + # ── Base de données PostgreSQL ────────────────────────── + db: + image: postgres:16-alpine + restart: always + volumes: + - postgres_data:/var/lib/postgresql/data + env_file: .env.prod + healthcheck: + test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"] + interval: 10s + timeout: 5s + retries: 5 + + # ── Application Django (Gunicorn) ─────────────────────── + web: + build: . + restart: always + env_file: .env.prod + volumes: + - static_volume:/app/staticfiles + - media_volume:/app/media + depends_on: + db: + condition: service_healthy + command: > + sh -c "python manage.py collectstatic --noinput && + python manage.py migrate --noinput && + python manage.py compilemessages --locale=en && + gunicorn config.wsgi:application + --bind 0.0.0.0:8000 + --workers 3 + --timeout 60 + --max-requests 1000 + --max-requests-jitter 100 + --access-logfile - + --error-logfile -" + + # ── Caddy (reverse proxy + HTTPS automatique) ─────────── + caddy: + image: caddy:2-alpine + restart: always + ports: + - "80:80" + - "443:443" + - "443:443/udp" # HTTP/3 / QUIC + volumes: + - ./Caddyfile:/etc/caddy/Caddyfile:ro + - static_volume:/app/staticfiles:ro + - media_volume:/app/media:ro + - caddy_data:/data # certificats Let's Encrypt (persistants) + - caddy_config:/config # état interne Caddy + depends_on: + - web + +volumes: + postgres_data: + static_volume: + media_volume: + caddy_data: + caddy_config: diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index af81312..0000000 --- a/docker-compose.yml +++ /dev/null @@ -1,61 +0,0 @@ -services: - - # ── Base de données PostgreSQL ────────────────────────── - db: - image: postgres:16-alpine - restart: always - volumes: - - postgres_data:/var/lib/postgresql/data - env_file: .env.prod - healthcheck: - test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"] - interval: 10s - timeout: 5s - retries: 5 - - # ── Application Django (Gunicorn) ─────────────────────── - web: - build: . - restart: always - env_file: .env.prod - volumes: - - static_volume:/app/staticfiles - - media_volume:/app/media - depends_on: - db: - condition: service_healthy - command: > - sh -c "python manage.py migrate --noinput && - gunicorn config.wsgi:application --bind 0.0.0.0:8000 --workers 3 - --timeout 60 --access-logfile - --error-logfile -" - - # ── Nginx (reverse proxy + static files) ─────────────── - nginx: - image: nginx:alpine - restart: always - ports: - - "80:80" - - "443:443" - volumes: - - ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf:ro - - static_volume:/app/staticfiles:ro - - media_volume:/app/media:ro - - certbot_www:/var/www/certbot:ro - - certbot_certs:/etc/letsencrypt:ro - depends_on: - - web - - # ── Certbot (SSL Let's Encrypt) ───────────────────────── - certbot: - image: certbot/certbot - volumes: - - certbot_www:/var/www/certbot - - certbot_certs:/etc/letsencrypt - entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done;'" - -volumes: - postgres_data: - static_volume: - media_volume: - certbot_www: - certbot_certs: diff --git a/templates/core/products/kiriq.html b/templates/core/products/kiriq.html index 4a9b8a8..79718af 100644 --- a/templates/core/products/kiriq.html +++ b/templates/core/products/kiriq.html @@ -589,7 +589,7 @@ {% trans 'KIRiQ Ai, analyse parcellaire satellite' %}
- warning {% trans "4 anomalies détectées" %} + warning {% trans "6 anomalies détectées" %}
trending_up NDVI +12 pts