refactor: optimize Docker build with multi-stage and slim runtime base image

This commit is contained in:
fawney19
2025-12-19 15:51:21 +08:00
parent af476ff21e
commit c2ddc6bd3c
4 changed files with 146 additions and 225 deletions

View File

@@ -148,7 +148,7 @@ jobs:
- name: Update Dockerfile.app to use registry base image - name: Update Dockerfile.app to use registry base image
run: | run: |
sed -i "s|FROM aether-base:latest|FROM ${{ env.REGISTRY }}/${{ env.BASE_IMAGE_NAME }}:latest|g" Dockerfile.app sed -i "s|FROM aether-base:latest AS builder|FROM ${{ env.REGISTRY }}/${{ env.BASE_IMAGE_NAME }}:latest AS builder|g" Dockerfile.app
- name: Build and push app image - name: Build and push app image
uses: docker/build-push-action@v5 uses: docker/build-push-action@v5

View File

@@ -1,16 +1,127 @@
# 应用镜像:基于基础镜像,只复制代码(秒级构建) # 运行镜像:从 base 提取产物到精简运行时
# 构建命令: docker build -f Dockerfile.app -t aether-app:latest . # 构建命令: docker build -f Dockerfile.app -t aether-app:latest .
FROM aether-base:latest FROM aether-base:latest AS builder
# ==================== 运行时镜像 ====================
FROM python:3.12-slim
WORKDIR /app WORKDIR /app
# 运行时依赖(无 gcc/nodejs/npm
RUN apt-get update && apt-get install -y \
nginx \
supervisor \
libpq5 \
curl \
&& rm -rf /var/lib/apt/lists/*
# 从 base 镜像复制 Python 包
COPY --from=builder /usr/local/lib/python3.12/site-packages /usr/local/lib/python3.12/site-packages
# 只复制需要的 Python 可执行文件
COPY --from=builder /usr/local/bin/gunicorn /usr/local/bin/
COPY --from=builder /usr/local/bin/uvicorn /usr/local/bin/
COPY --from=builder /usr/local/bin/alembic /usr/local/bin/
# 从 base 镜像复制前端产物
COPY --from=builder /app/frontend/dist /usr/share/nginx/html
# 复制后端代码 # 复制后端代码
COPY src/ ./src/ COPY src/ ./src/
COPY alembic.ini ./ COPY alembic.ini ./
COPY alembic/ ./alembic/ COPY alembic/ ./alembic/
# 构建前端(使用基础镜像中已安装的 node_modules # Nginx 配置模板
COPY frontend/ /tmp/frontend/ RUN printf '%s\n' \
RUN cd /tmp/frontend && npm run build && \ 'server {' \
cp -r dist/* /usr/share/nginx/html/ && \ ' listen 80;' \
rm -rf /tmp/frontend ' server_name _;' \
' root /usr/share/nginx/html;' \
' index index.html;' \
' client_max_body_size 100M;' \
'' \
' location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {' \
' expires 1y;' \
' add_header Cache-Control "public, no-transform";' \
' try_files $uri =404;' \
' }' \
'' \
' location ~ ^/(src|node_modules)/ {' \
' deny all;' \
' return 404;' \
' }' \
'' \
' location ~ ^/(dashboard|admin|login)(/|$) {' \
' try_files $uri $uri/ /index.html;' \
' }' \
'' \
' location / {' \
' try_files $uri $uri/ @backend;' \
' }' \
'' \
' location @backend {' \
' proxy_pass http://127.0.0.1:PORT_PLACEHOLDER;' \
' proxy_http_version 1.1;' \
' proxy_set_header Host $host;' \
' proxy_set_header X-Real-IP $remote_addr;' \
' proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;' \
' proxy_set_header X-Forwarded-Proto $scheme;' \
' proxy_set_header Connection "";' \
' proxy_set_header Accept $http_accept;' \
' proxy_set_header Content-Type $content_type;' \
' proxy_set_header Authorization $http_authorization;' \
' proxy_set_header X-Api-Key $http_x_api_key;' \
' proxy_buffering off;' \
' proxy_cache off;' \
' proxy_request_buffering off;' \
' chunked_transfer_encoding on;' \
' gzip off;' \
' add_header X-Accel-Buffering no;' \
' proxy_connect_timeout 600s;' \
' proxy_send_timeout 600s;' \
' proxy_read_timeout 600s;' \
' }' \
'}' > /etc/nginx/sites-available/default.template
# Supervisor 配置
RUN printf '%s\n' \
'[supervisord]' \
'nodaemon=true' \
'logfile=/var/log/supervisor/supervisord.log' \
'pidfile=/var/run/supervisord.pid' \
'' \
'[program:nginx]' \
'command=/bin/bash -c "sed \"s/PORT_PLACEHOLDER/${PORT:-8084}/g\" /etc/nginx/sites-available/default.template > /etc/nginx/sites-available/default && /usr/sbin/nginx -g \"daemon off;\""' \
'autostart=true' \
'autorestart=true' \
'stdout_logfile=/var/log/nginx/access.log' \
'stderr_logfile=/var/log/nginx/error.log' \
'' \
'[program:app]' \
'command=gunicorn src.main:app -w %(ENV_GUNICORN_WORKERS)s -k uvicorn.workers.UvicornWorker --bind 0.0.0.0:%(ENV_PORT)s --timeout 120 --access-logfile - --error-logfile - --log-level info' \
'directory=/app' \
'autostart=true' \
'autorestart=true' \
'stdout_logfile=/dev/stdout' \
'stdout_logfile_maxbytes=0' \
'stderr_logfile=/dev/stderr' \
'stderr_logfile_maxbytes=0' \
'environment=PYTHONUNBUFFERED=1,PYTHONIOENCODING=utf-8,LANG=C.UTF-8,LC_ALL=C.UTF-8,DOCKER_CONTAINER=true' > /etc/supervisor/conf.d/supervisord.conf
# 创建目录
RUN mkdir -p /var/log/supervisor /app/logs /app/data
# 环境变量
ENV PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1 \
PYTHONIOENCODING=utf-8 \
LANG=C.UTF-8 \
LC_ALL=C.UTF-8 \
PORT=8084
EXPOSE 80
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD curl -f http://localhost/health || exit 1
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"]

View File

@@ -1,124 +1,29 @@
# 基础镜像:包含所有依赖,只在依赖变化时需要重建 # 构建镜像:编译环境 + 预编译的依赖
# 用于 GitHub Actions CI 构建(不使用国内镜像源) # 用于 GitHub Actions CI 构建(不使用国内镜像源)
# 构建命令: docker build -f Dockerfile.base -t aether-base:latest .
FROM python:3.12-slim FROM python:3.12-slim
WORKDIR /app WORKDIR /app
# 系统依赖 # 构建工具
RUN apt-get update && apt-get install -y \ RUN apt-get update && apt-get install -y \
nginx \
supervisor \
libpq-dev \ libpq-dev \
gcc \ gcc \
curl \
gettext-base \
nodejs \ nodejs \
npm \ npm \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
# Python 依赖(安装到系统,不用 -e 模式) # Python 依赖
COPY pyproject.toml README.md ./ COPY pyproject.toml README.md ./
RUN mkdir -p src && touch src/__init__.py && \ RUN mkdir -p src && touch src/__init__.py && \
SETUPTOOLS_SCM_PRETEND_VERSION=0.1.0 pip install --no-cache-dir . SETUPTOOLS_SCM_PRETEND_VERSION=0.1.0 pip install --no-cache-dir . && \
pip cache purge
# 前端依赖 # 前端构建
COPY frontend/package*.json /tmp/frontend/ COPY frontend/ ./frontend/
WORKDIR /tmp/frontend RUN cd frontend && npm ci && npm run build && rm -rf node_modules && npm cache clean --force
RUN npm ci
# Nginx 配置模板 # 产物位置:
RUN printf '%s\n' \ # - Python 包: /usr/local/lib/python3.12/site-packages
'server {' \ # - Python 可执行文件: /usr/local/bin
' listen 80;' \ # - 前端构建产物: /app/frontend/dist
' server_name _;' \
' root /usr/share/nginx/html;' \
' index index.html;' \
' client_max_body_size 100M;' \
'' \
' location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {' \
' expires 1y;' \
' add_header Cache-Control "public, no-transform";' \
' try_files $uri =404;' \
' }' \
'' \
' location ~ ^/(src|node_modules)/ {' \
' deny all;' \
' return 404;' \
' }' \
'' \
' location ~ ^/(dashboard|admin|login)(/|$) {' \
' try_files $uri $uri/ /index.html;' \
' }' \
'' \
' location / {' \
' try_files $uri $uri/ @backend;' \
' }' \
'' \
' location @backend {' \
' proxy_pass http://127.0.0.1:PORT_PLACEHOLDER;' \
' proxy_http_version 1.1;' \
' proxy_set_header Host $host;' \
' proxy_set_header X-Real-IP $remote_addr;' \
' proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;' \
' proxy_set_header X-Forwarded-Proto $scheme;' \
' proxy_set_header Connection "";' \
' proxy_set_header Accept $http_accept;' \
' proxy_set_header Content-Type $content_type;' \
' proxy_set_header Authorization $http_authorization;' \
' proxy_set_header X-Api-Key $http_x_api_key;' \
' proxy_buffering off;' \
' proxy_cache off;' \
' proxy_request_buffering off;' \
' chunked_transfer_encoding on;' \
' gzip off;' \
' add_header X-Accel-Buffering no;' \
' proxy_connect_timeout 600s;' \
' proxy_send_timeout 600s;' \
' proxy_read_timeout 600s;' \
' }' \
'}' > /etc/nginx/sites-available/default.template
# Supervisor 配置
RUN printf '%s\n' \
'[supervisord]' \
'nodaemon=true' \
'logfile=/var/log/supervisor/supervisord.log' \
'pidfile=/var/run/supervisord.pid' \
'' \
'[program:nginx]' \
'command=/bin/bash -c "sed \"s/PORT_PLACEHOLDER/${PORT:-8084}/g\" /etc/nginx/sites-available/default.template > /etc/nginx/sites-available/default && /usr/sbin/nginx -g \"daemon off;\""' \
'autostart=true' \
'autorestart=true' \
'stdout_logfile=/var/log/nginx/access.log' \
'stderr_logfile=/var/log/nginx/error.log' \
'' \
'[program:app]' \
'command=gunicorn src.main:app -w %(ENV_GUNICORN_WORKERS)s -k uvicorn.workers.UvicornWorker --bind 0.0.0.0:%(ENV_PORT)s --timeout 120 --access-logfile - --error-logfile - --log-level info' \
'directory=/app' \
'autostart=true' \
'autorestart=true' \
'stdout_logfile=/dev/stdout' \
'stdout_logfile_maxbytes=0' \
'stderr_logfile=/dev/stderr' \
'stderr_logfile_maxbytes=0' \
'environment=PYTHONUNBUFFERED=1,PYTHONIOENCODING=utf-8,LANG=C.UTF-8,LC_ALL=C.UTF-8,DOCKER_CONTAINER=true' > /etc/supervisor/conf.d/supervisord.conf
# 创建目录
RUN mkdir -p /var/log/supervisor /app/logs /app/data /usr/share/nginx/html
WORKDIR /app
# 环境变量
ENV PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1 \
PYTHONIOENCODING=utf-8 \
LANG=C.UTF-8 \
LC_ALL=C.UTF-8 \
PORT=8084
EXPOSE 80
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD curl -f http://localhost/health || exit 1
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"]

View File

@@ -1,18 +1,14 @@
# 基础镜像:包含所有依赖,只在依赖变化时需要重建 # 构建镜像:编译环境 + 预编译的依赖(国内镜像源版本)
# 构建命令: docker build -f Dockerfile.base -t aether-base:latest . # 构建命令: docker build -f Dockerfile.base.local -t aether-base:latest .
FROM python:3.12-slim FROM python:3.12-slim
WORKDIR /app WORKDIR /app
# 系统依赖 # 构建工具(使用清华镜像源)
RUN sed -i 's/deb.debian.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apt/sources.list.d/debian.sources && \ RUN sed -i 's/deb.debian.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apt/sources.list.d/debian.sources && \
apt-get update && apt-get install -y \ apt-get update && apt-get install -y \
nginx \
supervisor \
libpq-dev \ libpq-dev \
gcc \ gcc \
curl \
gettext-base \
nodejs \ nodejs \
npm \ npm \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
@@ -20,109 +16,18 @@ RUN sed -i 's/deb.debian.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apt/sources.li
# pip 镜像源 # pip 镜像源
RUN pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple RUN pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple
# Python 依赖(安装到系统,不用 -e 模式) # Python 依赖
COPY pyproject.toml README.md ./ COPY pyproject.toml README.md ./
RUN mkdir -p src && touch src/__init__.py && \ RUN mkdir -p src && touch src/__init__.py && \
SETUPTOOLS_SCM_PRETEND_VERSION=0.1.0 pip install --no-cache-dir . SETUPTOOLS_SCM_PRETEND_VERSION=0.1.0 pip install --no-cache-dir . && \
pip cache purge
# 前端依赖 # 前端构建(使用淘宝镜像源)
COPY frontend/package*.json /tmp/frontend/ COPY frontend/ ./frontend/
WORKDIR /tmp/frontend RUN cd frontend && npm config set registry https://registry.npmmirror.com && \
RUN npm config set registry https://registry.npmmirror.com && npm ci npm ci && npm run build && rm -rf node_modules && npm cache clean --force
# Nginx 配置模板 # 产物位置:
RUN printf '%s\n' \ # - Python 包: /usr/local/lib/python3.12/site-packages
'server {' \ # - Python 可执行文件: /usr/local/bin
' listen 80;' \ # - 前端构建产物: /app/frontend/dist
' server_name _;' \
' root /usr/share/nginx/html;' \
' index index.html;' \
' client_max_body_size 100M;' \
'' \
' location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {' \
' expires 1y;' \
' add_header Cache-Control "public, no-transform";' \
' try_files $uri =404;' \
' }' \
'' \
' location ~ ^/(src|node_modules)/ {' \
' deny all;' \
' return 404;' \
' }' \
'' \
' location ~ ^/(dashboard|admin|login)(/|$) {' \
' try_files $uri $uri/ /index.html;' \
' }' \
'' \
' location / {' \
' try_files $uri $uri/ @backend;' \
' }' \
'' \
' location @backend {' \
' proxy_pass http://127.0.0.1:PORT_PLACEHOLDER;' \
' proxy_http_version 1.1;' \
' proxy_set_header Host $host;' \
' proxy_set_header X-Real-IP $remote_addr;' \
' proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;' \
' proxy_set_header X-Forwarded-Proto $scheme;' \
' proxy_set_header Connection "";' \
' proxy_set_header Accept $http_accept;' \
' proxy_set_header Content-Type $content_type;' \
' proxy_set_header Authorization $http_authorization;' \
' proxy_set_header X-Api-Key $http_x_api_key;' \
' proxy_buffering off;' \
' proxy_cache off;' \
' proxy_request_buffering off;' \
' chunked_transfer_encoding on;' \
' gzip off;' \
' add_header X-Accel-Buffering no;' \
' proxy_connect_timeout 600s;' \
' proxy_send_timeout 600s;' \
' proxy_read_timeout 600s;' \
' }' \
'}' > /etc/nginx/sites-available/default.template
# Supervisor 配置
RUN printf '%s\n' \
'[supervisord]' \
'nodaemon=true' \
'logfile=/var/log/supervisor/supervisord.log' \
'pidfile=/var/run/supervisord.pid' \
'' \
'[program:nginx]' \
'command=/bin/bash -c "sed \"s/PORT_PLACEHOLDER/${PORT:-8084}/g\" /etc/nginx/sites-available/default.template > /etc/nginx/sites-available/default && /usr/sbin/nginx -g \"daemon off;\""' \
'autostart=true' \
'autorestart=true' \
'stdout_logfile=/var/log/nginx/access.log' \
'stderr_logfile=/var/log/nginx/error.log' \
'' \
'[program:app]' \
'command=gunicorn src.main:app -w %(ENV_GUNICORN_WORKERS)s -k uvicorn.workers.UvicornWorker --bind 0.0.0.0:%(ENV_PORT)s --timeout 120 --access-logfile - --error-logfile - --log-level info' \
'directory=/app' \
'autostart=true' \
'autorestart=true' \
'stdout_logfile=/dev/stdout' \
'stdout_logfile_maxbytes=0' \
'stderr_logfile=/dev/stderr' \
'stderr_logfile_maxbytes=0' \
'environment=PYTHONUNBUFFERED=1,PYTHONIOENCODING=utf-8,LANG=C.UTF-8,LC_ALL=C.UTF-8,DOCKER_CONTAINER=true' > /etc/supervisor/conf.d/supervisord.conf
# 创建目录
RUN mkdir -p /var/log/supervisor /app/logs /app/data /usr/share/nginx/html
WORKDIR /app
# 环境变量
ENV PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1 \
PYTHONIOENCODING=utf-8 \
LANG=C.UTF-8 \
LC_ALL=C.UTF-8 \
PORT=8084
EXPOSE 80
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD curl -f http://localhost/health || exit 1
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"]