容器化部署

使用 Docker 容器化部署 Viswoole 应用可以实现环境一致性、快速扩缩容和简化运维。本文档提供完整的容器化部署方案。

Dockerfile

基础镜像选择

dockerfile
# 基于 PHP 8.3 + Swoole 的官方镜像
FROM phpswoole/swoole:php8.3-alpine

# 设置工作目录
WORKDIR /var/www/app

# 安装系统依赖
RUN apk add --no-cache 
    linux-headers 
    zlib-dev 
    libzip-dev 
    oniguruma-dev 
    && docker-php-ext-install -j$(nproc) 
    pdo_mysql 
    mysqli 
    opcache 
    bcmath 
    zip 
    mbstring 
    pcntl 
    && pecl install redis-6.0.2 
    && docker-php-ext-enable redis

# 复制 composer 文件并安装依赖
COPY composer.json composer.lock ./
RUN composer install --no-dev --no-interaction --optimize-autoloader

# 复制应用代码
COPY . .

# 创建运行时目录
RUN mkdir -p runtime/cache runtime/logs && 
    chown -R www-data:www-data /var/www/app

# 设置环境变量
ENV APP_ENV=production
ENV APP_DEBUG=false

# 暴露端口
EXPOSE 9501

# 启动命令
CMD ["php", "viswoole", "server:start"]

多阶段构建(减小镜像体积)

dockerfile
# ============================================
# 阶段一: Composer 依赖安装
# ============================================
FROM composer:2 AS composer-stage

WORKDIR /build

COPY composer.json composer.lock ./

RUN composer install --no-dev --no-interaction --prefer-dist --optimize-autoloader

# ============================================
# 阶段二:最终镜像
# ============================================
FROM phpswoole/swoole:php8.3-alpine AS production

# 安装运行时依赖
RUN apk add --no-cache 
    libzip-dev 
    curl 
    tzdata 
    && cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime 
    && echo "Asia/Shanghai" > /etc/timezone 
    && docker-php-ext-install -j$(nproc) 
    pdo_mysql 
    opcache 
    zip 
    bcmath 
    && pecl install redis-6.0.2 
    && docker-php-ext-enable redis

# 从阶段一复制 vendor 目录
COPY --from=composer-stage /build/vendor ./vendor

# 复制应用代码
WORKDIR /var/www/app
COPY . .

# 清理不需要的文件
RUN rm -rf .git .github .env.example tests phpunit.xml docker-compose*.yml 
    && chmod -R 755 /var/www/app 
    && chown -R www-data:www-data /var/www/app

USER www-data

ENV APP_ENV=production
ENV APP_DEBUG=false

EXPOSE 9501

HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 
    CMD curl -f http://localhost:9501/health || exit 1

CMD ["php", "viswoole", "server:start"]

Docker Compose 编排

基础编排(单机部署)

