Laravel 13 部署与运维完全指南
部署和运维是应用生命周期的重要环节。本文将深入探讨 Laravel 13 的部署策略和运维最佳实践。
环境配置
环境变量管理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| APP_NAME="My Application" APP_ENV=production APP_KEY=base64:your-app-key APP_DEBUG=false APP_URL=https:
DB_CONNECTION=mysql DB_HOST=127.0.0.1 DB_PORT=3306 DB_DATABASE=production_db DB_USERNAME=prod_user DB_PASSWORD=secure_password
CACHE_DRIVER=redis QUEUE_CONNECTION=redis SESSION_DRIVER=redis
REDIS_HOST=127.0.0.1 REDIS_PASSWORD=null REDIS_PORT=6379
|
配置缓存
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan event:cache
php artisan optimize:clear
|
部署流程
部署脚本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| #!/bin/bash
set -e
echo "开始部署..."
cd /var/www/app
php artisan down --message="系统升级中,请稍后..." --retry=60
git pull origin main
composer install --no-dev --optimize-autoloader
php artisan migrate --force
php artisan config:cache php artisan route:cache php artisan view:cache php artisan event:cache
npm install npm run build
php artisan queue:restart
php artisan up
echo "部署完成!"
|
零停机部署
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97
| <?php
namespace App\Console\Commands;
use Illuminate\Console\Command; use Illuminate\Support\Facades\Artisan;
class DeployCommand extends Command { protected $signature = 'app:deploy {--force}'; protected $description = '执行零停机部署';
public function handle(): int { $this->info('开始部署流程...');
$this->putIntoMaintenance(); $this->pullLatestCode(); $this->installDependencies(); $this->runMigrations(); $this->optimizeApplication(); $this->buildFrontend(); $this->restartServices(); $this->bringUpApplication();
$this->info('部署完成!');
return self::SUCCESS; }
protected function putIntoMaintenance(): void { $this->info('进入维护模式...'); Artisan::call('down', [ '--message' => '系统升级中', '--retry' => 60, ]); }
protected function pullLatestCode(): void { $this->info('拉取最新代码...'); exec('git pull origin main', $output, $return); if ($return !== 0) { throw new \Exception('Git pull failed'); } }
protected function installDependencies(): void { $this->info('安装依赖...'); exec('composer install --no-dev --optimize-autoloader', $output, $return); if ($return !== 0) { throw new \Exception('Composer install failed'); } }
protected function runMigrations(): void { $this->info('运行数据库迁移...'); Artisan::call('migrate', ['--force' => true]); }
protected function optimizeApplication(): void { $this->info('优化应用...'); Artisan::call('config:cache'); Artisan::call('route:cache'); Artisan::call('view:cache'); Artisan::call('event:cache'); }
protected function buildFrontend(): void { $this->info('构建前端资源...'); exec('npm install && npm run build', $output, $return); if ($return !== 0) { throw new \Exception('Frontend build failed'); } }
protected function restartServices(): void { $this->info('重启服务...'); Artisan::call('queue:restart'); exec('sudo supervisorctl restart all'); }
protected function bringUpApplication(): void { $this->info('退出维护模式...'); Artisan::call('up'); } }
|
Docker 部署
Dockerfile
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66
| FROM php:8.3-fpm-alpine
LABEL maintainer="your-email@example.com"
RUN apk add --no-cache \ nginx \ supervisor \ curl \ git \ zip \ unzip \ libzip-dev \ libpng-dev \ libjpeg-turbo-dev \ freetype-dev \ libxml2-dev \ oniguruma-dev \ nodejs \ npm
RUN docker-php-ext-configure gd --with-freetype --with-jpeg \ && docker-php-ext-install \ pdo_mysql \ mysqli \ zip \ gd \ mbstring \ xml \ bcmath \ opcache
RUN pecl install redis && docker-php-ext-enable redis
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
WORKDIR /var/www/html
COPY . .
RUN composer install --no-dev --optimize-autoloader --no-interaction
RUN npm install && npm run build
RUN chown -R www-data:www-data /var/www/html \ && chmod -R 755 /var/www/html/storage \ && chmod -R 755 /var/www/html/bootstrap/cache
COPY docker/nginx.conf /etc/nginx/http.d/default.conf COPY docker/supervisord.conf /etc/supervisor/conf.d/supervisord.conf COPY docker/php.ini /usr/local/etc/php/conf.d/custom.ini
EXPOSE 80
CMD ["supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"]
|
Docker Compose
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91
| version: '3.8'
services: app: build: context: . dockerfile: Dockerfile container_name: laravel_app restart: unless-stopped ports: - "80:80" environment: - APP_ENV=production - APP_DEBUG=false - DB_HOST=mysql - DB_DATABASE=${DB_DATABASE} - DB_USERNAME=${DB_USERNAME} - DB_PASSWORD=${DB_PASSWORD} - REDIS_HOST=redis volumes: - ./storage:/var/www/html/storage depends_on: - mysql - redis networks: - laravel_network
mysql: image: mysql:8.0 container_name: laravel_mysql restart: unless-stopped environment: MYSQL_DATABASE: ${DB_DATABASE} MYSQL_ROOT_PASSWORD: ${DB_PASSWORD} MYSQL_USER: ${DB_USERNAME} MYSQL_PASSWORD: ${DB_PASSWORD} volumes: - mysql_data:/var/lib/mysql networks: - laravel_network
redis: image: redis:alpine container_name: laravel_redis restart: unless-stopped volumes: - redis_data:/data networks: - laravel_network
queue: build: context: . dockerfile: Dockerfile container_name: laravel_queue restart: unless-stopped command: php artisan queue:work --sleep=3 --tries=3 environment: - APP_ENV=production - DB_HOST=mysql - REDIS_HOST=redis depends_on: - mysql - redis networks: - laravel_network
scheduler: build: context: . dockerfile: Dockerfile container_name: laravel_scheduler restart: unless-stopped command: php artisan schedule:work environment: - APP_ENV=production - DB_HOST=mysql - REDIS_HOST=redis depends_on: - mysql - redis networks: - laravel_network
networks: laravel_network: driver: bridge
volumes: mysql_data: redis_data:
|
Nginx 配置
生产环境配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69
| server { listen 80; listen [::]:80; server_name example.com www.example.com; return 301 https://$server_name$request_uri; }
server { listen 443 ssl http2; listen [::]:443 ssl http2; server_name example.com www.example.com;
root /var/www/app/public; index index.php;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256; ssl_prefer_server_ciphers off; ssl_session_cache shared:SSL:10m; ssl_session_timeout 1d; ssl_session_tickets off;
add_header X-Frame-Options "SAMEORIGIN" always; add_header X-Content-Type-Options "nosniff" always; add_header X-XSS-Protection "1; mode=block" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
gzip on; gzip_vary on; gzip_min_length 1024; gzip_proxied any; gzip_types text/plain text/css text/xml text/javascript application/javascript application/json application/xml;
location / { try_files $uri $uri/ /index.php?$query_string; }
location ~ \.php$ { fastcgi_pass unix:/run/php/php8.3-fpm.sock; fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; include fastcgi_params; fastcgi_hide_header X-Powered-By; fastcgi_buffer_size 16k; fastcgi_buffers 16 16k; }
location ~ /\.(?!well-known).* { deny all; }
location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2|ttf|eot)$ { expires 1y; add_header Cache-Control "public, immutable"; access_log off; }
location = /favicon.ico { log_not_found off; access_log off; }
location = /robots.txt { log_not_found off; access_log off; } }
|
监控与日志
健康检查端点
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67
| <?php
namespace App\Http\Controllers;
use Illuminate\Http\JsonResponse; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Redis;
class HealthController extends Controller { public function check(): JsonResponse { $checks = [ 'database' => $this->checkDatabase(), 'redis' => $this->checkRedis(), 'storage' => $this->checkStorage(), 'queue' => $this->checkQueue(), ];
$healthy = !in_array(false, $checks, true);
return response()->json([ 'status' => $healthy ? 'healthy' : 'unhealthy', 'checks' => $checks, 'timestamp' => now()->toIso8601String(), ], $healthy ? 200 : 503); }
protected function checkDatabase(): bool { try { DB::connection()->getPdo(); return true; } catch (\Exception $e) { return false; } }
protected function checkRedis(): bool { try { Redis::ping(); return true; } catch (\Exception $e) { return false; } }
protected function checkStorage(): bool { try { return is_writable(storage_path()); } catch (\Exception $e) { return false; } }
protected function checkQueue(): bool { try { $size = DB::table('jobs')->count(); return $size < 1000; } catch (\Exception $e) { return false; } } }
|
日志配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| return [ 'default' => env('LOG_CHANNEL', 'stack'),
'channels' => [ 'stack' => [ 'driver' => 'stack', 'channels' => ['daily', 'slack'], 'ignore_exceptions' => false, ],
'daily' => [ 'driver' => 'daily', 'path' => storage_path('logs/laravel.log'), 'level' => env('LOG_LEVEL', 'debug'), 'days' => 14, 'permission' => 0664, ],
'slack' => [ 'driver' => 'slack', 'url' => env('LOG_SLACK_WEBHOOK_URL'), 'username' => 'Laravel Log', 'emoji' => ':boom:', 'level' => 'critical', ],
'syslog' => [ 'driver' => 'syslog', 'level' => 'debug', 'facility' => LOG_USER, ], ], ];
|
备份策略
数据库备份
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63
| <?php
namespace App\Console\Commands;
use Illuminate\Console\Command; use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Process;
class BackupDatabase extends Command { protected $signature = 'db:backup {--keep=7 : 保留天数}'; protected $description = '备份数据库';
public function handle(): int { $filename = sprintf( 'backup_%s.sql.gz', now()->format('Y-m-d_His') );
$path = storage_path("backups/{$filename}");
$this->info('开始备份数据库...');
$command = sprintf( 'mysqldump -h%s -u%s -p%s %s | gzip > %s', config('database.connections.mysql.host'), config('database.connections.mysql.username'), config('database.connections.mysql.password'), config('database.connections.mysql.database'), $path );
Process::run($command);
if (!file_exists($path)) { $this->error('备份失败'); return self::FAILURE; }
Storage::disk('s3')->put("backups/{$filename}", file_get_contents($path));
$this->info("备份完成: {$filename}");
$this->cleanupOldBackups();
return self::SUCCESS; }
protected function cleanupOldBackups(): void { $keepDays = $this->option('keep'); $cutoff = now()->subDays($keepDays);
collect(Storage::disk('s3')->files('backups')) ->each(function ($file) use ($cutoff) { $time = Storage::disk('s3')->lastModified($file); if ($time < $cutoff->timestamp) { Storage::disk('s3')->delete($file); } }); } }
|
性能优化
OPcache 配置
1 2 3 4 5 6 7 8 9
| opcache.enable=1 opcache.memory_consumption=256 opcache.interned_strings_buffer=16 opcache.max_accelerated_files=10000 opcache.max_wasted_percentage=10 opcache.validate_timestamps=0 opcache.revalidate_freq=0 opcache.fast_shutdown=1
|
应用优化命令
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| <?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
class OptimizeApp extends Command { protected $signature = 'app:optimize'; protected $description = '优化应用性能';
public function handle(): int { $this->info('优化应用...');
$this->call('config:cache'); $this->call('route:cache'); $this->call('view:cache'); $this->call('event:cache');
$this->info('优化完成!');
return self::SUCCESS; } }
|
总结
Laravel 13 的部署与运维包括:
- 完善的环境配置管理
- 自动化部署流程
- Docker 容器化部署
- Nginx 生产配置
- 监控与健康检查
- 日志管理
- 备份策略
- 性能优化
良好的部署和运维实践是保证应用稳定运行的基础。