-
Notifications
You must be signed in to change notification settings - Fork 4
feat(deps): Docker Compose 部署——PostgreSQL + Redis + Backend #148
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
c042937
feat(docker): add Docker Compose deployment for backend and PostgreSQL
xiaocheny214 dc3c362
fix(deploy): make the container build and the API reachable from the …
Soli22de 0fb5bf0
fix(deps): docker-compose 添加 Redis 服务,注入 REDIS_URL
xiaocheny214 f99f30b
fix(db): init.sql 补充 windup_project 唯一约束,对齐 ORM 声明
xiaocheny214 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| # ── 后端 Dockerfile ────────────────────────────────────────────────── | ||
| # 多阶段构建:builder 装依赖 → runtime 只拷贝产物,镜像更小 | ||
|
|
||
| # ── 阶段 1: 构建 ── | ||
| FROM python:3.12-slim AS builder | ||
|
|
||
| # 安装 uv(比 pip 快 10x) | ||
| COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv | ||
|
|
||
| # ── 国内网络:走镜像源 + 拉长超时 ──────────────────────────────────────── | ||
| # 实测(2026-08-04,这台服务器):宿主机访问 pypi.org 需 8s,构建容器内默认超时 | ||
| # 会在下载大包(uvloop)时 "operation timed out" 直接失败。 | ||
| ENV UV_DEFAULT_INDEX=https://mirrors.aliyun.com/pypi/simple/ \ | ||
| UV_HTTP_TIMEOUT=180 | ||
|
|
||
| # 必须与 runtime 阶段同路径 —— venv 内的 shebang/.pth 是绝对路径,跨路径拷贝会失效 | ||
| WORKDIR /app | ||
|
|
||
| # 先拷贝依赖定义,利用 Docker layer cache | ||
| COPY pyproject.toml uv.lock ./ | ||
| COPY packages/common/pyproject.toml packages/common/ | ||
| COPY packages/framework/pyproject.toml packages/framework/ | ||
| COPY packages/ai_engine/pyproject.toml packages/ai_engine/ | ||
| COPY packages/app/pyproject.toml packages/app/ | ||
|
|
||
| # 安装依赖(不含 dev 依赖) | ||
| RUN uv sync --frozen --no-dev --no-install-workspace | ||
|
|
||
| # 拷贝源码并安装 | ||
| COPY packages/ packages/ | ||
| RUN uv sync --frozen --no-dev | ||
|
|
||
| # ── 阶段 2: 运行时 ── | ||
| FROM python:3.12-slim AS runtime | ||
|
|
||
| WORKDIR /app | ||
|
|
||
| # 从 builder 拷贝虚拟环境和包 | ||
| COPY --from=builder /app/.venv /app/.venv | ||
| COPY --from=builder /app/packages /app/packages | ||
|
|
||
| # 把 venv/bin 加入 PATH | ||
| ENV PATH="/app/.venv/bin:$PATH" | ||
|
|
||
| # 默认环境变量(可被 docker-compose / .env 覆盖) | ||
| ENV WINDUP_HOST=0.0.0.0 | ||
| ENV WINDUP_PORT=8000 | ||
| ENV WINDUP_RELOAD=false | ||
|
|
||
| EXPOSE 8000 | ||
|
|
||
| # 健康检查 | ||
| HEALTHCHECK --interval=30s --timeout=5s --retries=3 \ | ||
| CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/docs')" || exit 1 | ||
|
|
||
| # 启动命令 | ||
| CMD ["uvicorn", "windup_app.bootstrap.app:create_app", "--factory", "--host", "0.0.0.0", "--port", "8000"] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,15 +1,97 @@ | ||
|
|
||
| """FastAPI 应用工厂与装配入口。 | ||
|
|
||
| ``create_app`` 负责创建 FastAPI 实例并挂载路由 / 中间件 / 异常处理, | ||
| 是整个 web 服务的唯一装配点(composition root)。 | ||
|
|
||
| ``main`` 是开发启动入口:``python -m windup_app`` 或 ``windup`` 命令。 | ||
| """ | ||
|
|
||
| import os | ||
| from contextlib import asynccontextmanager | ||
|
|
||
| import windup_framework.db # noqa: F401 组装时显式触发 DB engine/session 初始化 | ||
| from fastapi import FastAPI | ||
| from fastapi.middleware.cors import CORSMiddleware | ||
|
|
||
| from windup_app.server.orchestrator.executor import run_action_task, run_image_task | ||
| from windup_app.web.api.character import router as character_router | ||
| from windup_app.web.api.generation import router as generation_router | ||
| from windup_app.web.api.media import router as media_router | ||
| from windup_app.web.api.project import router as project_router | ||
| from windup_app.web.handler.exception_handlers import register_exception_handlers | ||
|
|
||
|
|
||
| def _env_flag(name: str) -> bool: | ||
| """把环境变量解析为真正的布尔值:仅 1/true/yes/on(忽略大小写与空白)视为 True。""" | ||
| return os.getenv(name, "").strip().lower() in {"1", "true", "yes", "on"} | ||
|
|
||
|
|
||
|
|
||
| def _cors_origins() -> list[str]: | ||
| """允许跨域的前端来源,逗号分隔的 WINDUP_CORS_ORIGINS 覆盖。 | ||
|
|
||
| 不配这个中间件的话,浏览器会把前端的**所有**请求拦在预检那一步 | ||
| (OPTIONS 返回 405、响应无 access-control-* 头),后端日志里连请求都看不到。 | ||
| 默认值覆盖本地 dev server 与 Vercel 预览域名。 | ||
| """ | ||
| raw = os.getenv("WINDUP_CORS_ORIGINS", "").strip() | ||
| if raw: | ||
| return [o.strip() for o in raw.split(",") if o.strip()] | ||
| return ["http://localhost:5173", "http://127.0.0.1:5173", | ||
| "http://localhost:3000", "http://127.0.0.1:3000"] | ||
|
|
||
|
|
||
| def print_banner() -> None: | ||
| """启动时打印 banner(占位实现,后续替换为正式 ASCII banner)。""" | ||
| print("windup 0.1.0 starting ...") | ||
|
|
||
|
|
||
| @asynccontextmanager | ||
| async def _lifespan(app: FastAPI): | ||
| """应用启动时打印 banner,关闭时无特殊处理。""" | ||
| print_banner() | ||
| yield | ||
|
|
||
|
|
||
| def create_app() -> FastAPI: | ||
| app = FastAPI(title="windup", version="0.1.0") | ||
| app = FastAPI(title="windup", version="0.1.0", lifespan=_lifespan) | ||
| app.add_middleware( | ||
| CORSMiddleware, | ||
| allow_origins=_cors_origins(), | ||
| allow_origin_regex=r"https://.*\.vercel\.app", | ||
| allow_credentials=True, | ||
| allow_methods=["*"], | ||
| allow_headers=["*"], | ||
| ) | ||
| app.include_router(project_router) | ||
| app.include_router(character_router) | ||
| app.include_router(media_router) | ||
| app.include_router(generation_router) | ||
| # 生成后台调度器注入 app.state:bootstrap(composition root)持有 ai_engine 依赖, | ||
| # web 端运行期从 request.app.state 取,避免 web 静态 import ai_engine(入口层门禁)。 | ||
| app.state.run_action_task = run_action_task | ||
| app.state.run_image_task = run_image_task | ||
| register_exception_handlers(app) | ||
| return app | ||
|
|
||
|
|
||
| def main() -> None: | ||
| """开发启动入口:用 uvicorn 跑 ``create_app``。 | ||
|
|
||
| host/port/reload 可用 ``WINDUP_HOST`` / ``WINDUP_PORT`` / ``WINDUP_RELOAD`` 覆盖。 | ||
| """ | ||
| import uvicorn | ||
|
|
||
| uvicorn.run( | ||
| "windup_app.bootstrap.app:create_app", | ||
| factory=True, | ||
| host=os.getenv("WINDUP_HOST", "127.0.0.1"), | ||
| port=int(os.getenv("WINDUP_PORT", "8000")), | ||
| reload=_env_flag("WINDUP_RELOAD"), | ||
| ) | ||
|
|
||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| main() | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,128 @@ | ||
| -- ────────────────────────────────────────────────────────────── | ||
| -- Windup 数据库初始化脚本 | ||
| -- 首次启动时自动执行,创建所有表结构 | ||
| -- 数据库已通过 POSTGRES_DB 环境变量自动创建 | ||
| -- ────────────────────────────────────────────────────────────── | ||
|
|
||
| /*项目表,全局约束角色人物。*/ | ||
| CREATE TABLE windup_project ( | ||
| id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, | ||
| user_id BIGINT NOT NULL, | ||
| workflow_id BIGINT, | ||
| project_name varchar(20) NOT NULL, | ||
| character_perspective SMALLINT NOT NULL, | ||
| directional_movement SMALLINT NOT NULL, | ||
| sprite_width SMALLINT NOT NULL, | ||
| sprite_height SMALLINT NOT NULL, | ||
| game_style TEXT, | ||
| sprite_sample_url TEXT, | ||
| create_at TIMESTAMPTZ, | ||
| update_at TIMESTAMPTZ, | ||
|
|
||
| CONSTRAINT uq_windup_project_user_name UNIQUE (user_id, project_name) | ||
| ); | ||
|
|
||
| COMMENT ON TABLE windup_project IS '项目表'; | ||
| COMMENT ON COLUMN windup_project.id IS '项目表主键'; | ||
| COMMENT ON COLUMN windup_project.user_id IS '创建者 ID'; | ||
| COMMENT ON COLUMN windup_project.workflow_id IS '工作流 ID'; | ||
| COMMENT ON COLUMN windup_project.project_name IS '项目名称'; | ||
| COMMENT ON COLUMN windup_project.character_perspective IS '游戏视角 :1 = 横版视角,2 = 俯视 ,3 = 2.5D '; | ||
| COMMENT ON COLUMN windup_project.directional_movement IS '移动方向 :1 = 单向 ,2 = 四向,3 = 八向'; | ||
| COMMENT ON COLUMN windup_project.sprite_width IS '角色尺寸 宽:32 、64 、128、256、512、1024、2048'; | ||
| COMMENT ON COLUMN windup_project.sprite_height IS '角色尺寸 高:32 、64 、128、256、512、1024、2048'; | ||
| COMMENT ON COLUMN windup_project.game_style IS '游戏风格'; | ||
| COMMENT ON COLUMN windup_project.sprite_sample_url IS '参考图URL'; | ||
| COMMENT ON COLUMN windup_project.create_at IS '创建时间'; | ||
| COMMENT ON COLUMN windup_project.update_at IS '修改时间'; | ||
|
|
||
| /*用户表*/ | ||
| CREATE TABLE windup_user ( | ||
| id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, | ||
| email VARCHAR(255) UNIQUE, | ||
| password_hash VARCHAR(255) NOT NULL, | ||
| nickname VARCHAR(255), | ||
| email_verified_at TIMESTAMPTZ, | ||
| status SMALLINT DEFAULT 0, | ||
| last_login_at TIMESTAMPTZ, | ||
| create_at TIMESTAMPTZ, | ||
| update_at TIMESTAMPTZ | ||
| ); | ||
|
|
||
| COMMENT ON TABLE windup_user IS '用户表'; | ||
| COMMENT ON COLUMN windup_user.id IS '用户表主键'; | ||
| COMMENT ON COLUMN windup_user.email IS '邮箱地址'; | ||
| COMMENT ON COLUMN windup_user.password_hash IS '用户密码(加密存储)'; | ||
| COMMENT ON COLUMN windup_user.nickname IS '用户昵称'; | ||
| COMMENT ON COLUMN windup_user.email_verified_at IS '邮箱校验时间'; | ||
| COMMENT ON COLUMN windup_user.status IS '用户状态 默认0 正常,1 封禁'; | ||
| COMMENT ON COLUMN windup_user.last_login_at IS '上次登录时间'; | ||
| COMMENT ON COLUMN windup_user.create_at IS '创建时间'; | ||
| COMMENT ON COLUMN windup_user.update_at IS '修改时间'; | ||
|
|
||
| /*角色资产表*/ | ||
| CREATE TABLE windup_character ( | ||
| id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, | ||
| project_id BIGINT NOT NULL, | ||
| name varchar(20), | ||
| description TEXT NULL, | ||
| reference_image_url TEXT NULL, | ||
| character_data JSONB NOT NULL DEFAULT '{}'::jsonb, | ||
| status SMALLINT NOT NULL DEFAULT 1, | ||
| create_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, | ||
| update_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, | ||
|
|
||
| CONSTRAINT ck_windup_character_status CHECK (status IN (0, 1)), | ||
| CONSTRAINT ck_windup_character_data_object CHECK (jsonb_typeof(character_data) = 'object') | ||
| ); | ||
|
|
||
| CREATE INDEX idx_windup_character_project_id | ||
| ON windup_character (project_id); | ||
|
|
||
| CREATE INDEX idx_windup_character_project_update_at | ||
| ON windup_character (project_id, update_at DESC); | ||
|
|
||
| COMMENT ON TABLE windup_character IS '角色资产表;角色隶属于项目,是资产库中的基本资产'; | ||
| COMMENT ON COLUMN windup_character.id IS '角色资产主键'; | ||
| COMMENT ON COLUMN windup_character.project_id IS '所属项目 ID'; | ||
| COMMENT ON COLUMN windup_character.name IS '角色名称'; | ||
| COMMENT ON COLUMN windup_character.description IS '角色描述'; | ||
| COMMENT ON COLUMN windup_character.reference_image_url IS '角色参考图 URL;角色模板仅作为角色的参考图属性,不单独建表'; | ||
| COMMENT ON COLUMN windup_character.character_data IS '角色完整数据 JSON;包含造型、动作及动作帧等信息'; | ||
| COMMENT ON COLUMN windup_character.status IS '角色状态: 1-正常, 0-禁用'; | ||
| COMMENT ON COLUMN windup_character.create_at IS '创建时间'; | ||
| COMMENT ON COLUMN windup_character.update_at IS '最后更新时间'; | ||
| COMMENT ON CONSTRAINT ck_windup_character_status ON windup_character IS '角色状态只能为 0 或 1'; | ||
| COMMENT ON CONSTRAINT ck_windup_character_data_object ON windup_character IS '角色完整数据必须为 JSON 对象'; | ||
|
|
||
| /*生成任务表*/ | ||
| CREATE TABLE IF NOT EXISTS windup_generation_task ( | ||
| id BIGSERIAL PRIMARY KEY, | ||
| user_id BIGINT NOT NULL, | ||
| project_id BIGINT, | ||
| task_type TEXT NOT NULL DEFAULT 'character_image', | ||
| status TEXT NOT NULL DEFAULT 'pending', | ||
| input_payload JSONB NOT NULL DEFAULT '{}', | ||
| result_type TEXT, | ||
| result JSONB, | ||
| error_message TEXT, | ||
| create_at TIMESTAMPTZ NOT NULL DEFAULT now(), | ||
| update_at TIMESTAMPTZ NOT NULL DEFAULT now() | ||
| ); | ||
|
|
||
| COMMENT ON TABLE windup_generation_task IS '生成任务表——记录每次AI生成任务'; | ||
| COMMENT ON COLUMN windup_generation_task.id IS '任务主键'; | ||
| COMMENT ON COLUMN windup_generation_task.user_id IS '发起用户 ID'; | ||
| COMMENT ON COLUMN windup_generation_task.project_id IS '关联项目 ID'; | ||
| COMMENT ON COLUMN windup_generation_task.task_type IS '任务类型:character_image = 角色图片生成'; | ||
| COMMENT ON COLUMN windup_generation_task.status IS '任务状态:pending = 待执行,running = 执行中,completed = 完成,failed = 失败'; | ||
| COMMENT ON COLUMN windup_generation_task.input_payload IS '任务输入参数 JSON'; | ||
| COMMENT ON COLUMN windup_generation_task.result_type IS '结果类型'; | ||
| COMMENT ON COLUMN windup_generation_task.result IS '任务结果 JSON'; | ||
| COMMENT ON COLUMN windup_generation_task.error_message IS '错误信息(失败时记录)'; | ||
| COMMENT ON COLUMN windup_generation_task.create_at IS '创建时间'; | ||
| COMMENT ON COLUMN windup_generation_task.update_at IS '修改时间'; | ||
|
|
||
| CREATE INDEX IF NOT EXISTS idx_generation_task_user_id ON windup_generation_task (user_id); | ||
| CREATE INDEX IF NOT EXISTS idx_generation_task_project_id ON windup_generation_task (project_id); | ||
| CREATE INDEX IF NOT EXISTS idx_generation_task_status ON windup_generation_task (status); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,98 @@ | ||
| # ── Windup Docker Compose ───────────────────────────────────────────── | ||
| # 使用方式: | ||
| # 启动服务: docker compose up -d | ||
| # 查看日志: docker compose logs -f [service] | ||
| # 停止服务: docker compose down | ||
| # 清理数据: docker compose down -v (⚠️ 会删除数据库数据) | ||
| # 重新构建: docker compose up -d --build | ||
|
|
||
| services: | ||
| # ── Redis 缓存(验证码 / refresh_token) ── | ||
| redis: | ||
| image: redis:7-alpine | ||
| container_name: windup-redis | ||
| restart: unless-stopped | ||
| healthcheck: | ||
| test: ["CMD", "redis-cli", "ping"] | ||
| interval: 10s | ||
| timeout: 5s | ||
| retries: 5 | ||
| networks: | ||
| - windup-net | ||
|
|
||
| # ── PostgreSQL 数据库 ── | ||
| postgres: | ||
| image: postgres:16-alpine | ||
| container_name: windup-postgres | ||
| restart: unless-stopped | ||
| environment: | ||
| POSTGRES_USER: ${POSTGRES_USER:-root} | ||
| POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-W1ndup@2026!Secure} | ||
| POSTGRES_DB: ${POSTGRES_DB:-windup} | ||
| ports: | ||
| - "${POSTGRES_EXTERNAL_PORT:-7856}:5432" | ||
| volumes: | ||
| - postgres_data:/var/lib/postgresql/data | ||
| - ./db/init.sql:/docker-entrypoint-initdb.d/init.sql:ro | ||
| healthcheck: | ||
| test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-root} -d ${POSTGRES_DB:-windup}"] | ||
| interval: 10s | ||
| timeout: 5s | ||
| retries: 5 | ||
| networks: | ||
| - windup-net | ||
|
|
||
| # ── 后端 API 服务 ── | ||
| backend: | ||
| build: | ||
| context: ./backend | ||
| dockerfile: Dockerfile | ||
| container_name: windup-backend | ||
| restart: unless-stopped | ||
| depends_on: | ||
| postgres: | ||
| condition: service_healthy | ||
| redis: | ||
| condition: service_healthy | ||
| environment: | ||
| # Redis(验证码 / refresh_token) | ||
| REDIS_URL: redis://redis:6379/0 | ||
| # 数据库连接(容器内部通信用 postgres 主机名,端口 5432) | ||
| POSTGRES_HOST: postgres | ||
| POSTGRES_PORT: 5432 | ||
| POSTGRES_USER: ${POSTGRES_USER:-root} | ||
| POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-admin123} | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The backend falls back to |
||
| POSTGRES_DB: ${POSTGRES_DB:-windup} | ||
| # LLM 配置 | ||
| LLM_API_KEY: ${LLM_API_KEY} | ||
| LLM_MODEL_ID: ${LLM_MODEL_ID:-doubao-seed-2.0-mini} | ||
| LLM_BASE_URL: ${LLM_BASE_URL:-https://api.qnaigc.com/v1} | ||
| LLM_IMAGE_MODEL_ID: ${LLM_IMAGE_MODEL_ID:-gemini-3.0-pro-image-preview} | ||
| LLM_VIDEO_MODEL_ID: ${LLM_VIDEO_MODEL_ID} | ||
| # 搜索 | ||
| SERPAPI_API_KEY: ${SERPAPI_API_KEY} | ||
| # 七牛云存储 | ||
| QINIU_ACCESS_KEY: ${QINIU_ACCESS_KEY} | ||
| QINIU_SECRET_KEY: ${QINIU_SECRET_KEY} | ||
| QINIU_BUCKET_NAME: ${QINIU_BUCKET_NAME} | ||
| QINIU_BUCKET_DOMAIN: ${QINIU_BUCKET_DOMAIN} | ||
| QINIU_PRIVATE_SPACE: ${QINIU_PRIVATE_SPACE:-false} | ||
| # AI Provider | ||
| AI_BASE_URL: ${AI_BASE_URL:-https://api.qnaigc.com/v1} | ||
| AI_API_KEY: ${AI_API_KEY} | ||
| ports: | ||
| - "${WINDUP_PORT:-8000}:8000" | ||
| networks: | ||
| - windup-net | ||
|
|
||
| volumes: | ||
| postgres_data: | ||
| driver: local | ||
|
|
||
| networks: | ||
| windup-net: | ||
| driver: bridge | ||
| # 宿主机链路 MTU 是 1480(eno1),compose 自建网络不会继承 daemon 的 mtu 设置, | ||
| # 默认仍是 1500 → 大包被丢,表现为 TLS 握手超时(七牛上传域名连不上、pip 下载卡死)。 | ||
| driver_opts: | ||
| com.docker.network.driver.mtu: "1450" | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This import points at
windup_app.server.orchestrator.executor, but there is noserver/orchestratorpackage in the checked-out tree.create_app()is imported by the smoke test, so this will raiseModuleNotFoundErrorbefore the app can start.