使用场景:在一台机器上构建 tianlu-api 镜像,通过文件拷贝到另一台机器,再用 docker-compose 一键启动整套服务。
命令环境:示例均基于 Windows PowerShell
目录要求:所有命令请在项目根目录执行(能看到 Dockerfiledocker-compose.yaml 的目录)。

迁移流程图

flowchart TD
    Start([开始]) --> Source[源机器]
    Source --> Build[1. 构建镜像<br/>docker build --ssh ...]
    Build --> Export[2. 导出镜像<br/>docker save -o tianlu-api.tar]
    Export --> Copy[3. 拷贝文件<br/>tianlu-api.tar]
    Copy --> Target[目标机器]
    Target --> Load[4. 导入镜像<br/>docker load -i tianlu-api.tar]
    Load --> Compose[5. 启动服务<br/>docker-compose up -d]
    Compose --> Verify[6. 验证服务<br/>检查状态和日志]
    Verify --> End([完成])
    
    style Start fill:#e1f5e1
    style End fill:#e1f5e1
    style Source fill:#fff4e1
    style Target fill:#e1f0ff
    style Build fill:#ffe1e1
    style Export fill:#ffe1e1
    style Copy fill:#f0e1ff
    style Load fill:#e1f0ff
    style Compose fill:#e1f0ff
    style Verify fill:#e1f0ff

流程说明

  1. 源机器:构建 Docker 镜像并导出为 tar 文件

  2. 文件传输:将 tar 文件拷贝到目标机器(U盘/网络/其他方式)

  3. 目标机器:导入镜像并使用 docker-compose 启动服务


1. 在源机器打包镜像

1.1 构建镜像(从 Gitee 拉取私有依赖)

  1. 打开 PowerShell,并切换到项目根目录,例如:

    cd D:\work\code\tianlu\project-tianlu
    
  2. 确认本机已经配置好可用于访问 Gitee 的 SSH 私钥(例如 C:\Users\你的用户名\.ssh\id_ed25519),且该私钥已添加到 SSH agent 中(可执行 ssh-add -l 检查)。

  3. 执行构建命令(注意使用双引号和 $() 展开变量):

    docker build --ssh "default=$($env:USERPROFILE)\.ssh\id_ed25519" -f Dockerfile -t tianlu-api:latest .
    
    • --ssh "default=...":将本机 SSH 私钥以 default 这一标识挂载到构建容器中,供 uv sync 访问 Gitee 上的 tf-mltfcommon 等私有仓库。

    • -f Dockerfile:指定使用当前项目中的 Dockerfile

    • -t tianlu-api:latest:为构建出的镜像打上名字和标签,后续 docker-compose 会直接使用这个镜像名。

  4. 构建成功后,可以通过以下命令确认镜像已经存在:

    docker images tianlu-api:latest
    

SSH 相关注意事项(与 Dockerfile 中 known_hosts 配置对应)

  • 必须传入 SSH 凭证:如果不带 --ssh ... 参数,构建过程中 uv sync 无法从 Gitee 拉取私有依赖,会报 Host key verification failed 或权限错误。

  • 私钥路径可调整:如果你的私钥文件名不是 id_ed25519,只需把路径替换为实际的私钥路径即可。

  • 安全性说明

    • Dockerfile 中只写入了 gitee.comknown_hosts(主机指纹),不会将你的私钥打进镜像

    • SSH 私钥仅在构建阶段通过 --ssh 临时挂载到构建容器中,最终生成的运行时镜像中不包含任何 SSH 密钥。

  • 不使用 SSH agent 的情况:如果没有启用 SSH agent,也可以直接传文件路径,例如:

    docker build --ssh default=C:\Users\你的用户名\.ssh\id_ed25519 -f Dockerfile -t tianlu-api:latest .
    

    实际效果与使用 default=$($env:USERPROFILE)\.ssh\id_ed25519 类似。

