Deploying Laravel with Docker, GitHub Actions, and Kubernetes

Deploying Laravel with Docker, GitHub Actions, and Kubernetes

使用 Docker、GitHub Actions 和 Kubernetes 部署 Laravel

Chapter 3: The Dockerfile — Multi-Stage Build

第 3 章:Dockerfile — 多阶段构建

3.1 Why Multi-Stage Builds?

3.1 为什么需要多阶段构建?

A naive Dockerfile installs everything (Composer, Node.js, npm, dev dependencies) in a single layer. The resulting image is hundreds of megabytes larger than it needs to be and may contain security vulnerabilities from build tools that have no business being in production. 一个简单的 Dockerfile 会将所有内容(Composer、Node.js、npm、开发依赖)安装在同一个层中。这样生成的镜像会比实际需要的大几百兆字节,并且可能包含生产环境中完全不需要的构建工具所带来的安全漏洞。

Multi-stage builds solve this by separating the build environment from the runtime environment: 多阶段构建通过将构建环境与运行环境分离来解决这个问题:

  • Stage 1 (builder): Has Composer, Node.js, npm, and all dev tooling installed. Installs production PHP dependencies and compiles JavaScript assets.
  • 阶段 1 (builder): 安装了 Composer、Node.js、npm 和所有开发工具。用于安装生产环境的 PHP 依赖并编译 JavaScript 资源。
  • Stage 2 (production): Starts from a clean Alpine Linux base. Copies only the compiled output from Stage 1. Contains nothing that is not needed at runtime.
  • 阶段 2 (production): 从干净的 Alpine Linux 基础镜像开始。仅复制阶段 1 的编译输出。不包含任何运行时不需要的内容。
MetricSingle-StageMulti-Stage
Image size~800MB~120MB
Attack surfaceHigh (build tools, dev deps)Low (runtime only)
Build cachingPoorExcellent (stages cache independently)
Security scanningMany CVEs from build toolsMinimal
指标单阶段多阶段
镜像大小~800MB~120MB
攻击面高(包含构建工具、开发依赖)低(仅运行时)
构建缓存优秀(各阶段独立缓存)
安全扫描存在大量构建工具相关的 CVE极少

3.2 The Complete Dockerfile

3.2 完整的 Dockerfile

# docker/Dockerfile

# ─────────────────────────────────────────────────────────────────────────
# Stage 1: builder
# Installs all dependencies and compiles assets.
# This stage is NEVER shipped to production.
# ─────────────────────────────────────────────────────────────────────────
# 阶段 1:builder
# 安装所有依赖并编译资源。
# 此阶段的内容绝不会发布到生产环境。
# ─────────────────────────────────────────────────────────────────────────
FROM php:8.3-cli AS builder