yaml
# docker-compose.yml
services:
  # Viswoole 应用
  app:
    build:
      context: .
      dockerfile: Dockerfile
    container_name: viswoole-app
    restart: unless-stopped
    ports:
      - '#123;APP_PORT:-9501}:9501'
    environment:
      - APP_ENV=#123;APP_ENV:-production}
      - APP_DEBUG=#123;APP_DEBUG:-false}
      - DATABASE_HOST=db
      - DATABASE_PORT=3306
      - DATABASE_NAME=#123;DATABASE_NAME:-viswoole}
      - DATABASE_USER=#123;DATABASE_USER:-root}
      - DATABASE_PASSWORD=#123;DATABASE_PASSWORD:-secret}
      - REDIS_HOST=redis
      - REDIS_PORT=6379
      - REDIS_PASSWORD=#123;REDIS_PASSWORD:-}
      - CACHE_STORE=redis
      - WORKER_NUM=#123;WORKER_NUM:-4}
      - TASK_WORKER_NUM=#123;TASK_WORKER_NUM:-2}
      - SWOOLE_DAEMONIZE=false
      - LOG_FILE=/var/log/app/swoole.log
    volumes:
      - ./runtime:/var/www/app/runtime:rw
      - app-logs:/var/log/app:rw
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_healthy
    networks:
      - app-network
    healthcheck:
      test: ['CMD', 'curl', '-f', 'http://localhost:9501/health']
      interval: 30s
      timeout: 5s
      retries: 3
    deploy:
      resources:
        limits:
          memory: 512M
          cpus: '2'
        reservations:
          memory: 256M
          cpus: '1'

  # MySQL 数据库
  db:
    image: mysql:8.0
    container_name: viswoole-db
    restart: unless-stopped
    environment:
      MYSQL_ROOT_PASSWORD: #123;DATABASE_ROOT_PASSWORD:-root_secret}
      MYSQL_DATABASE: #123;DATABASE_NAME:-viswoole}
      MYSQL_USER: #123;DATABASE_USER:-app_user}
      MYSQL_PASSWORD: #123;DATABASE_PASSWORD:-secret}
      MYSQL_CHARACTER_SET_SERVER: utf8mb4
      MYSQL_COLLATION_SERVER: utf8mb4_unicode_ci
    ports:
      - '#123;DATABASE_PORT:-3306}:3306'
    volumes:
      - mysql_data:/var/lib/mysql:rw
      - ./deploy/mysql/conf.d:/etc/mysql/conf.d:ro
      - ./deploy/mysql/init.d:/docker-entrypoint-initdb.d:ro
    networks:
      - app-network
    healthcheck:
      test:
        [
          'CMD',
          'mysqladmin',
          'ping',
          '-h',
          'localhost',
          '-u',
          'root',
          '-p#123;DATABASE_ROOT_PASSWORD:-root_secret}'
        ]
      interval: 10s
      timeout: 5s
      retries: 5

  # Redis 缓存
  redis:
    image: redis:7-alpine
    container_name: viswoole-redis
    restart: unless-stopped
    command: redis-server --requirepass #123;REDIS_PASSWORD:-} --maxmemory 256mb --maxmemory-policy allkeys-lru
    ports:
      - '#123;REDIS_PORT:-6379}:6379'
    volumes:
      - redis_data:/data:rw
    networks:
      - app-network
    healthcheck:
      test: ['CMD', 'redis-cli', '-a', '#123;REDIS_PASSWORD:-}', 'ping']
      interval: 10s
      timeout: 5s
      retries: 5

  # Nginx 反向代理(可选)
  nginx:
    image: nginx:alpine
    container_name: viswoole-nginx
    restart: unless-stopped
    ports:
      - '80:80'
      - '443:443'
    volumes:
      - ./deploy/nginx/nginx.conf:/etc/nginx/nginx.conf:ro
      - ./deploy/nginx/conf.d:/etc/nginx/conf.d:ro
      - ./deploy/nginx/ssl:/etc/nginx/ssl:ro
      - nginx_logs:/var/log/nginx:rw
    depends_on:
      - app
    networks:
      - app-network

volumes:
  mysql_data:
    driver: local
  redis_data:
    driver: local
  app-logs:
    driver: local
  nginx_logs:
    driver: local

networks:
  app-network:
    driver: bridge

多实例水平扩展

yaml
# docker-compose.scale.yml
services:
  app:
    build: .
    environment:
      - SERVER_PORT=9501
    deploy:
      replicas: 4 # 运行 4 个实例
      resources:
        limits:
          cpus: '1'
          memory: 512M
      restart_policy:
        condition: on-failure
        delay: 5s
        max_attempts: 3
      update_config:
        parallelism: 2
        delay: 10s
        failure_action: rollback

  # 使用 HAProxy 或 Nginx 做负载均衡
  loadbalancer:
    image: haproxy:alpine
    ports:
      - '80:80'
    volumes:
      - ./deploy/haproxy/haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:ro
    depends_on:
      - app

Kubernetes 部署

Deployment 配置

yaml
# k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: viswoole-app
  labels:
    app: viswoole
spec:
  replicas: 3
  selector:
    matchLabels:
      app: viswoole
  template:
    metadata:
      labels:
        app: viswoole
    spec:
      containers:
        - name: viswoole
          image: your-registry/viswoole-app:latest
          ports:
            - containerPort: 9501
          env:
            - name: APP_ENV
              value: 'production'
            - name: APP_DEBUG
              value: 'false'
            - name: DATABASE_HOST
              valueFrom:
                secretKeyRef:
                  name: app-secrets
                  key: db-host
            - name: DATABASE_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: app-secrets
                  key: db-password
            - name: REDIS_HOST
              value: 'redis-service'
            - name: WORKER_NUM
              value: '4'
            - name: SWOOLE_DAEMONIZE
              value: 'false'
          resources:
            requests:
              memory: '256Mi'
              cpu: '250m'
            limits:
              memory: '512Mi'
              cpu: '500m'
          livenessProbe:
            httpGet:
              path: /health
              port: 9501
            initialDelaySeconds: 15
            periodSeconds: 20
          readinessProbe:
            httpGet:
              path: /health
              port: 9501
            initialDelaySeconds: 5
            periodSeconds: 10
          volumeMounts:
            - name: app-logs
              mountPath: /var/log/app
      volumes:
        - name: app-logs
          emptyDir: {}
