docker: 前端 nginx HTTP :2000(外部反代接管域名/TLS,不再内置证书);README + logo
- docker-compose.yml:去 acme,web 端口 2000(WEB_PORT),CORS_ORIGINS/COOKIE_SECURE 直配,清理证书 volumes - frontend nginx 模板/Dockerfile:纯 HTTP :2000,删自签证书 + cert-watch 入口脚本 - 删除 install.sh、.env.docker.example(compose 直接 up,不再需要一键脚本/模板) - README(中英):部署改为 Docker(2000端口反代)+ 源码两法;去 acme/自动 HTTPS 表述;结构/徽章同步;顶部加 Vivid logo(favicon.svg);视频 720p/1080p 分辨率对照表(DocsView) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,28 +0,0 @@
|
|||||||
# Copy to .env (next to docker-compose.yml), edit, then:
|
|
||||||
# docker compose up -d --build
|
|
||||||
|
|
||||||
# Domain nginx binds (server_name) and acme.sh issues the cert for. Must resolve
|
|
||||||
# to this host with ports 80 + 443 reachable for the Let's Encrypt http-01 check.
|
|
||||||
# Use "localhost" for local testing — then ACME is skipped and a self-signed cert
|
|
||||||
# is used (443 still works, browser will warn).
|
|
||||||
DOMAIN=vividai.run
|
|
||||||
|
|
||||||
# Email for the Let's Encrypt / ACME account (renewal notices).
|
|
||||||
ACME_EMAIL=admin@example.com
|
|
||||||
|
|
||||||
# Host ports nginx listens on (80 = ACME challenge + redirect, 443 = TLS).
|
|
||||||
HTTP_PORT=80
|
|
||||||
HTTPS_PORT=443
|
|
||||||
|
|
||||||
# Session cookie Secure flag — keep true when serving over HTTPS (the default).
|
|
||||||
COOKIE_SECURE=true
|
|
||||||
|
|
||||||
APP_TITLE=image2api
|
|
||||||
|
|
||||||
# --- Postgres ---
|
|
||||||
POSTGRES_PASSWORD=change-me-postgres
|
|
||||||
|
|
||||||
# --- S3 / object storage (RustFS) ---
|
|
||||||
S3_BUCKET=vivid-ai
|
|
||||||
S3_ACCESS_KEY=vividai
|
|
||||||
S3_SECRET_KEY=change-me-s3-secret
|
|
||||||
+11
-27
@@ -1,5 +1,7 @@
|
|||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
|
<img src="frontend/public/favicon.svg" width="84" alt="Vivid AI" />
|
||||||
|
|
||||||
<h1>image2api</h1>
|
<h1>image2api</h1>
|
||||||
|
|
||||||
**Multi-provider AI image / video generation gateway — one OpenAI-compatible API, seven platforms aggregated, a ready-to-run operations system**
|
**Multi-provider AI image / video generation gateway — one OpenAI-compatible API, seven platforms aggregated, a ready-to-run operations system**
|
||||||
@@ -14,7 +16,7 @@
|
|||||||
[](https://vuejs.org)
|
[](https://vuejs.org)
|
||||||
[](#-deployment)
|
[](#-deployment)
|
||||||
[](#-openai-compatible-api)
|
[](#-openai-compatible-api)
|
||||||
[](#option-1-docker-one-command-recommended)
|
[](#-deployment)
|
||||||
[](#-supported-models--providers)
|
[](#-supported-models--providers)
|
||||||
[](#-deployment)
|
[](#-deployment)
|
||||||
[](#-license)
|
[](#-license)
|
||||||
@@ -51,7 +53,7 @@ It's more than an API proxy: it ships with **credit billing, CDK top-ups, referr
|
|||||||
|
|
||||||
> 💡 Both frontend and backend are **fully open-source** (MIT) — Go + Vue 3, free to fork and self-host.
|
> 💡 Both frontend and backend are **fully open-source** (MIT) — Go + Vue 3, free to fork and self-host.
|
||||||
|
|
||||||
**At a glance** 🔌 OpenAI-compatible · 🤖 7 platforms, 10+ models · 🔁 auto failover / token keep-alive · 💳 credits + agent pricing · 🎨 generation frontend + admin console · 🐳 one-command deploy + auto HTTPS
|
**At a glance** 🔌 OpenAI-compatible · 🤖 7 platforms, 10+ models · 🔁 auto failover / token keep-alive · 💳 credits + agent pricing · 🎨 generation frontend + admin console · 🐳 one-command deploy (bring your own TLS proxy)
|
||||||
|
|
||||||
## 🖼️ Screenshots
|
## 🖼️ Screenshots
|
||||||
|
|
||||||
@@ -100,7 +102,7 @@ It's more than an API proxy: it ships with **credit billing, CDK top-ups, referr
|
|||||||
- Overview dashboard (trends / DAU / top failures / top spenders)
|
- Overview dashboard (trends / DAU / top failures / top spenders)
|
||||||
- Model management (normal + agent price) · account management (bulk import / dedup / quota) · **concurrency groups** · **order management** (filter / search / paginate) · site-wide logs · user management (set as agent / assign concurrency group / view cumulative top-up) · CDK · showcase · **announcements** · site config (incl. epay)
|
- Model management (normal + agent price) · account management (bulk import / dedup / quota) · **concurrency groups** · **order management** (filter / search / paginate) · site-wide logs · user management (set as agent / assign concurrency group / view cumulative top-up) · CDK · showcase · **announcements** · site config (incl. epay)
|
||||||
|
|
||||||
**🧰 Engineering highlights**: tls-client (Chrome JA3/JA4 fingerprint) reliably passes Cloudflare · media stored in S3/RustFS, served through an authenticated proxy with retention cleanup · self-healing maintenance loop (quota recovery / credential refresh / orphan-job cleanup with refunds) · one-command Docker deploy with acme.sh auto HTTPS.
|
**🧰 Engineering highlights**: tls-client (Chrome JA3/JA4 fingerprint) reliably passes Cloudflare · media stored in S3/RustFS, served through an authenticated proxy with retention cleanup · self-healing maintenance loop (quota recovery / credential refresh / orphan-job cleanup with refunds) · one-command Docker deploy (TLS via your own reverse proxy).
|
||||||
|
|
||||||
## 🤖 Supported Models / Providers
|
## 🤖 Supported Models / Providers
|
||||||
|
|
||||||
@@ -139,27 +141,11 @@ Images return OpenAI-style `{ "created": ..., "data": [{ "b64_json": "..." }] }`
|
|||||||
|
|
||||||
## 🚀 Deployment
|
## 🚀 Deployment
|
||||||
|
|
||||||
Both frontend and backend are open-source. Docker one-command is recommended; you can also build from source with **Go 1.26+**.
|
> Domain + HTTPS are handled by your own reverse proxy (this project issues no certificates).
|
||||||
|
|
||||||
> Prerequisite: a domain A-record pointing to this host, with **ports 80 / 443 open to the internet** (required for Let's Encrypt verification).
|
**Docker (recommended)**: `docker compose up -d --build` brings up PostgreSQL + Redis + RustFS + backend + frontend (nginx serving **HTTP on container port 2000**); point your reverse proxy at `http://<host>:2000` (port via `WEB_PORT`; set passwords / keys / `CORS_ORIGINS` in a `.env` beside the compose file, `COOKIE_SECURE=true` when your proxy serves HTTPS).
|
||||||
|
|
||||||
### Option 1: Docker, one command (recommended)
|
Or **build from source** — bring your own **PostgreSQL · Redis · RustFS (or any S3) · reverse proxy**:
|
||||||
|
|
||||||
Requires Docker + Docker Compose. A single command brings up Postgres + Redis + RustFS + backend + frontend, and **auto-issues / renews the HTTPS certificate** (built-in acme.sh).
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cp .env.docker.example .env # fill in DOMAIN / ACME_EMAIL / POSTGRES_PASSWORD / S3_SECRET_KEY
|
|
||||||
sh install.sh # = docker compose up -d --build
|
|
||||||
```
|
|
||||||
|
|
||||||
Open `https://<your-domain>/`; watch cert progress with `docker compose logs -f acme`. With `DOMAIN=localhost` a self-signed cert is used (local testing).
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary><b>Option 2: Manual install</b> — bring your own PostgreSQL / Redis / RustFS / Nginx (click to expand)</summary>
|
|
||||||
|
|
||||||
<br/>
|
|
||||||
|
|
||||||
Provide your own **PostgreSQL · Redis · RustFS (or any S3) · Nginx**, **Go 1.26+** for the backend and **Node 18+** for the frontend.
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 1. Create an empty database (the backend auto-migrates on start)
|
# 1. Create an empty database (the backend auto-migrates on start)
|
||||||
@@ -216,7 +202,7 @@ server {
|
|||||||
|---|---|
|
|---|---|
|
||||||
| Backend | Go · gin · gorm (PostgreSQL) · go-redis · tls-client (Chrome fingerprint) |
|
| Backend | Go · gin · gorm (PostgreSQL) · go-redis · tls-client (Chrome fingerprint) |
|
||||||
| Frontend | Vue 3 · Vue Router · Vite · Tailwind CSS v4 |
|
| Frontend | Vue 3 · Vue Router · Vite · Tailwind CSS v4 |
|
||||||
| Infrastructure | PostgreSQL · Redis · RustFS (S3-compatible) · Nginx · acme.sh |
|
| Infrastructure | PostgreSQL · Redis · RustFS (S3-compatible) · Nginx |
|
||||||
|
|
||||||
## 📦 Repository Layout
|
## 📦 Repository Layout
|
||||||
|
|
||||||
@@ -256,12 +242,10 @@ frontend/ Frontend source (Vue 3 + Vite)
|
|||||||
│ ├── layouts/ Public / admin layouts
|
│ ├── layouts/ Public / admin layouts
|
||||||
│ ├── utils/ Utility functions
|
│ ├── utils/ Utility functions
|
||||||
│ └── api.js · auth.js … API client, auth, theme, credits, etc.
|
│ └── api.js · auth.js … API client, auth, theme, credits, etc.
|
||||||
├── Dockerfile Nginx static hosting + cert watcher
|
├── Dockerfile Nginx static hosting (HTTP :2000) + API proxy
|
||||||
└── default.conf.template Nginx site template (reverse proxy + caching)
|
└── default.conf.template Nginx site template (reverse proxy + caching)
|
||||||
|
|
||||||
docker-compose.yml Docker orchestration (Postgres / Redis / RustFS / backend / frontend / acme)
|
docker-compose.yml Docker orchestration (Postgres / Redis / RustFS / backend / frontend)
|
||||||
install.sh One-command deploy script (= docker compose up -d --build)
|
|
||||||
.env.docker.example Deployment env-var template
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🗺️ Roadmap
|
## 🗺️ Roadmap
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
|
<img src="frontend/public/favicon.svg" width="84" alt="Vivid AI" />
|
||||||
|
|
||||||
<h1>image2api</h1>
|
<h1>image2api</h1>
|
||||||
|
|
||||||
**多供应商 AI 生图 / 生视频网关 —— 一套 OpenAI 兼容 API,聚合七大平台,开箱即用的运营系统**
|
**多供应商 AI 生图 / 生视频网关 —— 一套 OpenAI 兼容 API,聚合七大平台,开箱即用的运营系统**
|
||||||
@@ -14,7 +16,7 @@
|
|||||||
[](https://vuejs.org)
|
[](https://vuejs.org)
|
||||||
[](#-部署)
|
[](#-部署)
|
||||||
[](#-openai-兼容-api)
|
[](#-openai-兼容-api)
|
||||||
[](#方式一docker-一键推荐)
|
[](#-部署)
|
||||||
[](#-支持的模型--供应商)
|
[](#-支持的模型--供应商)
|
||||||
[](#-部署)
|
[](#-部署)
|
||||||
[](#-license)
|
[](#-license)
|
||||||
@@ -51,7 +53,7 @@
|
|||||||
|
|
||||||
> 💡 前后端**完全开源**(MIT),Go + Vue 3,可自由二开 / 自部署。
|
> 💡 前后端**完全开源**(MIT),Go + Vue 3,可自由二开 / 自部署。
|
||||||
|
|
||||||
**一句话亮点** 🔌 OpenAI 兼容 · 🤖 7 平台十余模型 · 🔁 自动换号 / Token 保活 · 💳 积分 + 在线充值(易支付)+ 代理价 · 🧩 并发分组 · 🎨 画图前端 + 管理后台 · 🐳 一键部署 + 自动 HTTPS
|
**一句话亮点** 🔌 OpenAI 兼容 · 🤖 7 平台十余模型 · 🔁 自动换号 / Token 保活 · 💳 积分 + 在线充值(易支付)+ 代理价 · 🧩 并发分组 · 🎨 画图前端 + 管理后台 · 🐳 一键部署(TLS 反代自理)
|
||||||
|
|
||||||
## 🖼️ 界面预览
|
## 🖼️ 界面预览
|
||||||
|
|
||||||
@@ -107,7 +109,7 @@
|
|||||||
- 概览看板(趋势 / DAU / 失败 Top / 消费榜)
|
- 概览看板(趋势 / DAU / 失败 Top / 消费榜)
|
||||||
- 模型管理(普通价 + 代理价) · 账号管理(批量导入 / 去重 / 额度) · **并发分组** · **订单管理**(筛选 / 搜索 / 分页) · 全站日志 · 用户管理(设为代理 / 分配并发组 / 看累计充值) · CDK · 展示位 · **站点公告** · 站点配置(含易支付)
|
- 模型管理(普通价 + 代理价) · 账号管理(批量导入 / 去重 / 额度) · **并发分组** · **订单管理**(筛选 / 搜索 / 分页) · 全站日志 · 用户管理(设为代理 / 分配并发组 / 看累计充值) · CDK · 展示位 · **站点公告** · 站点配置(含易支付)
|
||||||
|
|
||||||
**🧰 工程亮点**:tls-client(Chrome JA3/JA4 指纹)稳定穿透 Cloudflare · 媒体存 S3/RustFS 经鉴权代理分发 + 保留期清理 · 自愈式维护轮询(恢复额度 / 刷新凭据 / 清理僵死任务并退款) · 一条命令 Docker 部署 + acme.sh 自动 HTTPS。
|
**🧰 工程亮点**:tls-client(Chrome JA3/JA4 指纹)稳定穿透 Cloudflare · 媒体存 S3/RustFS 经鉴权代理分发 + 保留期清理 · 自愈式维护轮询(恢复额度 / 刷新凭据 / 清理僵死任务并退款) · 一条命令 Docker 部署(TLS 交给你的反代)。
|
||||||
|
|
||||||
## 🤖 支持的模型 / 供应商
|
## 🤖 支持的模型 / 供应商
|
||||||
|
|
||||||
@@ -147,27 +149,11 @@ curl https://你的域名/v1/images/edits \
|
|||||||
|
|
||||||
## 🚀 部署
|
## 🚀 部署
|
||||||
|
|
||||||
前后端均开源。推荐 Docker 一键;也可用 **Go 1.26+** 从源码构建。
|
> 域名 + HTTPS 由你自己的反向代理处理(本项目不内置证书签发)。
|
||||||
|
|
||||||
> 前置:域名 A 记录指向本机,**80 / 443 对公网开放**(Let's Encrypt 验证需要)。
|
**Docker(推荐)**:`docker compose up -d --build` 一条命令拉起 PostgreSQL + Redis + RustFS + 后端 + 前端(nginx **HTTP 监听容器 2000 端口**),把你的反向代理指到 `http://<本机>:2000`(端口用 `WEB_PORT` 改;要自定义密码 / 密钥 / `CORS_ORIGINS` 就在 compose 同目录建 `.env` 配,反代走 HTTPS 时设 `COOKIE_SECURE=true`)。
|
||||||
|
|
||||||
### 方式一:Docker 一键(推荐)
|
也可**从源码手动构建**,自备 **PostgreSQL · Redis · RustFS(或任意 S3)· 反向代理**:
|
||||||
|
|
||||||
需要 Docker + Docker Compose。一条命令拉起 Postgres + Redis + RustFS + 后端 + 前端,并**自动签发 / 续期 HTTPS 证书**(内置 acme.sh)。
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cp .env.docker.example .env # 填 DOMAIN / ACME_EMAIL / POSTGRES_PASSWORD / S3_SECRET_KEY
|
|
||||||
sh install.sh # = docker compose up -d --build
|
|
||||||
```
|
|
||||||
|
|
||||||
打开 `https://<你的域名>/`;证书进度 `docker compose logs -f acme`。`DOMAIN=localhost` 时用自签证书(本地测试)。
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary><b>方式二:手动安装</b> — 自建 PostgreSQL / Redis / RustFS / Nginx(点击展开)</summary>
|
|
||||||
|
|
||||||
<br/>
|
|
||||||
|
|
||||||
自备 **PostgreSQL · Redis · RustFS(或任意 S3)· Nginx**,后端用 **Go 1.26+**,前端用 **Node 18+**。
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 1. 创建空库(后端启动自动建表)
|
# 1. 创建空库(后端启动自动建表)
|
||||||
@@ -216,15 +202,13 @@ server {
|
|||||||
|
|
||||||
> 完整环境变量见 `backend/.env.example`。
|
> 完整环境变量见 `backend/.env.example`。
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
## 🧱 技术栈
|
## 🧱 技术栈
|
||||||
|
|
||||||
| 层 | 技术 |
|
| 层 | 技术 |
|
||||||
|---|---|
|
|---|---|
|
||||||
| 后端 | Go · gin · gorm(PostgreSQL)· go-redis · tls-client(Chrome 指纹) |
|
| 后端 | Go · gin · gorm(PostgreSQL)· go-redis · tls-client(Chrome 指纹) |
|
||||||
| 前端 | Vue 3 · Vue Router · Vite · Tailwind CSS v4 |
|
| 前端 | Vue 3 · Vue Router · Vite · Tailwind CSS v4 |
|
||||||
| 基础设施 | PostgreSQL · Redis · RustFS(S3 兼容)· Nginx · acme.sh |
|
| 基础设施 | PostgreSQL · Redis · RustFS(S3 兼容)· Nginx |
|
||||||
|
|
||||||
## 📦 仓库结构
|
## 📦 仓库结构
|
||||||
|
|
||||||
@@ -264,12 +248,11 @@ frontend/ 前端源码(Vue 3 + Vite)
|
|||||||
│ ├── layouts/ 公共 / 后台布局
|
│ ├── layouts/ 公共 / 后台布局
|
||||||
│ ├── utils/ 工具函数
|
│ ├── utils/ 工具函数
|
||||||
│ └── api.js · auth.js … 接口封装、鉴权、主题、积分等
|
│ └── api.js · auth.js … 接口封装、鉴权、主题、积分等
|
||||||
├── Dockerfile Nginx 静态托管 + 证书监听
|
├── Dockerfile Nginx 静态托管(HTTP :2000)+ API 反代
|
||||||
└── default.conf.template Nginx 站点模板(反代 + 缓存策略)
|
└── default.conf.template Nginx 站点模板(反代 + 缓存策略)
|
||||||
|
|
||||||
docker-compose.yml Docker 编排(Postgres / Redis / RustFS / 后端 / 前端 / acme)
|
docker-compose.yml Docker 编排(Postgres / Redis / RustFS / 后端 / 前端)
|
||||||
install.sh 一键部署脚本(= docker compose up -d --build)
|
.env.example → backend/.env 后端环境变量模板
|
||||||
.env.docker.example 部署环境变量模板
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🗺️ Roadmap
|
## 🗺️ Roadmap
|
||||||
|
|||||||
+13
-62
@@ -1,9 +1,10 @@
|
|||||||
# image2api — one-command stack: PostgreSQL + Redis + RustFS (S3) + backend
|
# image2api — one-command stack: PostgreSQL + Redis + RustFS (S3) + backend
|
||||||
# (pre-built binary, closed-source — backend/bin/api ships with the repo) +
|
# (pre-built binary) + frontend/nginx (built here, HTTP only on port 2000).
|
||||||
# frontend/nginx (open-source, built here).
|
# TLS + domain are NOT handled here — put your own reverse proxy in front of the
|
||||||
# 1) cp .env.docker.example .env # 填 DOMAIN / ACME_EMAIL / 密码 / 密钥
|
# web container (host port ${WEB_PORT:-2000}) and terminate TLS there.
|
||||||
# 2) docker compose up -d --build (或 sh install.sh)
|
# 1) cp .env.docker.example .env # 填 密码 / 密钥 / CORS_ORIGINS
|
||||||
# Then open https://<DOMAIN>/ (nginx serves the SPA, proxies the API, TLS via acme.sh).
|
# 2) docker compose up -d --build
|
||||||
|
# Then point your reverse proxy at http://<host>:${WEB_PORT:-2000}.
|
||||||
services:
|
services:
|
||||||
postgres:
|
postgres:
|
||||||
image: postgres:16-alpine
|
image: postgres:16-alpine
|
||||||
@@ -77,8 +78,10 @@ services:
|
|||||||
RUSTFS_BUCKET: ${S3_BUCKET:-vivid-ai}
|
RUSTFS_BUCKET: ${S3_BUCKET:-vivid-ai}
|
||||||
RUSTFS_ACCESS_KEY: ${S3_ACCESS_KEY:-vividai}
|
RUSTFS_ACCESS_KEY: ${S3_ACCESS_KEY:-vividai}
|
||||||
RUSTFS_SECRET_KEY: ${S3_SECRET_KEY:-vividai-secret-change-me}
|
RUSTFS_SECRET_KEY: ${S3_SECRET_KEY:-vividai-secret-change-me}
|
||||||
CORS_ORIGINS: https://${DOMAIN:-localhost}
|
# Your site origin(s), comma-separated (the URL your reverse proxy serves).
|
||||||
COOKIE_SECURE: ${COOKIE_SECURE:-true}
|
CORS_ORIGINS: ${CORS_ORIGINS:-http://localhost:2000}
|
||||||
|
# Keep true only if your reverse proxy serves HTTPS; false for plain HTTP.
|
||||||
|
COOKIE_SECURE: ${COOKIE_SECURE:-false}
|
||||||
volumes:
|
volumes:
|
||||||
- generated:/app/data/generated
|
- generated:/app/data/generated
|
||||||
depends_on:
|
depends_on:
|
||||||
@@ -98,68 +101,16 @@ services:
|
|||||||
web:
|
web:
|
||||||
build:
|
build:
|
||||||
context: ./frontend
|
context: ./frontend
|
||||||
environment:
|
# nginx serves the SPA + proxies the API on container port 2000 (HTTP only).
|
||||||
DOMAIN: ${DOMAIN:-localhost}
|
# Point your own reverse proxy at host:${WEB_PORT:-2000}.
|
||||||
# Only substitute ${DOMAIN} in the nginx template — leave $host/$uri/etc.
|
|
||||||
NGINX_ENVSUBST_FILTER: DOMAIN
|
|
||||||
ports:
|
ports:
|
||||||
- "${HTTP_PORT:-80}:80"
|
- "${WEB_PORT:-2000}:2000"
|
||||||
- "${HTTPS_PORT:-443}:443"
|
|
||||||
volumes:
|
|
||||||
- certs:/etc/nginx/certs
|
|
||||||
- acme_webroot:/var/www/acme
|
|
||||||
depends_on:
|
depends_on:
|
||||||
- backend
|
- backend
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
# acme.sh — issues + auto-renews the Let's Encrypt cert via http-01 (webroot
|
|
||||||
# shared with nginx) into the shared certs volume. nginx's cert-watch reloads
|
|
||||||
# it. Requires DOMAIN to resolve to this host and ports 80/443 reachable.
|
|
||||||
acme:
|
|
||||||
image: neilpang/acme.sh:latest
|
|
||||||
depends_on:
|
|
||||||
- web
|
|
||||||
environment:
|
|
||||||
DOMAIN: ${DOMAIN:-localhost}
|
|
||||||
ACME_EMAIL: ${ACME_EMAIL:-admin@example.com}
|
|
||||||
# Persist account + cert state here (a volume on /acme.sh would shadow the
|
|
||||||
# installed binary, so use a separate config-home).
|
|
||||||
LE_CONFIG_HOME: /acme-data
|
|
||||||
volumes:
|
|
||||||
- certs:/etc/nginx/certs
|
|
||||||
- acme_webroot:/var/www/acme
|
|
||||||
- acmedata:/acme-data
|
|
||||||
entrypoint: ["/bin/sh", "-c"]
|
|
||||||
command:
|
|
||||||
- |
|
|
||||||
set -e
|
|
||||||
D="$${DOMAIN:-localhost}"
|
|
||||||
# Skip ACME for non-public domains (localhost / bare IP) — the self-signed
|
|
||||||
# bootstrap cert keeps 443 working for local testing.
|
|
||||||
case "$$D" in
|
|
||||||
localhost|127.0.0.1|"") echo "acme: DOMAIN=$$D not public, skipping issuance"; exec tail -f /dev/null ;;
|
|
||||||
esac
|
|
||||||
mkdir -p /etc/nginx/certs/live/$$D /var/www/acme /acme-data
|
|
||||||
acme.sh --set-default-ca --server letsencrypt
|
|
||||||
acme.sh --register-account -m "$${ACME_EMAIL}" || true
|
|
||||||
# Retry the first issue until nginx :80 is reachable for the http-01 check.
|
|
||||||
i=0
|
|
||||||
while [ $$i -lt 10 ]; do
|
|
||||||
if acme.sh --issue -d "$$D" -w /var/www/acme --keylength ec-256; then break; fi
|
|
||||||
i=$$((i+1)); echo "acme: issue attempt $$i failed, retry in 30s"; sleep 30
|
|
||||||
done
|
|
||||||
acme.sh --install-cert -d "$$D" --ecc \
|
|
||||||
--key-file /etc/nginx/certs/live/$$D/privkey.pem \
|
|
||||||
--fullchain-file /etc/nginx/certs/live/$$D/fullchain.pem || true
|
|
||||||
# daemon: renew checks daily; renewals auto-reinstall to the paths above.
|
|
||||||
while true; do sleep 12h; acme.sh --cron || true; done
|
|
||||||
restart: unless-stopped
|
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
pgdata:
|
pgdata:
|
||||||
redisdata:
|
redisdata:
|
||||||
rustfsdata:
|
rustfsdata:
|
||||||
generated:
|
generated:
|
||||||
certs:
|
|
||||||
acme_webroot:
|
|
||||||
acmedata:
|
|
||||||
|
|||||||
+5
-10
@@ -1,7 +1,7 @@
|
|||||||
# syntax=docker/dockerfile:1
|
# syntax=docker/dockerfile:1
|
||||||
# Frontend is open-source: built from source inside the image, then served by
|
# Frontend is open-source: built from source inside the image, then served by
|
||||||
# nginx which also reverse-proxies the API and terminates TLS (certs from the
|
# nginx (HTTP only on :2000) which also reverse-proxies the API. TLS + domain are
|
||||||
# acme.sh sidecar via a shared volume).
|
# handled by the user's own reverse proxy in front of this container.
|
||||||
# ---- build stage ----
|
# ---- build stage ----
|
||||||
FROM node:22-alpine AS build
|
FROM node:22-alpine AS build
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
@@ -12,12 +12,7 @@ RUN npm run build
|
|||||||
|
|
||||||
# ---- serve stage ----
|
# ---- serve stage ----
|
||||||
FROM nginx:1.27-alpine
|
FROM nginx:1.27-alpine
|
||||||
# openssl: self-signed bootstrap cert. The nginx image substitutes ${DOMAIN} in
|
|
||||||
# /etc/nginx/templates/*.template at startup (limited to DOMAIN via the filter).
|
|
||||||
RUN apk add --no-cache openssl
|
|
||||||
COPY --from=build /app/dist /usr/share/nginx/html
|
COPY --from=build /app/dist /usr/share/nginx/html
|
||||||
COPY default.conf.template /etc/nginx/templates/default.conf.template
|
# Plain config (no envsubst / no ${DOMAIN}) — copied straight into conf.d.
|
||||||
COPY docker-entrypoint.d/10-selfsigned.sh /docker-entrypoint.d/10-selfsigned.sh
|
COPY default.conf.template /etc/nginx/conf.d/default.conf
|
||||||
COPY docker-entrypoint.d/30-cert-watch.sh /docker-entrypoint.d/30-cert-watch.sh
|
EXPOSE 2000
|
||||||
RUN chmod +x /docker-entrypoint.d/10-selfsigned.sh /docker-entrypoint.d/30-cert-watch.sh
|
|
||||||
EXPOSE 80 443
|
|
||||||
|
|||||||
@@ -1,43 +1,16 @@
|
|||||||
# nginx for the docker stack. ${DOMAIN} is filled at container start (envsubst,
|
# nginx for the docker stack — HTTP only on :2000. TLS + domain are handled by
|
||||||
# limited to DOMAIN via NGINX_ENVSUBST_FILTER). Port 80 serves the ACME
|
# the user's OWN reverse proxy placed in front of this container; this just serves
|
||||||
# http-01 challenge and redirects everything else to HTTPS; port 443 serves the
|
# the SPA and reverse-proxies the API to the backend.
|
||||||
# SPA + reverse-proxies the API. Certs come from the shared volume, issued/renewed
|
|
||||||
# by the acme.sh sidecar (a self-signed cert bootstraps 443 before the real one).
|
|
||||||
|
|
||||||
# ---- 80: ACME challenge + redirect to HTTPS ----
|
|
||||||
server {
|
server {
|
||||||
listen 80;
|
listen 2000;
|
||||||
listen [::]:80;
|
listen [::]:2000;
|
||||||
server_name ${DOMAIN};
|
server_name _;
|
||||||
|
|
||||||
# acme.sh writes http-01 challenge files here (shared volume).
|
|
||||||
location ^~ /.well-known/acme-challenge/ {
|
|
||||||
root /var/www/acme;
|
|
||||||
default_type "text/plain";
|
|
||||||
}
|
|
||||||
location / {
|
|
||||||
return 301 https://$host$request_uri;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# ---- 443: the app ----
|
|
||||||
server {
|
|
||||||
listen 443 ssl;
|
|
||||||
listen [::]:443 ssl;
|
|
||||||
http2 on;
|
|
||||||
server_name ${DOMAIN};
|
|
||||||
|
|
||||||
ssl_certificate /etc/nginx/certs/live/${DOMAIN}/fullchain.pem;
|
|
||||||
ssl_certificate_key /etc/nginx/certs/live/${DOMAIN}/privkey.pem;
|
|
||||||
ssl_protocols TLSv1.2 TLSv1.3;
|
|
||||||
ssl_session_cache shared:SSL:10m;
|
|
||||||
|
|
||||||
root /usr/share/nginx/html;
|
root /usr/share/nginx/html;
|
||||||
index index.html;
|
index index.html;
|
||||||
|
|
||||||
client_max_body_size 50m;
|
client_max_body_size 50m;
|
||||||
|
|
||||||
# Proxy headers (inherited by all proxy_pass locations below).
|
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
@@ -61,12 +34,10 @@ server {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# ---- API / media / health -> backend ----
|
# ---- API / media / health -> backend ----
|
||||||
# 动态 API no-store,防止 CDN 缓存 GET 响应(如 managed-models)→ 改价不生效。
|
|
||||||
location ^~ /admin/api/ { proxy_pass http://backend:6666; add_header Cache-Control "no-store" always; }
|
location ^~ /admin/api/ { proxy_pass http://backend:6666; add_header Cache-Control "no-store" always; }
|
||||||
location ^~ /images/ { proxy_pass http://backend:6666; }
|
location ^~ /images/ { proxy_pass http://backend:6666; }
|
||||||
location = /health { proxy_pass http://backend:6666; }
|
location = /health { proxy_pass http://backend:6666; }
|
||||||
|
# /v1 is per-API-key authenticated — never cache.
|
||||||
# /v1 is per-API-key authenticated — must NOT be cached by any CDN/proxy.
|
|
||||||
location ^~ /v1/ {
|
location ^~ /v1/ {
|
||||||
proxy_pass http://backend:6666;
|
proxy_pass http://backend:6666;
|
||||||
add_header Cache-Control "no-store" always;
|
add_header Cache-Control "no-store" always;
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
# Bootstrap a self-signed cert so nginx's 443 server block can start BEFORE
|
|
||||||
# acme.sh has issued the real certificate. acme.sh later overwrites these files
|
|
||||||
# in the shared volume; 30-cert-watch.sh reloads nginx when that happens.
|
|
||||||
set -e
|
|
||||||
D="${DOMAIN:-localhost}"
|
|
||||||
CERT_DIR="/etc/nginx/certs/live/$D"
|
|
||||||
mkdir -p "$CERT_DIR" /var/www/acme
|
|
||||||
if [ ! -s "$CERT_DIR/fullchain.pem" ] || [ ! -s "$CERT_DIR/privkey.pem" ]; then
|
|
||||||
echo "nginx: generating self-signed bootstrap cert for $D"
|
|
||||||
openssl req -x509 -newkey rsa:2048 -nodes -days 3650 \
|
|
||||||
-keyout "$CERT_DIR/privkey.pem" -out "$CERT_DIR/fullchain.pem" \
|
|
||||||
-subj "/CN=$D" >/dev/null 2>&1
|
|
||||||
fi
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
# Reload nginx whenever the certificate file changes — i.e. once acme.sh has
|
|
||||||
# issued/renewed the real cert into the shared volume, nginx picks it up within
|
|
||||||
# ~a minute without a container restart. Runs in the background so it doesn't
|
|
||||||
# block startup.
|
|
||||||
D="${DOMAIN:-localhost}"
|
|
||||||
CERT="/etc/nginx/certs/live/$D/fullchain.pem"
|
|
||||||
(
|
|
||||||
last=""
|
|
||||||
while true; do
|
|
||||||
sleep 60
|
|
||||||
cur="$(stat -c %Y "$CERT" 2>/dev/null || echo '')"
|
|
||||||
if [ -n "$cur" ] && [ "$cur" != "$last" ]; then
|
|
||||||
# Skip the very first observation (the self-signed bootstrap); only reload
|
|
||||||
# on a genuine change (acme.sh overwrote the cert).
|
|
||||||
[ -n "$last" ] && nginx -s reload 2>/dev/null || true
|
|
||||||
last="$cur"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
) &
|
|
||||||
@@ -78,6 +78,18 @@ const sizeTable = [
|
|||||||
{ ratio: '1:3 · 竖', k1: '512x1536', k2: '768x2304', k4: '1280x3840' },
|
{ ratio: '1:3 · 竖', k1: '512x1536', k2: '768x2304', k4: '1280x3840' },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
// ---- 视频 size → 比例 × 分辨率(720p / 1080p)----
|
||||||
|
// 视频按「短边」判档:短边 <1080 → 720p,≥1080 → 1080p;宽高比映射比例。
|
||||||
|
const videoSizeTable = [
|
||||||
|
{ ratio: '16:9 · 横', p720: '1280x720', p1080: '1920x1080' },
|
||||||
|
{ ratio: '9:16 · 竖', p720: '720x1280', p1080: '1080x1920' },
|
||||||
|
{ ratio: '1:1 · 方', p720: '720x720', p1080: '1080x1080' },
|
||||||
|
{ ratio: '4:3 · 横', p720: '960x720', p1080: '1440x1080' },
|
||||||
|
{ ratio: '3:4 · 竖', p720: '720x960', p1080: '1080x1440' },
|
||||||
|
{ ratio: '3:2 · 横', p720: '1080x720', p1080: '1620x1080' },
|
||||||
|
{ ratio: '2:3 · 竖', p720: '720x1080', p1080: '1080x1620' },
|
||||||
|
]
|
||||||
|
|
||||||
// ---- examples (built in script so refs resolve correctly) ----
|
// ---- examples (built in script so refs resolve correctly) ----
|
||||||
const examples = computed(() => [
|
const examples = computed(() => [
|
||||||
{
|
{
|
||||||
@@ -331,10 +343,10 @@ async function copy(text) {
|
|||||||
|
|
||||||
<!-- size 对照表(课时表)—— 解决"传错分辨率" -->
|
<!-- size 对照表(课时表)—— 解决"传错分辨率" -->
|
||||||
<section>
|
<section>
|
||||||
<h2 class="text-lg font-semibold mb-1">分辨率对照表 · <code class="text-white/70 text-sm">size</code> 该传什么</h2>
|
<h2 class="text-lg font-semibold mb-1">图像分辨率对照表 · <code class="text-white/70 text-sm">size</code> 该传什么</h2>
|
||||||
<p class="text-xs text-white/45 mb-3">
|
<p class="text-xs text-white/45 mb-3">
|
||||||
左边选比例,上面选分辨率档,交叉格里就是 <code class="text-white/70">size</code> 要传的值(直接复制)。
|
左边选比例,上面选分辨率档,交叉格里就是 <code class="text-white/70">size</code> 要传的值(直接复制)。
|
||||||
没有 <code class="text-white/70">quality</code> 参数,分辨率只看 <code class="text-white/70">size</code> 的长边。
|
没有 <code class="text-white/70">quality</code> 参数,图像分辨率只看 <code class="text-white/70">size</code> 的<strong class="text-white/70">长边</strong>。
|
||||||
档位必须是该模型支持的(见上方「可用模型」的分辨率列),不支持会自动回退到该模型最低档。
|
档位必须是该模型支持的(见上方「可用模型」的分辨率列),不支持会自动回退到该模型最低档。
|
||||||
</p>
|
</p>
|
||||||
<div class="card overflow-hidden">
|
<div class="card overflow-hidden">
|
||||||
@@ -359,6 +371,33 @@ async function copy(text) {
|
|||||||
例:想要 <strong class="text-white/70">2K 的 16:9 横图</strong> → <code class="text-white/70">"size": "2048x1152"</code>。
|
例:想要 <strong class="text-white/70">2K 的 16:9 横图</strong> → <code class="text-white/70">"size": "2048x1152"</code>。
|
||||||
留空 size = 默认 <strong class="text-white/70">1:1 · 2K</strong>。
|
留空 size = 默认 <strong class="text-white/70">1:1 · 2K</strong>。
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<!-- 视频分辨率(720p / 1080p,按短边判) -->
|
||||||
|
<h2 class="text-lg font-semibold mb-1 mt-8">视频分辨率对照表 · <code class="text-white/70 text-sm">size</code> 该传什么</h2>
|
||||||
|
<p class="text-xs text-white/45 mb-3">
|
||||||
|
视频用 <code class="text-white/70">720p</code> / <code class="text-white/70">1080p</code> 两档,只看 <code class="text-white/70">size</code> 的<strong class="text-white/70">短边</strong>(短边 ≥1080 = 1080p,否则 720p)。
|
||||||
|
档位必须是该视频模型支持的(如 grok-video 仅 720p),不支持会被拒。
|
||||||
|
</p>
|
||||||
|
<div class="card overflow-hidden">
|
||||||
|
<table class="w-full text-sm">
|
||||||
|
<thead><tr class="text-left text-[11px] uppercase tracking-wider text-white/40 border-b border-white/[0.08]">
|
||||||
|
<th class="px-4 py-2.5 font-medium">比例</th>
|
||||||
|
<th class="px-4 py-2.5 font-medium">720p</th>
|
||||||
|
<th class="px-4 py-2.5 font-medium">1080p</th>
|
||||||
|
</tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="row in videoSizeTable" :key="row.ratio" class="border-b border-white/[0.04] last:border-0">
|
||||||
|
<td class="px-4 py-2.5 text-white/75">{{ row.ratio }}</td>
|
||||||
|
<td class="px-4 py-2.5 font-mono text-white/85">{{ row.p720 }}</td>
|
||||||
|
<td class="px-4 py-2.5 font-mono text-white/85">{{ row.p1080 }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<p class="text-xs text-white/40 mt-2">
|
||||||
|
例:想要 <strong class="text-white/70">720p 的 16:9 横版视频</strong> → <code class="text-white/70">"size": "1280x720"</code>;
|
||||||
|
竖版 9:16 → <code class="text-white/70">"720x1280"</code>。
|
||||||
|
</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- examples -->
|
<!-- examples -->
|
||||||
|
|||||||
-42
@@ -1,42 +0,0 @@
|
|||||||
#!/usr/bin/env sh
|
|
||||||
# image2api — one-command install (Docker). Run from the repo root:
|
|
||||||
# sh install.sh
|
|
||||||
# Brings up Postgres + Redis + RustFS + backend + frontend, and auto-issues a
|
|
||||||
# Let's Encrypt HTTPS certificate via the built-in acme.sh service.
|
|
||||||
set -e
|
|
||||||
cd "$(dirname "$0")"
|
|
||||||
|
|
||||||
# --- docker present? ---
|
|
||||||
if ! command -v docker >/dev/null 2>&1; then
|
|
||||||
echo "ERROR: 未安装 Docker。请先安装 Docker + Docker Compose。"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# --- env file ---
|
|
||||||
if [ ! -f .env ]; then
|
|
||||||
echo "==> 生成 .env(从 .env.docker.example),请按提示编辑后重跑"
|
|
||||||
cp .env.docker.example .env
|
|
||||||
echo
|
|
||||||
echo " 必填:DOMAIN(你的域名)、ACME_EMAIL(证书邮箱)、POSTGRES_PASSWORD、S3_SECRET_KEY"
|
|
||||||
echo " 编辑好后再次执行: sh install.sh"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# --- backend binary (closed-source, shipped prebuilt) ---
|
|
||||||
if [ ! -f backend/bin/api ]; then
|
|
||||||
echo "ERROR: 缺少 backend/bin/api(后端二进制)。"
|
|
||||||
echo " 请从 Releases 下载 linux/amd64 的 api 放到 backend/bin/api 后重试。"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
chmod +x backend/bin/api 2>/dev/null || true
|
|
||||||
|
|
||||||
# --- up ---
|
|
||||||
echo "==> docker compose up -d --build"
|
|
||||||
docker compose up -d --build
|
|
||||||
|
|
||||||
DOMAIN_VAL="$(grep -E '^DOMAIN=' .env | head -1 | cut -d= -f2-)"
|
|
||||||
echo
|
|
||||||
echo "完成 ✅ 打开 https://${DOMAIN_VAL:-<你的域名>}/"
|
|
||||||
echo "证书签发进度: docker compose logs -f acme"
|
|
||||||
echo "后端日志: docker compose logs -f backend"
|
|
||||||
echo "停止: docker compose down"
|
|
||||||
Reference in New Issue
Block a user