# Install system dependencies needed during the build phase:
# git - Composer needs this to clone packages from GitHub
# zip/unzip - needed for PHP archive operations
# libpng-dev, libonig-dev, libxml2-dev - headers for PHP extensions
# nodejs, npm - for compiling JS assets with Vite
# 安装构建阶段所需的系统依赖:
# git - Composer 需要它从 GitHub 克隆包
# zip/unzip - PHP 归档操作所需
# libpng-dev, libonig-dev, libxml2-dev - PHP 扩展的头文件
# nodejs, npm - 用于使用 Vite 编译 JS 资源
RUN apt-get update && apt-get install -y \
    git curl zip unzip \
    libpng-dev libonig-dev libxml2-dev \
    nodejs npm \
    && docker-php-ext-install pdo pdo_sqlite mbstring exif pcntl bcmath gd \
    && curl -sS https://getcomposer.org/installer | php \
    && mv composer.phar /usr/local/bin/composer \
    && rm -rf /var/lib/apt/lists/*

WORKDIR /app

# Copy composer files first to leverage Docker layer caching.
# If composer.json and composer.lock have not changed, Docker reuses
# the cached layer even if application code has changed.
# 先复制 composer 文件以利用 Docker 层缓存。
# 如果 composer.json 和 composer.lock 没有变化,即使应用代码变了,Docker 也会重用缓存层。
COPY composer.json composer.lock ./
RUN composer install \
    --no-dev \
    --optimize-autoloader \
    --no-interaction \
    --no-scripts

# Copy package.json for the same caching benefit
# 复制 package.json 以获得同样的缓存优势
COPY package.json package-lock.json ./
RUN npm ci

# Now copy the rest of the application code
# 现在复制其余的应用代码
COPY . .

# Run Composer scripts now that the full codebase is present
# 在代码库完整的情况下运行 Composer 脚本
RUN composer run-script post-autoload-dump --no-interaction 2>/dev/null || true

# Compile JavaScript/CSS assets
# 编译 JavaScript/CSS 资源
RUN npm run build

# ─────────────────────────────────────────────────────────────────────────
# Stage 2: production
# Lean Alpine-based image. Only runtime dependencies.
# ─────────────────────────────────────────────────────────────────────────
# 阶段 2:production
# 基于 Alpine 的精简镜像。仅包含运行时依赖。
# ─────────────────────────────────────────────────────────────────────────
FROM php:8.3-fpm-alpine AS production

# Install runtime system packages:
# nginx - HTTP server to accept incoming requests
# supervisor - process manager to run both nginx and php-fpm
# sqlite - SQLite runtime libraries
# 安装运行时系统包:
# nginx - 用于接收请求的 HTTP 服务器
# supervisor - 同时运行 nginx 和 php-fpm 的进程管理器
# sqlite - SQLite 运行时库
RUN apk add --no-cache \
    nginx \
    supervisor \
    sqlite \
    && docker-php-ext-install pdo pdo_sqlite mbstring exif pcntl bcmath

WORKDIR /var/www/html

# Copy the compiled application from the builder stage.
# Crucially, we do NOT copy node_modules or dev dependencies.
# 从 builder 阶段复制编译好的应用。
# 关键点:我们不会复制 node_modules 或开发依赖。
COPY --from=builder /app /var/www/html

# Copy configuration files
# 复制配置文件
COPY docker/nginx.conf /etc/nginx/nginx.conf
COPY docker/php.ini /usr/local/etc/php/conf.d/custom.ini
COPY docker/supervisord.conf /etc/supervisord.conf

# Create the SQLite database directory.
# The actual .sqlite file will come from a PersistentVolumeClaim in k8s.
# Locally it will be mounted via Docker volume.
# 创建 SQLite 数据库目录。
# 实际的 .sqlite 文件将来自 k8s 中的 PersistentVolumeClaim。
# 在本地,它将通过 Docker 卷挂载。
RUN mkdir -p /var/www/html/database

# Fix storage permissions.
# www-data is the nginx/php-fpm user.
# 修复存储权限。
# www-data 是 nginx/php-fpm 的用户。
RUN chown -R www-data:www-data \
    /var/www/html/storage \
    /var/www/html/bootstrap/cache \
    /var/www/html/database \
    && chmod -R 775 \
    /var/www/html/storage \
    /var/www/html/bootstrap/cache \
    /var/www/html/database

# Optimise Laravel for production
# 为生产环境优化 Laravel
RUN php artisan config:cache \
    && php artisan route:cache \
    && php artisan view:cache

EXPOSE 80

# Supervisord starts both nginx and php-fpm
# Supervisord 同时启动 nginx 和 php-fpm
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisord.conf"]

3.3 Nginx Configuration

3.3 Nginx 配置

Nginx acts as the front-facing HTTP server inside the container. It serves static assets (CSS, JS, images) directly from disk and proxies all PHP requests to PHP-FPM via FastCGI. Nginx 在容器内充当前端 HTTP 服务器。它直接从磁盘提供静态资源(CSS、JS、图片),并将所有 PHP 请求通过 FastCGI 代理给 PHP-FPM。

# docker/nginx.conf
# We use the events block even though we are not customising it.
# Nginx requires it.
# 即使我们没有自定义,也需要使用 events 块,这是 Nginx 的要求。
events {
    worker_connections 1024;
}

http {
    include /etc/nginx/mime.types;
    default_type application/octet-stream;

    # Logging — output to stdout/stderr so kubectl logs works
    # 日志记录 — 输出到 stdout/stderr 以便 kubectl logs 可以查看
    access_log /dev/stdout;
    error_log /dev/stderr warn;

    # Basic performance settings
    # 基本性能设置
    sendfile on;
    keepalive_timeout 65;
    gzip on;
    gzip_types text/plain application/json application/javascript text/css;

    server {
        listen 80;
        server_name _;
        root /var/www/html/public;
        index index.php;

        # Laravel's try_files pattern:
        # 1. Try to serve the exact file ($uri)
        # 2. Try to serve as a directory ($uri/)
        # 3. Fall back to index.php with the query string
        # Laravel 的 try_files 模式:
        # 1. 尝试提供确切的文件 ($uri)
        # 2. 尝试作为目录提供 ($uri/)
        # 3. 回退到 index.php 并带上查询字符串
        location / {
            try_files $uri $uri/ /index.php?$query_string;
        }

        # Send all .php files to PHP-FPM running on port 9000
        # 将所有 .php 文件发送到运行在 9000 端口的 PHP-FPM
        location ~ \.php$ {
            fastcgi_pass 127.0.0.1:9000;
            fastcgi_index index.php;
            include fastcgi_params;
            fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
            fastcgi_param PATH_INFO $fastcgi_path_info;
            fastcgi_read_timeout 300;
        }

        # Deny access to .htaccess and other hidden files
        # 禁止访问 .htaccess 和其他隐藏文件
        location ~ /\.ht {
            deny all;
        }

        # Deny direct access to the SQLite database file
        # 禁止直接访问 SQLite 数据库文件
        location ~ \.sqlite$ {
            deny all;
        }
    }
}

3.4 PHP Configuration

3.4 PHP 配置

; docker/php.ini
; Production PHP settings for a Laravel API
; Laravel API 的生产环境 PHP 设置

; Memory limit — increase for large Eloquent operations
; 内存限制 — 为大型 Eloquent 操作增加内存
memory_limit = 256M

; Maximum execution time in seconds
; 最大执行时间(秒)
max_execution_time = 60

; Maximum POST body size (for file uploads via API)
; 最大 POST 正文大小(用于 API 文件上传)
post_max_size = 20M
upload_max_filesize = 20M