From 62c32ad0ad4420d3ab20d1ca0acb984d0449c20c Mon Sep 17 00:00:00 2001 From: amania-jailbreak Date: Wed, 5 Aug 2026 10:31:03 +0900 Subject: [PATCH] =?UTF-8?q?MinIO=E8=A8=AD=E5=AE=9A=E3=82=92Coolify?= =?UTF-8?q?=E5=AF=BE=E5=BF=9C=E3=81=AB=E6=95=B4=E5=82=99=E3=81=97=E3=80=81?= =?UTF-8?q?bucket=E8=87=AA=E5=8B=95=E4=BD=9C=E6=88=90=E3=82=92=E8=BF=BD?= =?UTF-8?q?=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit compose: - MinIOにヘルスチェック(mc ready)を追加し、api/workerはhealthyを待つように変更 - 全認証情報を${VAR:-default}でパラメータ化し、Coolifyの環境変数/シークレットで上書き可能に - MINIO_SERVER_URL / MINIO_BROWSER_REDIRECT_URL でリバースプロキシ背後の公開URLに対応 - workerの依存先をapiからpostgres+minioに修正し、起動順序のデッドロックを解消 API: - S3StorageにensureBucket()を追加(HeadBucket→なければCreateBucket、冪等) - runtime起動時にbucketを自動作成し、minio-init依存を必須ではなくした .env.exampleにMinIO関連変数を追記 --- .env.example | 12 ++++-- apps/api/src/runtime.ts | 9 +++- apps/api/src/storage.ts | 23 ++++++++++ docker-compose.yml | 96 ++++++++++++++++++++++++++++------------- 4 files changed, 105 insertions(+), 35 deletions(-) diff --git a/.env.example b/.env.example index 372846a..ae2d283 100644 --- a/.env.example +++ b/.env.example @@ -2,18 +2,24 @@ NODE_ENV=development PORT=4000 PUBLIC_BASE_URL=http://localhost:4000 WEB_ORIGIN=http://localhost:3000 -NEXT_PUBLIC_API_BASE_URL=http://localhost:4000 NEXT_PUBLIC_AUTH_MODE=demo -# Use PostgreSQL + MinIO in Docker Compose. Leave DATABASE_URL empty for memory mode. +# --- Storage (MinIO via docker compose) ------------------------------------- +# Leave DATABASE_URL empty to run with an in-memory store instead of Postgres. DATABASE_URL=postgres://altdock:altdock@localhost:5432/altdock STORAGE_MODE=s3 S3_ENDPOINT=http://localhost:9000 S3_REGION=us-east-1 S3_BUCKET=altdock S3_ACCESS_KEY_ID=minioadmin -S3_SECRET_ACCESS_KEY=minioadmin +S3_SECRET_ACCESS_KEY=minioadmin123 S3_FORCE_PATH_STYLE=true +# MinIO root credentials, consumed by docker-compose.yml. CHANGE in production. +MINIO_ROOT_USER=minioadmin +MINIO_ROOT_PASSWORD=minioadmin123 +# Only needed when MinIO is behind a reverse proxy / domain (e.g. on Coolify): +MINIO_SERVER_URL= +MINIO_BROWSER_REDIRECT_URL= # demo works without an identity provider; production should use generic OIDC. AUTH_MODE=demo diff --git a/apps/api/src/runtime.ts b/apps/api/src/runtime.ts index b53960d..2900585 100644 --- a/apps/api/src/runtime.ts +++ b/apps/api/src/runtime.ts @@ -5,5 +5,12 @@ import { createStorage } from "./storage"; export async function createRuntime(appConfig: AppConfig) { const { store, pool } = await createStore(appConfig); await store.init(); - return { store, storage: createStorage(appConfig), pool }; + const storage = createStorage(appConfig); + // When using S3-compatible storage (e.g. MinIO), make sure the target bucket + // exists before serving traffic. This keeps the service self-contained and + // removes the need for a separate init container. + if (storage.ensureBucket) { + await storage.ensureBucket(); + } + return { store, storage, pool }; } diff --git a/apps/api/src/storage.ts b/apps/api/src/storage.ts index 4726009..331e3ab 100644 --- a/apps/api/src/storage.ts +++ b/apps/api/src/storage.ts @@ -5,9 +5,11 @@ import { dirname, normalize, relative, resolve } from "node:path"; import { pipeline } from "node:stream/promises"; import { Readable } from "node:stream"; import { + CreateBucketCommand, CompleteMultipartUploadCommand, CreateMultipartUploadCommand, GetObjectCommand, + HeadBucketCommand, HeadObjectCommand, PutObjectCommand, S3Client, @@ -35,6 +37,7 @@ export interface StorageAdapter { downloadToFile(key: string, filePath: string): Promise; getObject(key: string): Promise; headObject(key: string): Promise<{ sizeBytes: number; contentType: string } | null>; + ensureBucket?(): Promise; } function contentTypeForPath(path: string) { @@ -126,6 +129,26 @@ export class S3Storage implements StorageAdapter { }); } + // Idempotently create the configured bucket so the service is self-contained + // and does not depend on an external init step (e.g. a separate minio-init + // container) before it can store objects. Safe to call on every startup. + async ensureBucket() { + const bucket = this.appConfig.S3_BUCKET; + try { + await this.client.send(new HeadBucketCommand({ Bucket: bucket })); + return; + } catch (error: any) { + const notFound = error?.$metadata?.httpStatusCode === 404 || error?.name === "NotFound" || error?.name === "NoSuchBucket"; + if (!notFound) { + // Re-throw unexpected errors (auth, network) so they surface loudly. + throw error; + } + } + // us-east-1 / "auto" regions must NOT send a LocationConstraint. + await this.client.send(new CreateBucketCommand({ Bucket: bucket })); + } + + async createUploadPlan(key: string, contentType: string, sizeBytes: number) { const partSizeBytes = 16 * 1024 * 1024; const partCount = Math.ceil(sizeBytes / partSizeBytes); diff --git a/docker-compose.yml b/docker-compose.yml index 8e5b860..3f9a082 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,16 +1,30 @@ +# AltDock full stack. Works locally with `docker compose up` and on Coolify, +# where you override the `${VAR:-default}` values from the project's +# Environment Variables / secrets panel. +# +# Coolify notes: +# - All credentials are parameterized; set them in Coolify's env to avoid +# the insecure local defaults. +# - MinIO has a real healthcheck, so api/worker wait until it can serve. +# - The API also creates the S3 bucket itself on startup (ensureBucket), +# so the stack no longer depends on minio-init completing successfully. +# - If you expose MinIO through a domain, set MINIO_SERVER_URL (public S3 API +# origin) and MINIO_BROWSER_REDIRECT_URL (public console origin) so +# presigned upload URLs and console redirects work from the browser. + services: postgres: image: postgres:16-alpine environment: - POSTGRES_DB: altdock - POSTGRES_USER: altdock - POSTGRES_PASSWORD: altdock + POSTGRES_DB: ${POSTGRES_DB:-altdock} + POSTGRES_USER: ${POSTGRES_USER:-altdock} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-altdock} ports: - "5432:5432" volumes: - postgres-data:/var/lib/postgresql/data healthcheck: - test: ["CMD-SHELL", "pg_isready -U altdock -d altdock"] + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-altdock} -d ${POSTGRES_DB:-altdock}"] interval: 5s timeout: 5s retries: 10 @@ -19,22 +33,37 @@ services: image: minio/minio:latest command: server /data --console-address ":9001" environment: - MINIO_ROOT_USER: minioadmin - MINIO_ROOT_PASSWORD: minioadmin + MINIO_ROOT_USER: ${MINIO_ROOT_USER:-minioadmin} + MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:-minioadmin123} + # Public origins, only needed when MinIO is behind a reverse proxy/domain. + MINIO_SERVER_URL: ${MINIO_SERVER_URL:-} + MINIO_BROWSER_REDIRECT_URL: ${MINIO_BROWSER_REDIRECT_URL:-} ports: - "9000:9000" - "9001:9001" volumes: - minio-data:/data + healthcheck: + test: ["CMD", "mc", "ready", "local"] + interval: 5s + timeout: 5s + retries: 12 + start_period: 10s + # Optional belt-and-suspenders: pre-creates the bucket. The API does the + # same on startup, so this can be removed without affecting functionality. minio-init: image: minio/mc:latest depends_on: - - minio + minio: + condition: service_healthy entrypoint: ["/bin/sh", "-c"] command: >- - "until mc alias set local http://minio:9000 minioadmin minioadmin; do sleep 1; done; - mc mb --ignore-existing local/altdock" + "until mc alias set local http://minio:9000 ${MINIO_ROOT_USER:-minioadmin} ${MINIO_ROOT_PASSWORD:-minioadmin123}; do sleep 1; done; + mc mb --ignore-existing local/${S3_BUCKET:-altdock}" + environment: + MINIO_ROOT_USER: ${MINIO_ROOT_USER:-minioadmin} + MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:-minioadmin123} api: image: node:22-bookworm @@ -43,17 +72,19 @@ services: environment: NODE_ENV: development PORT: 4000 - PUBLIC_BASE_URL: http://localhost:4000 - WEB_ORIGIN: http://localhost:3000 - DATABASE_URL: postgres://altdock:altdock@postgres:5432/altdock + PUBLIC_BASE_URL: ${PUBLIC_BASE_URL:-http://localhost:4000} + WEB_ORIGIN: ${WEB_ORIGIN:-http://localhost:3000} + DATABASE_URL: postgres://${POSTGRES_USER:-altdock}:${POSTGRES_PASSWORD:-altdock}@postgres:5432/${POSTGRES_DB:-altdock} STORAGE_MODE: s3 - S3_ENDPOINT: http://minio:9000 - S3_REGION: us-east-1 - S3_BUCKET: altdock - S3_ACCESS_KEY_ID: minioadmin - S3_SECRET_ACCESS_KEY: minioadmin + # S3_ENDPOINT is the internal address; presigned URLs resolve via the + # public MINIO_SERVER_URL when set. + S3_ENDPOINT: ${S3_ENDPOINT:-http://minio:9000} + S3_REGION: ${S3_REGION:-us-east-1} + S3_BUCKET: ${S3_BUCKET:-altdock} + S3_ACCESS_KEY_ID: ${MINIO_ROOT_USER:-minioadmin} + S3_SECRET_ACCESS_KEY: ${MINIO_ROOT_PASSWORD:-minioadmin123} S3_FORCE_PATH_STYLE: "true" - AUTH_MODE: demo + AUTH_MODE: ${AUTH_MODE:-demo} PROCESS_INLINE: "false" ports: - "4000:4000" @@ -62,8 +93,8 @@ services: depends_on: postgres: condition: service_healthy - minio-init: - condition: service_completed_successfully + minio: + condition: service_healthy worker: image: node:22-bookworm @@ -71,34 +102,37 @@ services: command: sh -c "npm install --no-audit --no-fund && npm run dev:worker" environment: NODE_ENV: development - DATABASE_URL: postgres://altdock:altdock@postgres:5432/altdock + DATABASE_URL: postgres://${POSTGRES_USER:-altdock}:${POSTGRES_PASSWORD:-altdock}@postgres:5432/${POSTGRES_DB:-altdock} STORAGE_MODE: s3 - S3_ENDPOINT: http://minio:9000 - S3_REGION: us-east-1 - S3_BUCKET: altdock - S3_ACCESS_KEY_ID: minioadmin - S3_SECRET_ACCESS_KEY: minioadmin + S3_ENDPOINT: ${S3_ENDPOINT:-http://minio:9000} + S3_REGION: ${S3_REGION:-us-east-1} + S3_BUCKET: ${S3_BUCKET:-altdock} + S3_ACCESS_KEY_ID: ${MINIO_ROOT_USER:-minioadmin} + S3_SECRET_ACCESS_KEY: ${MINIO_ROOT_PASSWORD:-minioadmin123} S3_FORCE_PATH_STYLE: "true" - AUTH_MODE: demo + AUTH_MODE: ${AUTH_MODE:-demo} PROCESS_INLINE: "false" volumes: - .:/workspace depends_on: - api: - condition: service_started + postgres: + condition: service_healthy + minio: + condition: service_healthy web: image: node:22-bookworm working_dir: /workspace command: sh -c "npm install --no-audit --no-fund && npm run dev -- --host 0.0.0.0" environment: - NEXT_PUBLIC_API_BASE_URL: http://localhost:4000 + NEXT_PUBLIC_API_BASE_URL: ${NEXT_PUBLIC_API_BASE_URL:-http://localhost:4000} ports: - "3000:3000" volumes: - .:/workspace depends_on: - - api + api: + condition: service_started volumes: postgres-data: