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
// .env.production
APP_NAME="My Application"
APP_ENV=production
APP_KEY=base64:your-app-key
APP_DEBUG=false
APP_URL=https://example.com

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
# deploy.sh

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

# 安装 PHP 扩展
RUN docker-php-ext-configure gd --with-freetype --with-jpeg \
&& docker-php-ext-install \
pdo_mysql \
mysqli \
zip \
gd \
mbstring \
xml \
bcmath \
opcache

# 安装 Redis
RUN pecl install redis && docker-php-ext-enable redis

# 安装 Composer
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
// config/logging.php
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
; php.ini
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 生产配置
  • 监控与健康检查
  • 日志管理
  • 备份策略
  • 性能优化

良好的部署和运维实践是保证应用稳定运行的基础。