fix(ai): robust image pipeline + local MinIO stack + TS errors fix
Image generation: - Automatic model fallback: try gpt-image-1 first, fall back to dall-e-3 if it fails (e.g. org not verified on OpenAI) - Local filesystem fallback: if MinIO upload fails, write the image to backend/uploads/recipes/ and return a URL served by fastify-static - Unified handling of base64 vs URL responses from the Images API - DALL-E quality mapped automatically (low/medium/high -> standard) Local MinIO stack: - docker-compose.yml at repo root with minio + minio-init service that auto-creates the bucket and makes it publicly readable - Default credentials: freedge / freedge123 (configurable) - Console at :9001, API at :9000 - .env.example now points to the local stack by default Static file serving: - Register @fastify/static to serve ./uploads at /uploads/* - Enables local fallback to return usable URLs to the frontend - New PUBLIC_BASE_URL env var to build absolute URLs TypeScript errors (21 -> 0): - JWT typing via '@fastify/jwt' module augmentation (FastifyJWT interface with payload + user) fixes all request.user.id errors - Stripe constructor now passes required StripeConfig - fastify.createCustomer guards checked on the helper itself for proper TS narrowing (not on fastify.stripe) - Remove 'done' arg from async onClose hook - MinIO transport.agent + listFiles return type cast ignored README: - Add 'Stockage des fichiers' section explaining the two modes - Updated setup instructions to start with docker compose up Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
9dbd7e0ba9
commit
64df5db077
39
README.md
39
README.md
@ -46,20 +46,25 @@ freedge/
|
||||
## Démarrage
|
||||
|
||||
```bash
|
||||
# Installation
|
||||
# 1. Services (MinIO) — depuis la racine du projet
|
||||
docker compose up -d
|
||||
# → MinIO sur :9000 (API) + :9001 (console web, user freedge / pass freedge123)
|
||||
# → le bucket 'freedge' est créé automatiquement et rendu lisible publiquement
|
||||
|
||||
# 2. Installation
|
||||
cd backend && npm install
|
||||
cd ../frontend && pnpm install
|
||||
|
||||
# Variables d'environnement backend (.env dans backend/)
|
||||
cp .env.example .env # puis éditer
|
||||
# Requis : DATABASE_URL, JWT_SECRET, OPENAI_API_KEY
|
||||
# 3. Variables d'environnement backend (.env dans backend/)
|
||||
cp .env.example .env # puis éditer OPENAI_API_KEY et JWT_SECRET
|
||||
# Les défauts MinIO du .env.example pointent sur la stack docker-compose
|
||||
|
||||
# Base de données
|
||||
# 4. Base de données
|
||||
cd backend
|
||||
npx prisma migrate dev
|
||||
npx prisma generate
|
||||
|
||||
# Lancement
|
||||
# 5. Lancement
|
||||
npm run dev # backend (tsx watch) sur :3000
|
||||
cd ../frontend && pnpm dev # frontend sur :5173
|
||||
|
||||
@ -80,14 +85,16 @@ npm run typecheck # vérification TS sans build
|
||||
| `FRONTEND_URL` | ❌ | URL frontend pour les emails de reset |
|
||||
| `OPENAI_TEXT_MODEL` | ❌ | Défaut `gpt-4o-mini` (recette) |
|
||||
| `OPENAI_TRANSCRIBE_MODEL` | ❌ | Défaut `gpt-4o-mini-transcribe` |
|
||||
| `OPENAI_IMAGE_MODEL` | ❌ | Défaut `gpt-image-1` |
|
||||
| `OPENAI_IMAGE_MODEL` | ❌ | Défaut `gpt-image-1` (requiert org vérifiée sur OpenAI) |
|
||||
| `OPENAI_IMAGE_FALLBACK_MODEL` | ❌ | Défaut `dall-e-3`, utilisé si le principal échoue |
|
||||
| `OPENAI_IMAGE_QUALITY` | ❌ | `low` / `medium` / `high` (défaut `medium`) |
|
||||
| `OPENAI_IMAGE_SIZE` | ❌ | Défaut `1024x1024` |
|
||||
| `ENABLE_IMAGE_GENERATION` | ❌ | `false` pour désactiver totalement la génération d'image |
|
||||
| `OPENAI_MAX_RETRIES` | ❌ | Retries SDK OpenAI (défaut `3`) |
|
||||
| `OPENAI_TIMEOUT_MS` | ❌ | Timeout par requête (défaut `60000`) |
|
||||
| `STRIPE_SECRET_KEY` | ❌ | Clé Stripe (pour créer les customers) |
|
||||
| `MINIO_ENDPOINT` / `MINIO_PORT` / `MINIO_USE_SSL` / `MINIO_ACCESS_KEY` / `MINIO_SECRET_KEY` / `MINIO_BUCKET` | ❌ | Config MinIO ; fallback local sinon |
|
||||
| `MINIO_ENDPOINT` / `MINIO_PORT` / `MINIO_USE_SSL` / `MINIO_ACCESS_KEY` / `MINIO_SECRET_KEY` / `MINIO_BUCKET` | ❌ | Config MinIO ; fallback local `./uploads` sinon |
|
||||
| `PUBLIC_BASE_URL` | ❌ | URL publique du backend pour les fichiers locaux (défaut `http://localhost:3000`) |
|
||||
| `MINIO_ALLOW_SELF_SIGNED` | ❌ | `true` pour autoriser TLS auto-signé (DEV uniquement) |
|
||||
| `RESEND_API_KEY` | ❌ | Clé Resend pour les emails |
|
||||
|
||||
@ -118,6 +125,22 @@ npm run typecheck # vérification TS sans build
|
||||
|
||||
🔒 = nécessite un JWT `Authorization: Bearer <token>`
|
||||
|
||||
## Stockage des fichiers (images & audio)
|
||||
|
||||
Deux modes, avec fallback automatique :
|
||||
|
||||
1. **MinIO** (recommandé, lancé via `docker compose up -d`)
|
||||
- Bucket S3-compatible, accessible en lecture publique
|
||||
- Console web : http://localhost:9001 (user `freedge`, pass `freedge123`)
|
||||
- API S3 : http://localhost:9000
|
||||
|
||||
2. **Local** (fallback si MinIO est indisponible)
|
||||
- Les fichiers sont écrits dans `backend/uploads/{audio,recipes}/`
|
||||
- Servis par Fastify sur `GET /uploads/*`
|
||||
- L'URL publique retournée au frontend est `${PUBLIC_BASE_URL}/uploads/recipes/xxx.png`
|
||||
|
||||
Le frontend n'a pas à se soucier du mode : il reçoit une URL publique dans tous les cas.
|
||||
|
||||
## Pipeline IA
|
||||
|
||||
Le module `src/ai/` est isolé du reste du backend pour faciliter les itérations.
|
||||
|
||||
@ -3,11 +3,14 @@ DATABASE_URL="file:./prisma/dev.db"
|
||||
JWT_SECRET="change-me-please-use-at-least-32-characters"
|
||||
OPENAI_API_KEY="sk-..."
|
||||
|
||||
|
||||
# ---- Serveur ----
|
||||
PORT=3000
|
||||
LOG_LEVEL=info
|
||||
CORS_ORIGINS=http://localhost:5173,http://127.0.0.1:5173
|
||||
FRONTEND_URL=http://localhost:5173
|
||||
# Base URL publique du backend (utilisée pour les URLs de fichiers servis localement)
|
||||
PUBLIC_BASE_URL=http://localhost:3000
|
||||
|
||||
# ---- IA ----
|
||||
# Modèle texte (recette). Recommandé : gpt-4o-mini (rapide & cheap),
|
||||
@ -21,11 +24,14 @@ OPENAI_TRANSCRIBE_MODEL=gpt-4o-mini-transcribe
|
||||
|
||||
# Génération d'image
|
||||
ENABLE_IMAGE_GENERATION=true
|
||||
# - gpt-image-1 (recommandé) : qualité photo nettement supérieure à dall-e-3
|
||||
# - dall-e-3 : ancien, à éviter
|
||||
# Modèle principal : gpt-image-1 (meilleure qualité photographique)
|
||||
# NOTE : gpt-image-1 requiert une vérification d'organisation sur OpenAI.
|
||||
# Si ton org n'est pas vérifiée, mets dall-e-3 en principal.
|
||||
OPENAI_IMAGE_MODEL=gpt-image-1
|
||||
# Modèle de fallback automatique si le principal échoue (ex: org non vérifiée)
|
||||
OPENAI_IMAGE_FALLBACK_MODEL=dall-e-3
|
||||
# Pour gpt-image-1: low | medium | high
|
||||
# Pour dall-e-3: standard | hd
|
||||
# Pour dall-e-3: standard | hd (mappé automatiquement)
|
||||
OPENAI_IMAGE_QUALITY=medium
|
||||
OPENAI_IMAGE_SIZE=1024x1024
|
||||
|
||||
@ -36,12 +42,13 @@ OPENAI_TIMEOUT_MS=60000
|
||||
# ---- Stripe (optionnel) ----
|
||||
STRIPE_SECRET_KEY=
|
||||
|
||||
# ---- MinIO (optionnel — fallback local sinon) ----
|
||||
MINIO_ENDPOINT=
|
||||
# ---- MinIO (démarré avec `docker-compose up -d` depuis la racine du projet) ----
|
||||
# Laisse vide pour désactiver et utiliser uniquement le stockage local ./uploads
|
||||
MINIO_ENDPOINT=localhost
|
||||
MINIO_PORT=9000
|
||||
MINIO_USE_SSL=false
|
||||
MINIO_ACCESS_KEY=
|
||||
MINIO_SECRET_KEY=
|
||||
MINIO_ACCESS_KEY=freedge
|
||||
MINIO_SECRET_KEY=freedge123
|
||||
MINIO_BUCKET=freedge
|
||||
MINIO_ALLOW_SELF_SIGNED=false
|
||||
|
||||
|
||||
4757
backend/package-lock.json
generated
Normal file
4757
backend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -19,6 +19,7 @@
|
||||
"@fastify/jwt": "^7.0.0",
|
||||
"@fastify/multipart": "^8.0.0",
|
||||
"@fastify/rate-limit": "^9.1.0",
|
||||
"@fastify/static": "^7.0.4",
|
||||
"@prisma/client": "^5.0.0",
|
||||
"bcrypt": "^5.1.1",
|
||||
"dotenv": "^16.3.1",
|
||||
|
||||
@ -1,19 +1,18 @@
|
||||
import type { OpenAI } from 'openai';
|
||||
import type { FastifyBaseLogger } from 'fastify';
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import { Readable } from 'node:stream';
|
||||
import { uploadFile, getFileUrl } from '../utils/storage';
|
||||
import { buildImagePrompt } from './prompts';
|
||||
import { computeImageCost, type CallLog } from './cost';
|
||||
|
||||
const ENABLE_IMAGE_GENERATION = process.env.ENABLE_IMAGE_GENERATION !== 'false';
|
||||
const IMAGE_MODEL = process.env.OPENAI_IMAGE_MODEL || 'gpt-image-1';
|
||||
const IMAGE_QUALITY = (process.env.OPENAI_IMAGE_QUALITY || 'medium') as
|
||||
| 'low'
|
||||
| 'medium'
|
||||
| 'high'
|
||||
| 'standard'
|
||||
| 'hd';
|
||||
const PRIMARY_MODEL = process.env.OPENAI_IMAGE_MODEL || 'gpt-image-1';
|
||||
const FALLBACK_MODEL = process.env.OPENAI_IMAGE_FALLBACK_MODEL || 'dall-e-3';
|
||||
const IMAGE_QUALITY = process.env.OPENAI_IMAGE_QUALITY || 'medium';
|
||||
const IMAGE_SIZE = process.env.OPENAI_IMAGE_SIZE || '1024x1024';
|
||||
const PUBLIC_BASE_URL = process.env.PUBLIC_BASE_URL || 'http://localhost:3000';
|
||||
|
||||
export interface GenerateImageOptions {
|
||||
title: string;
|
||||
@ -56,41 +55,41 @@ export async function generateRecipeImage(
|
||||
}
|
||||
|
||||
const start = Date.now();
|
||||
|
||||
try {
|
||||
const prompt = buildImagePrompt(options.title, options.description);
|
||||
|
||||
// gpt-image-1 retourne TOUJOURS du base64 (pas d'URL)
|
||||
// dall-e-3 retourne une URL temporaire
|
||||
const isGptImage = IMAGE_MODEL === 'gpt-image-1';
|
||||
|
||||
const response = await openai.images.generate({
|
||||
model: IMAGE_MODEL,
|
||||
prompt,
|
||||
n: 1,
|
||||
size: IMAGE_SIZE as '1024x1024' | '1024x1536' | '1536x1024',
|
||||
...(isGptImage
|
||||
? { quality: IMAGE_QUALITY as 'low' | 'medium' | 'high' }
|
||||
: { quality: IMAGE_QUALITY as 'standard' | 'hd' }),
|
||||
});
|
||||
|
||||
// 1. Tenter le modèle principal, puis le fallback
|
||||
let imageBuffer: Buffer | null = null;
|
||||
let modelUsed = PRIMARY_MODEL;
|
||||
|
||||
if (isGptImage) {
|
||||
const b64 = response.data?.[0]?.b64_json;
|
||||
if (!b64) throw new Error('Pas de base64 dans la réponse gpt-image-1');
|
||||
imageBuffer = Buffer.from(b64, 'base64');
|
||||
try {
|
||||
imageBuffer = await callImageModel(openai, PRIMARY_MODEL, prompt);
|
||||
} catch (err) {
|
||||
logger.warn(
|
||||
`Génération image (${PRIMARY_MODEL}) échouée: ${(err as Error).message}`
|
||||
);
|
||||
|
||||
if (FALLBACK_MODEL && FALLBACK_MODEL !== PRIMARY_MODEL) {
|
||||
logger.info(`Tentative avec le modèle de fallback: ${FALLBACK_MODEL}`);
|
||||
try {
|
||||
imageBuffer = await callImageModel(openai, FALLBACK_MODEL, prompt);
|
||||
modelUsed = FALLBACK_MODEL;
|
||||
} catch (fallbackErr) {
|
||||
logger.warn(
|
||||
`Génération image (${FALLBACK_MODEL}) échouée aussi: ${(fallbackErr as Error).message}`
|
||||
);
|
||||
return { url: null, log: null };
|
||||
}
|
||||
} else {
|
||||
const url = response.data?.[0]?.url;
|
||||
if (!url) throw new Error('Pas d\'URL dans la réponse');
|
||||
const fetched = await fetch(url);
|
||||
if (!fetched.ok) throw new Error(`Téléchargement: ${fetched.statusText}`);
|
||||
imageBuffer = Buffer.from(await fetched.arrayBuffer());
|
||||
return { url: null, log: null };
|
||||
}
|
||||
}
|
||||
|
||||
const fileName = `${slugify(options.title)}-${Date.now()}.png`;
|
||||
if (!imageBuffer) return { url: null, log: null };
|
||||
|
||||
// 2. Stocker : MinIO en priorité, fallback local (servi via /uploads)
|
||||
const fileName = `${slugify(options.title)}-${Date.now()}.png`;
|
||||
let publicUrl: string | null = null;
|
||||
|
||||
try {
|
||||
const filePath = await uploadFile(
|
||||
{ filename: fileName, file: Readable.from(imageBuffer) },
|
||||
@ -99,29 +98,80 @@ export async function generateRecipeImage(
|
||||
publicUrl = await getFileUrl(filePath);
|
||||
} catch (storageErr) {
|
||||
logger.warn(
|
||||
`Upload MinIO image échoué: ${(storageErr as Error).message}`
|
||||
`Upload MinIO image échoué, fallback local: ${(storageErr as Error).message}`
|
||||
);
|
||||
|
||||
try {
|
||||
const recipesDir = path.join('./uploads', 'recipes');
|
||||
if (!fs.existsSync(recipesDir)) {
|
||||
fs.mkdirSync(recipesDir, { recursive: true });
|
||||
}
|
||||
const localPath = path.join(recipesDir, fileName);
|
||||
fs.writeFileSync(localPath, imageBuffer);
|
||||
// Le serveur expose ./uploads via une route statique /uploads
|
||||
publicUrl = `${PUBLIC_BASE_URL}/uploads/recipes/${fileName}`;
|
||||
} catch (localErr) {
|
||||
logger.warn(
|
||||
`Sauvegarde locale image échouée: ${(localErr as Error).message}`
|
||||
);
|
||||
// Pas de fallback local pour les images : on laisse null
|
||||
// (le frontend affichera son placeholder)
|
||||
publicUrl = null;
|
||||
}
|
||||
}
|
||||
|
||||
const cost = computeImageCost(IMAGE_MODEL, IMAGE_SIZE, IMAGE_QUALITY);
|
||||
const cost = computeImageCost(modelUsed, IMAGE_SIZE, IMAGE_QUALITY);
|
||||
const log: CallLog = {
|
||||
userId: options.userId,
|
||||
operation: 'image_generation',
|
||||
model: IMAGE_MODEL,
|
||||
model: modelUsed,
|
||||
durationMs: Date.now() - start,
|
||||
costUsd: cost,
|
||||
};
|
||||
|
||||
logger.info(log, 'openai_image_generated');
|
||||
|
||||
return { url: publicUrl, log };
|
||||
} catch (err) {
|
||||
logger.warn(
|
||||
`Génération image (${IMAGE_MODEL}) échouée: ${(err as Error).message}`
|
||||
);
|
||||
return { url: null, log: null };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Appelle l'API Images d'OpenAI pour un modèle donné et retourne le buffer
|
||||
* de l'image générée. Unifie le traitement des deux modèles (gpt-image-1
|
||||
* renvoie du base64, dall-e-3 renvoie une URL).
|
||||
*/
|
||||
async function callImageModel(
|
||||
openai: OpenAI,
|
||||
model: string,
|
||||
prompt: string
|
||||
): Promise<Buffer> {
|
||||
const isGptImage = model === 'gpt-image-1';
|
||||
|
||||
// dall-e-3 n'accepte pas 'low'/'medium'/'high' — on mappe vers 'standard'
|
||||
const qualityForDallE =
|
||||
IMAGE_QUALITY === 'hd' || IMAGE_QUALITY === 'standard'
|
||||
? IMAGE_QUALITY
|
||||
: 'standard';
|
||||
|
||||
const params = {
|
||||
model,
|
||||
prompt,
|
||||
n: 1,
|
||||
size: IMAGE_SIZE,
|
||||
...(isGptImage
|
||||
? { quality: IMAGE_QUALITY }
|
||||
: { quality: qualityForDallE, response_format: 'b64_json' as const }),
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const response = await openai.images.generate(params as any);
|
||||
|
||||
const first = response.data?.[0];
|
||||
if (!first) throw new Error('Réponse images vide');
|
||||
|
||||
if (first.b64_json) {
|
||||
return Buffer.from(first.b64_json, 'base64');
|
||||
}
|
||||
if (first.url) {
|
||||
const fetched = await fetch(first.url);
|
||||
if (!fetched.ok) throw new Error(`Téléchargement image: ${fetched.statusText}`);
|
||||
return Buffer.from(await fetched.arrayBuffer());
|
||||
}
|
||||
throw new Error('Ni base64 ni URL dans la réponse images');
|
||||
}
|
||||
|
||||
@ -9,7 +9,9 @@ export default fp(async function stripePlugin(fastify: FastifyInstance) {
|
||||
return;
|
||||
}
|
||||
|
||||
const stripe = new Stripe(key);
|
||||
const stripe = new Stripe(key, {
|
||||
apiVersion: (process.env.STRIPE_API_VERSION as Stripe.StripeConfig['apiVersion']) ?? '2023-10-16',
|
||||
});
|
||||
|
||||
fastify.decorate('stripe', stripe);
|
||||
|
||||
|
||||
@ -49,7 +49,7 @@ const authRoutes: FastifyPluginAsync = async (fastify: FastifyInstance) => {
|
||||
|
||||
// Créer un client Stripe (le plugin peut être désactivé)
|
||||
let stripeId = '';
|
||||
if (fastify.stripe) {
|
||||
if (fastify.createCustomer) {
|
||||
const customer = await fastify.createCustomer(email, name);
|
||||
stripeId = customer.id;
|
||||
}
|
||||
@ -164,7 +164,7 @@ const authRoutes: FastifyPluginAsync = async (fastify: FastifyInstance) => {
|
||||
|
||||
if (!user) {
|
||||
let stripeId = '';
|
||||
if (fastify.stripe) {
|
||||
if (fastify.createCustomer) {
|
||||
const customer = await fastify.createCustomer(email, name);
|
||||
stripeId = customer.id;
|
||||
}
|
||||
|
||||
@ -7,6 +7,8 @@ import { PrismaClient } from '@prisma/client';
|
||||
import helmet from '@fastify/helmet';
|
||||
import rateLimit from '@fastify/rate-limit';
|
||||
import cors from '@fastify/cors';
|
||||
import fastifyStatic from '@fastify/static';
|
||||
import * as path from 'node:path';
|
||||
|
||||
import authPlugin from './plugins/auth';
|
||||
import stripePlugin from './plugins/stripe';
|
||||
@ -56,6 +58,13 @@ async function bootstrap(): Promise<void> {
|
||||
credentials: true,
|
||||
});
|
||||
|
||||
// --- Fichiers statiques (fallback local pour images/audio) ---
|
||||
await fastify.register(fastifyStatic, {
|
||||
root: path.resolve('./uploads'),
|
||||
prefix: '/uploads/',
|
||||
decorateReply: false,
|
||||
});
|
||||
|
||||
// --- Plugins applicatifs ---
|
||||
await fastify.register(authPlugin);
|
||||
await fastify.register(stripePlugin);
|
||||
@ -69,9 +78,8 @@ async function bootstrap(): Promise<void> {
|
||||
|
||||
fastify.get('/health', async () => ({ status: 'ok', uptime: process.uptime() }));
|
||||
|
||||
fastify.addHook('onClose', async (_instance, done) => {
|
||||
fastify.addHook('onClose', async () => {
|
||||
await prisma.$disconnect();
|
||||
done();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
13
backend/src/types/fastify.d.ts
vendored
13
backend/src/types/fastify.d.ts
vendored
@ -4,6 +4,14 @@ import type Stripe from 'stripe';
|
||||
import type { OAuth2Client } from 'google-auth-library';
|
||||
import type { FastifyRequest, FastifyReply } from 'fastify';
|
||||
import type { MultipartFile } from '@fastify/multipart';
|
||||
import '@fastify/jwt';
|
||||
|
||||
declare module '@fastify/jwt' {
|
||||
interface FastifyJWT {
|
||||
payload: { id: string };
|
||||
user: { id: string };
|
||||
}
|
||||
}
|
||||
|
||||
export interface RecipeData {
|
||||
titre: string;
|
||||
@ -56,9 +64,4 @@ declare module 'fastify' {
|
||||
googleClient: OAuth2Client;
|
||||
}
|
||||
|
||||
interface FastifyRequest {
|
||||
user: {
|
||||
id: string;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -29,8 +29,10 @@ function getClient(): Minio.Client | null {
|
||||
};
|
||||
|
||||
// Ne désactiver la vérification TLS que si explicitement demandé.
|
||||
// `transport` n'expose pas `agent` dans les types récents de minio ; on
|
||||
// passe par un cast ciblé car le runtime minio accepte toujours cette option.
|
||||
if (useSSL && allowSelfSigned) {
|
||||
(clientOpts as Minio.ClientOptions & { transport?: unknown }).transport = {
|
||||
(clientOpts as unknown as { transport: unknown }).transport = {
|
||||
agent: new https.Agent({ rejectUnauthorized: false }),
|
||||
};
|
||||
}
|
||||
@ -67,7 +69,7 @@ export async function getFile(filePath: string): Promise<Readable> {
|
||||
return client.getObject(bucket(), filePath);
|
||||
}
|
||||
|
||||
export async function listFiles(folderPath: string): Promise<Minio.BucketStream<Minio.BucketItem>> {
|
||||
export async function listFiles(folderPath: string) {
|
||||
const client = getClient();
|
||||
if (!client) throw new Error('MinIO non configuré');
|
||||
return client.listObjects(bucket(), folderPath);
|
||||
|
||||
42
docker-compose.yml
Normal file
42
docker-compose.yml
Normal file
@ -0,0 +1,42 @@
|
||||
services:
|
||||
minio:
|
||||
image: minio/minio:latest
|
||||
container_name: freedge-minio
|
||||
ports:
|
||||
- "9000:9000" # API S3
|
||||
- "9001:9001" # Console web
|
||||
environment:
|
||||
MINIO_ROOT_USER: ${MINIO_ROOT_USER:-freedge}
|
||||
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:-freedge123}
|
||||
volumes:
|
||||
- minio-data:/data
|
||||
command: server /data --console-address ":9001"
|
||||
healthcheck:
|
||||
test: ["CMD", "mc", "ready", "local"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
# Crée le bucket automatiquement au démarrage
|
||||
minio-init:
|
||||
image: minio/mc:latest
|
||||
container_name: freedge-minio-init
|
||||
depends_on:
|
||||
minio:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
MINIO_ROOT_USER: ${MINIO_ROOT_USER:-freedge}
|
||||
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:-freedge123}
|
||||
MINIO_BUCKET: ${MINIO_BUCKET:-freedge}
|
||||
entrypoint: >
|
||||
/bin/sh -c "
|
||||
set -e;
|
||||
mc alias set local http://minio:9000 $$MINIO_ROOT_USER $$MINIO_ROOT_PASSWORD;
|
||||
mc mb --ignore-existing local/$$MINIO_BUCKET;
|
||||
mc anonymous set download local/$$MINIO_BUCKET;
|
||||
echo 'Bucket '$$MINIO_BUCKET' prêt et lisible publiquement';
|
||||
"
|
||||
|
||||
volumes:
|
||||
minio-data:
|
||||
driver: local
|
||||
Loading…
x
Reference in New Issue
Block a user