Docker prod
This commit is contained in:
68
Caddyfile
Normal file
68
Caddyfile
Normal 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
95
deploy.sh
Executable 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
68
docker-compose.prod.yml
Normal 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:
|
||||
@@ -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:
|
||||
@@ -589,7 +589,7 @@
|
||||
<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">
|
||||
<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 class="chip-float cf-br">
|
||||
<span class="material-icons-round">trending_up</span> NDVI +12 pts
|
||||
|
||||
Reference in New Issue
Block a user