Added a nginx reverse proxy to handle the two services (FastAPI+Streamlit) in one single container. Now, it is possible using the two services with only one exposed port (mandatory on Google Cloud)

This commit is contained in:
Grégoire Mureau 2025-07-10 11:34:59 +02:00
parent 1720a7c454
commit b388c0b3f9
8 changed files with 79 additions and 45 deletions

View file

@ -76,6 +76,8 @@ Pour le lancement du docker en local (développement), il sera important de donn
Pour un déploiement sur un cloud personnel ou grand public (avec docker-compose, kubernetes), il sera préférable de renseigner les variables d'environnement nécessaires dans `docker-compose.yml` ou dans les secrets. Pour un déploiement sur un cloud personnel ou grand public (avec docker-compose, kubernetes), il sera préférable de renseigner les variables d'environnement nécessaires dans `docker-compose.yml` ou dans les secrets.
Ajouter --rm comme argument pour que le conteneur soit supprimé après son arrêt.
Dans notre cas, il a été décidé d'utiliser Google Cloud. Une problématique classique se pose ici pour nous : on ne peut exposer plusieurs ports (contrairement à l'instruction ci-dessus) sur un service cloud. Dans notre cas, il a été décidé d'utiliser Google Cloud. Une problématique classique se pose ici pour nous : on ne peut exposer plusieurs ports (contrairement à l'instruction ci-dessus) sur un service cloud.
2 options sont donc possibles: 2 options sont donc possibles:
- Séparer le service en deux docker (un pour le backend fastAPI et un pour le frontend streamlit) - Séparer le service en deux docker (un pour le backend fastAPI et un pour le frontend streamlit)

View file

@ -0,0 +1,5 @@
[server]
enableXsrfProtection = false
enableCORS = false
enableWebsocketCompression = false
baseUrlPath = "/"

View file

@ -1,22 +1,33 @@
# Utiliser une image légère Python 3.12
FROM python:3.12-slim FROM python:3.12-slim
# Définir le dossier de travail
WORKDIR /app WORKDIR /app
# Copier uniquement requirements.txt d'abord (pour optimiser le cache Docker) # Mettre à jour les packages et installer nginx
COPY requirements.txt . RUN apt-get update && \
apt-get install -y nginx && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
# Installer les dépendances # Copier les fichiers de requirements et installer les dépendances Python
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt RUN pip install --no-cache-dir -r requirements.txt
# Copier le reste du code source # Copier le code de l'application
COPY . . COPY . .
# Exposer les ports et 8501 (streamlit) et 8000 (fastAPI) # Copier la configuration nginx
COPY nginx.conf /etc/nginx/nginx.conf
EXPOSE 8501 8000 # Créer les répertoires nécessaires pour nginx
RUN mkdir -p /var/log/nginx && \
mkdir -p /var/lib/nginx && \
mkdir -p /run
# Donner les droits d'exécution au script de démarrage
RUN chmod +x /app/start.sh RUN chmod +x /app/start.sh
# Exposer le port 8080
EXPOSE 8080
# Lancer le script de démarrage
CMD ["/app/start.sh"] CMD ["/app/start.sh"]

View file

