Docker prod

This commit is contained in:
ifaryd
2026-06-11 10:45:23 +00:00
parent 93e9a00994
commit e1656f31a0
5 changed files with 232 additions and 62 deletions

68
Caddyfile Normal file
View File

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

95
deploy.sh Executable file
View File

@@ -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 <commit-précédent> && ./deploy.sh"

68
docker-compose.prod.yml Normal file
View File

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

View File

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

View File

@@ -589,7 +589,7 @@
<img src="{% static 'img/Kiriq AI.jpg' %}" alt="{% trans 'KIRiQ Ai, analyse parcellaire satellite' %}" <img src="{% static 'img/Kiriq AI.jpg' %}" alt="{% trans 'KIRiQ Ai, analyse parcellaire satellite' %}"
style="width:100%;height:auto;display:block;border-radius:24px;" loading="lazy" decoding="async"> style="width:100%;height:auto;display:block;border-radius:24px;" loading="lazy" decoding="async">
<div class="chip-float cf-tl"> <div class="chip-float cf-tl">
<span class="material-icons-round">warning</span> {% trans "4 anomalies détectées" %} <span class="material-icons-round">warning</span> {% trans "6 anomalies détectées" %}
</div> </div>
<div class="chip-float cf-br"> <div class="chip-float cf-br">
<span class="material-icons-round">trending_up</span> NDVI +12 pts <span class="material-icons-round">trending_up</span> NDVI +12 pts