1.2 导出镜像为文件

  1. 仍在项目根目录,执行如下命令将镜像打包为一个 .tar 文件:

    docker save -o tianlu-api.tar tianlu-api:latest
    
    • -o tianlu-api.tar:指定输出文件名为 tianlu-api.tar,会保存在当前目录。

    • tianlu-api:latest:要导出的镜像名。

  2. 将生成的 tianlu-api.tar 拷贝到目标机器,可以通过:

    • U 盘 / 移动硬盘

    • 内网共享目录

    • 网盘 / 文件传输工具等方式


2. 在目标机器导入镜像

  1. 将在源机器打包好的 tianlu-api.tar 拷贝到目标机器上(建议放到与项目同级目录,便于管理)。

  2. 打开 PowerShell,切换到 tianlu-api.tar 所在目录,执行:

    docker load -i tianlu-api.tar
    
    • 该命令会将 tianlu-api.tar 中的镜像导入到目标机器的本地 Docker 镜像仓库中。

  3. 导入完成后,可以执行:

    docker images tianlu-api:latest
    

    如果能看到 tianlu-api 这一镜像,说明导入成功。


3. 使用 docker-compose 启动服务

docker-compose.yaml 已经为你配置好了完整的运行环境,包含:

  • postgres

    • 使用镜像:postgres:18

    • 数据库名:tianlu

    • 账号密码:tianlu / tianlu

    • 端口映射:宿主机 5432 → 容器 5432

    • 数据卷:postgres_data(持久化数据库数据)

    • 网络:tianlu-network

  • api

    • 使用镜像:tianlu-api:latest(前面导入的镜像)

    • 端口映射:宿主机 8000 → 容器 8000

    • 环境变量:DATABASE_URLSYNC_DATABASE_URL 等已指向 postgres 服务

    • 启动命令:先执行 alembic upgrade head 迁移数据库,再启动 uvicorn 服务

    • 网络:与 postgres 一致,均在 tianlu-network

3.1 启动数据库和 API

  1. 在目标机器上,确保已经有 docker-compose.yaml 文件(内容参考文档底部的附录)。

  2. 在该文件所在目录执行:

    docker-compose --profile dev up -d
    
    • --profile dev:启用 api 服务声明的 dev profile。

    • up -d:以后台方式启动所有定义的服务(本项目是 postgresapi)。

  3. 首次启动时:

    • Docker 会自动创建网络 tianlu-network 和数据卷 postgres_data

    • postgres 服务启动后会初始化数据库。

    • api 服务在 postgres 健康检查通过后启动,并自动执行数据库迁移。

3.2 查看服务状态与实时日志

  1. 查看当前服务是否都已运行:

    docker-compose ps
    
    • 期望看到 postgresapi 均为 Up 状态。

  2. 如果需要排查问题,可以查看实时日志:

    docker-compose logs -f
    
    • Ctrl + C 可退出日志跟随模式,但不会停止容器。

3.3 在浏览器中访问

  • 默认访问地址:

    • 应用入口(前端 + 后端):http://127.0.0.1:8000/

  • 如果在远程服务器部署,需要将 127.0.0.1 替换为服务器 IP。

3.4 停止并清理容器

当不再需要运行服务时,可以在 docker-compose.yaml 所在目录执行:

docker-compose down
  • 该命令会停止并删除 postgresapi 容器,但默认不会删除数据卷 postgres_data

  • 如果希望连数据卷一起删除(慎用),可执行:

docker-compose down -v

附录:Dockerfile(参考)

# https://docs.astral.sh/uv/guides/integration/docker/#available-images
FROM python:3.10-trixie AS uv
RUN wget -qO- https://astral.sh/uv/install.sh | sh

WORKDIR /workspace
ENV PATH="/root/.local/bin:$PATH" \
    UV_COMPILE_BYTECODE=1 \
    UV_LINK_MODE=copy

FROM uv AS ci