@ -3,6 +3,8 @@ import requests
st.title("Analyse de Sentiment - Air Paradis") st.title("Analyse de Sentiment - Air Paradis")
API_BASE_URL = "http://127.0.0.1:8000"
tweet = st.text_area("Entrez votre tweet ici :", height=150) tweet = st.text_area("Entrez votre tweet ici :", height=150)
# Initialisation des états # Initialisation des états
@ -32,7 +34,7 @@ if st.button("Analyser le sentiment"):
else: else:
try: try:
response = requests.post( response = requests.post(
"http://localhost:8000/predict", f"{API_BASE_URL}/predict",
json={"tweet": tweet} json={"tweet": tweet}
) )
if response.status_code == 200: if response.status_code == 200:
@ -60,7 +62,7 @@ if st.session_state.show_report_button and not st.session_state.report_sent:
if st.button("Signaler une mauvaise prédiction"): if st.button("Signaler une mauvaise prédiction"):
try: try:
response = requests.post( response = requests.post(
"http://localhost:8000/report_error", f"{API_BASE_URL}/report_error",
json={ json={
"tweet": tweet, "tweet": tweet,
"prediction": sentiment, "prediction": sentiment,

View file

@ -15,6 +15,7 @@ def send_error_report_email(error_reports):
smtp_password = os.getenv("SMTP_PASSWORD") smtp_password = os.getenv("SMTP_PASSWORD")
smtp_from_alias = os.getenv("SMTP_FROM_ALIAS", smtp_email) # mail sender if not none (alias) smtp_from_alias = os.getenv("SMTP_FROM_ALIAS", smtp_email) # mail sender if not none (alias)
admin_email = os.getenv("ADMIN_EMAIL") # this one will receive the mail admin_email = os.getenv("ADMIN_EMAIL") # this one will receive the mail
if not all([smtp_server, smtp_email, smtp_password, admin_email]): if not all([smtp_server, smtp_email, smtp_password, admin_email]):
print("❌ Configuration email manquante dans .env") print("❌ Configuration email manquante dans .env")

View file

@ -98,6 +98,8 @@ def load_model_and_artifacts(model_name=MODEL_NAME, stage=STAGE):
# 🚀 Lancer l'API # 🚀 Lancer l'API
app = FastAPI(title="API Prédiction Sentiment - Air Paradis") app = FastAPI(title="API Prédiction Sentiment - Air Paradis")
# app = FastAPI(title="API Prédiction Sentiment - Air Paradis",
# root_path="/api")
try: try:
model, tokenizer, embedding_type = load_model_and_artifacts() model, tokenizer, embedding_type = load_model_and_artifacts()
@ -139,37 +141,6 @@ def predict_sentiment(request: TweetRequest):
"probability": float(prediction_prob) "probability": float(prediction_prob)
} }
# @app.post("/report_error")
# def report_error(request: ReportRequest):
# tweet = request.tweet.strip()
# prediction = request.prediction.strip().lower()
# probability = request.probability
# if not tweet or not prediction:
# raise HTTPException(status_code=400, detail="Tweet ou prédiction manquants.")
# if probability < 0 or probability > 1:
# raise HTTPException(status_code=400, detail="Probabilité invalide. Elle doit être entre 0 et 1.")
# # Ajouter ou mettre à jour le signalement du tweet avec la probabilité
# error_reports[tweet] = prediction + ' (p = ' + str(probability) + ') '
# # ✅ Envoi de l'email sans attendre un multiple de 3
# try:
# email_success = send_error_report_email(error_reports.copy())
# if email_success:
# print(f"✅ Email envoyé avec succès pour {len(error_reports)} signalements")
# report_sent = True
# else:
# print(f"❌ Échec de l'envoi de l'email")
# report_sent = False
# except Exception as e:
# print(f"❌ Erreur lors de l'envoi de l'email : {e}")
# report_sent = False
# return {"report_sent": report_sent}
@app.post("/report_error") @app.post("/report_error")
def report_error(request: ReportRequest): def report_error(request: ReportRequest):
tweet = request.tweet.strip() tweet = request.tweet.strip()

34
deployment/api/nginx.conf Normal file
View file

@ -0,0 +1,34 @@
worker_processes 1;
events { worker_connections 1024; }
http {
server {
listen 8080;
# Proxy pour le backend FastAPI
location /api/ {
# Le / à la fin de proxy_pass est important,
# il retire /api/ avant de le transmettre à FastAPI.
proxy_pass http://127.0.0.1:8000/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Proxy pour le frontend Streamlit
location / {
# Le / à la fin est crucial pour que Streamlit fonctionne bien
proxy_pass http://127.0.0.1:8501/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 86400; # Empêche les timeouts sur les connexions longues
}
}
}

View file

@ -1,7 +1,15 @@
#!/bin/bash #!/bin/bash
# Lancer Streamlit en arrière-plan # Lancer Streamlit en arrière-plan avec un baseUrlPath dédié
streamlit run app.py --server.port 8501 --server.address 0.0.0.0 & streamlit run app.py \
--server.port 8501 \
--server.address 127.0.0.1 \
--server.baseUrlPath "/app" \
--server.enableXsrfProtection false \
&
# Lancer Uvicorn en premier plan (Docker "suit" ce processus) # Lancer Uvicorn en arrière-plan
uvicorn main:app --host 0.0.0.0 --port 8000 --loop asyncio uvicorn main:app --host 127.0.0.1 --port 8000 --loop asyncio &
# Lancer nginx au premier plan (Docker suit ce processus)
nginx -g 'daemon off;'