---
apiVersion: v1
kind: Service
metadata:
  name: viswoole-service
spec:
  selector:
    app: viswoole
  ports:
    - protocol: TCP
      port: 9501
      targetPort: 9501
  type: ClusterIP
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: viswoole-ingress
  annotations:
    nginx.ingress.kubernetes.io/proxy-body-size: '20m'
spec:
  ingressClassName: nginx
  rules:
    - host: api.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: viswoole-service
                port:
                  number: 9501

ConfigMap 与 Secret

yaml
# k8s/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config
data:
  APP_NAME: 'Viswoole Production'
  APP_URL: 'https://api.example.com'
  CACHE_STORE: 'redis'
  WORKER_NUM: '4'
---
apiVersion: v1
kind: Secret
metadata:
  name: app-secrets
type: Opaque
stringData:
  db-host: 'mysql-service'
  db-database: 'production_db'
  db-username: 'app_user'
  db-password: '<strong_password>'
  redis-password: '<redis_password>'
  app-key: '<generated_app_key>'

CI/CD 流水线

GitHub Actions 示例

yaml
# .github/workflows/deploy.yml
name: Deploy to Production

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: '8.3'
          extensions: swoole, redis, pdo_mysql, bcmath
          tools: composer:v2

      - name: Install dependencies
        run: composer install --no-dev --optimize-autoloader

      - name: Run tests
        run: vendor/bin/phpunit

      - name: Build Docker image
        run: docker build -t your-registry/viswoole:#123;{ github.sha }} .

      - name: Push to Registry
        run: |
          echo "#123;{ secrets.REGISTRY_PASSWORD }}" | docker login your-registry -u "#123;{ secrets.REGISTRY_USERNAME }}" --password-stdin
          docker push your-registry/viswoole:#123;{ github.sha }}

      - name: Deploy to Server
        uses: appleboy/ssh-action@v1
        with:
          host: #123;{ secrets.SERVER_HOST }}
          username: #123;{ secrets.SERVER_USER }}
          key: #123;{ secrets.SSH_PRIVATE_KEY }}
          script: |
            cd /opt/viswoole
            docker compose pull
            docker compose up -d --remove-orphans
            docker image prune -f

运维操作

常用 Docker 命令

bash
# 构建镜像
docker build -t viswoole-app:v1.0 .

# 启动服务
docker compose up -d

# 查看日志
docker compose logs -f app

# 进入容器
docker compose exec app bash

# 重启服务
docker compose restart app

# 平滑重载(零停机)
docker compose exec app php viswoole server:reload

# 扩缩容
docker compose up -d --scale app=4

# 查看资源使用
docker stats viswoole-app

# 清理无用资源
docker system prune -a

监控与排查

bash
# 查看容器状态
docker compose ps

# 查看健康检查
docker inspect --format='{{json .State.Health}}' viswoole-app

# 查看 Swoole 进程信息
docker compose exec app php --ri swoole

# 查看实时日志
docker compose logs -f --tail=100 app

# 查看资源统计
docker stats --no-stream

最佳实践

1. 镜像优化

  • 使用多阶段构建减少镜像体积
  • 利用 Docker 层缓存(先 COPY composer.json 再 COPY .)
  • 生产镜像中排除 .gittests.env.example 等非必需文件
  • 使用 Alpine 基础镜像减小体积

2. 安全实践

  • 不要在镜像中硬编码敏感信息,使用环境变量或 Secrets
  • 以非 root 用户运行容器
  • 只暴露必要的端口
  • 定期更新基础镜像以修复安全漏洞
  • 使用 .dockerignore 排除敏感文件
dockerfile
# .dockerignore
.git
.github
.env
.env.*
tests/
phpunit.xml
Dockerfile*
docker-compose*.yml
README.md
docs/
.vscode/
.idea/

3. 健康检查

始终为容器配置健康检查端点:

php
// app/Controller/HealthController.php
class HealthController extends Controller
{
    public function index(): array
    {
        // 检查关键依赖
        $checks = [
            'database' => $this->checkDatabase(),
            'redis' => $this->checkRedis(),
        ];

        $healthy = !in_array(false, array_values($checks));

        return json([
            'status' => $healthy ? 'ok' : 'degraded',
            'checks' => $checks,
            'timestamp' => time(),
        ], $healthy ? 200 : 503);
    }
}

4. 日志收集

将容器日志统一输出到 stdout/stderr,便于集中收集:

bash
# docker-compose.yml 中的日志配置
services:
  app:
    logging:
      driver: json-file
      options:
        max-size: "50m"
        max-file: "5"