# 配置 SSH known_hosts 以避免 Host key verification failed
RUN mkdir -p /root/.ssh && \
    ssh-keyscan gitee.com >> /root/.ssh/known_hosts

ENV UV_PROJECT_ENVIRONMENT=/root/.cache/uv/venv\
    PYTHONUNBUFFERED=1
COPY pyproject.toml uv.lock ./
RUN --mount=type=cache,target=/root/.cache/uv --mount=type=ssh \
    uv sync --frozen

FROM uv AS runtime

# 配置 SSH known_hosts 以避免 Host key verification failed
RUN mkdir -p /root/.ssh && \
    ssh-keyscan gitee.com >> /root/.ssh/known_hosts

COPY pyproject.toml uv.lock ./
RUN --mount=type=cache,target=/root/.cache/uv --mount=type=ssh \
    uv sync --frozen --no-dev


FROM python:3.10-slim-trixie AS app

RUN groupadd -r appuser && useradd -r -g appuser appuser

WORKDIR /workspace

# 复制虚拟环境并清理缓存文件以减小镜像大小
COPY --from=runtime /workspace/.venv ./.venv
RUN find .venv -type d -name __pycache__ -exec rm -r {} + 2>/dev/null || true && \
    find .venv -type f -name "*.pyc" -delete && \
    find .venv -type f -name "*.pyo" -delete && \
    rm -rf .venv/lib/python*/test .venv/lib/python*/tests .venv/lib/python*/idlelib 2>/dev/null || true && \
    rm -rf .venv/lib/python*/ensurepip .venv/lib/python*/pydoc_data 2>/dev/null || true

COPY --chown=appuser:appuser alembic.ini ./
COPY --chown=appuser:appuser alembic ./alembic
COPY --chown=appuser:appuser scripts ./scripts
COPY --chown=appuser:appuser src ./src

ENV PATH="/workspace/.venv/bin:$PATH" \
    PORT=8000

USER appuser
EXPOSE $PORT
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
    CMD python -c "import httpx; httpx.get('http://localhost:$PORT', timeout=5.0)" || exit 1

CMD ["uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "3"]

附录:docker-compose.yaml(参考)

name: tianlu-test

services:
  postgres:
    image: postgres:18 # 使用 PostgreSQL 18 镜像
    container_name: tianlu-postgres-test
    environment:
      POSTGRES_DB: tianlu # 数据库名称
      POSTGRES_USER: tianlu # 数据库用户
      POSTGRES_PASSWORD: tianlu # 数据库密码
    ports:
      - "5432:5432" # PostgreSQL 默认端口 前面本地端口 后面容器端口
    volumes:
      - postgres_data:/var/lib/postgresql # 数据卷
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U tianlu -d tianlu"] # PostgreSQL 健康检查
      interval: 10s
      timeout: 5s
      retries: 5
    networks:
      - tianlu-network

  api:
    profiles:
      - dev
    image: tianlu-api:latest
    container_name: tianlu-api-test
    environment:
      DATABASE_URL: "postgresql+asyncpg://tianlu:tianlu@postgres:5432/tianlu" # 更新为 PostgreSQL 连接字符串
      SYNC_DATABASE_URL: "postgresql+psycopg2://tianlu:tianlu@postgres:5432/tianlu"
      SECRET_KEY: "your-secret-key-change-in-production"
      DEBUG: "false"
      CORS_ORIGINS: '["http://localhost:8000"]'
    command: ["sh", "-c", "alembic upgrade head && python -m uvicorn src.main:app --host 0.0.0.0 --port 8000 --workers 3"]
    ports:
      - "8000:8000"
    depends_on:
      postgres: # 依赖 postgres 服务
        condition: service_healthy
    networks:
      - tianlu-network
    restart: unless-stopped

volumes:
  postgres_data: # 更新数据卷名称

networks:
  tianlu-network:
    driver: bridge