如何创建和使用 Shell 脚本实现 PHP 部署自动化
如何创建和使用 Shell 脚本实现 PHP 部署自动化
传统部署方式的困境
如果你的 PHP 部署流程是这样的:
- SSH 登录服务器
git pullcomposer install- 可能跑一下
php artisan migrate - 清一些缓存
- 重载 PHP-FPM 或 nginx
- 双手合十祈祷
这个流程能跑,直到:
- 你要管理多台服务器
- 你需要快速回滚
- 你忘了某个小步骤,然后生产环境炸了
- 团队里其他人的操作方式跟你不太一样
到那时候,部署就不再是一个任务了——它变成了一种仪式,脆弱、没有文档、而且只有"知道确切步骤"的那个人才能搞定。
Shell 脚本是解决这个问题的一种非常简单的方式。
你不需要 Kubernetes、Terraform,也不需要一整套 CI/CD 平台来实现真正的部署自动化。一个写得好的 shell 脚本可以:
- 把 15 条手动命令变成一条可重复执行的命令
- 让你的部署过程用代码记录下来
- 减少人为错误和"哎呀,我忘了清缓存"的时刻
- 成为后续 CI/CD 流水线的构建基础
在这篇文章中,我们将讲解:
- Shell 脚本基础(从 PHP 开发者的角度)
- 为 PHP 应用构建一个简单的部署脚本
- 用安全检查、日志、回滚来改进它
- 用
releases/和current/符号链接组织部署 - 把脚本接入 Git 或 CI
读完之后,你会有一个可以适配到自己应用的部署脚本——无论是 Laravel 项目、自定义 PHP 后端,还是其他业务系统。
原文链接 如何创建和使用 Shell 脚本实现 PHP 部署自动化
PHP 应用部署的核心步骤
在写任何脚本之前,先搞清楚在你的场景下"部署"具体意味着什么会很有帮助。
在 Linux 服务器上,一个典型的 PHP 部署可能需要:
获取代码
- 克隆仓库(或拉取最新更改)
- 切换到特定的分支或标签
安装依赖
composer install --no-dev --optimize-autoloader- (可选)前端:
npm ci && npm run build
准备环境
- 确保
.env文件存在 - 链接共享目录(上传文件、storage、日志)
运行维护任务
- 数据库迁移:
php artisan migrate --force - 清除/优化缓存:
php artisan config:cache、route:cache等
切换版本并重启服务
- 更新符号链接指向新版本
- 重载 PHP-FPM(
systemctl reload php-fpm) - 可能需要重启队列 worker
可选:回滚
- 如果出问题,能够切换回上一个版本
你脚本的工作就是把这一切用可靠、可重复的方式编码下来。
Shell 脚本入门(PHP 开发者视角)
如果你对 PHP 很熟悉但对 shell 脚本还不太了解,这里有足够的 Bash 基础让你能上手干活。
每个 shell 脚本都应该以一行开头,告诉系统用什么解释器:
#!/usr/bin/env bash
这让你的脚本可以像其他命令一样执行。
让脚本可执行
创建一个文件:
nano deploy.sh
写入:
#!/usr/bin/env bash
echo "Deploying PHP app..."
保存,然后:
chmod +x deploy.sh
./deploy.sh
你应该会看到:
Deploying PHP app...
这样你的第一个 shell 脚本就跑起来了。
快速失败:set -euo pipefail
在脚本顶部(shebang 之后),加上:
set -euo pipefail
这做了三件重要的事:
-e:如果任何命令返回非零退出码,脚本就退出-u:把未设置的变量当作错误-o pipefail:如果管道cmd1 | cmd2中cmd1失败了,整个管道都算失败
这就像告诉你的脚本:"如果出了任何问题,就停下来。别继续跑然后假装一切正常。"
变量和参数
基本变量:
APP_NAME="my-php-app"
REPO_URL="git@github.com:yourname/your-app.git"
访问位置参数:
ENVIRONMENT="${1:-production}" # 如果没提供参数,默认是 production
运行:
./deploy.sh staging
在脚本里,$ENVIRONMENT 就是 staging。
函数
你可以用函数来组织脚本:
deploy() {
echo "Deploying to environment: $ENVIRONMENT"
}
rollback() {
echo "Rolling back..."
}
调用它们:
case "${1:-deploy}" in
deploy)
deploy
;;
rollback)
rollback
;;
*)
echo "Usage: $0 [deploy|rollback]"
exit 1
;;
esac
这种模式让你的脚本更易读、更好维护。
退出码
exit 0→ 成功exit 1→ 通用失败- 其他代码可以表示特定错误(可选但挺好)
知道了这些基础,你就可以开始自动化真正的工作了。
构建简单的 PHP 部署脚本(单服务器)
让我们从一个直接的场景开始:
- 单台 Linux 服务器(比如 Ubuntu)
- Nginx + PHP-FPM
- PHP 应用在
/var/www/myapp - 你通过 SSH 登录服务器然后运行
./deploy.sh来部署
目录结构
我们先保持简单:
/var/www/myapp/
├── .git/
├── public/
├── vendor/
├── storage/
└── ...
部署流程:
git pullcomposer install- 清缓存
- 重载 PHP-FPM
这是一个最小脚本:
#!/usr/bin/env bash
set -euo pipefail
APP_DIR="/var/www/myapp"
PHP_FPM_SERVICE="php8.2-fpm" # 根据你的 PHP 版本调整
BRANCH="${1:-main}" # 默认分支
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"
}
cd "$APP_DIR"
log "Fetching latest code from branch '$BRANCH'..."
git fetch --all
git checkout "$BRANCH"
git pull origin "$BRANCH" --ff-only
log "Installing PHP dependencies with Composer..."
COMPOSER_ALLOW_SUPERUSER=1 composer install \
--no-dev \
--prefer-dist \
--optimize-autoloader
# 如果你用的是 Laravel 或其他框架,添加框架特定的步骤:
if [[ -f artisan ]]; then
log "Running database migrations..."
php artisan migrate --force
log "Clearing and caching Laravel configuration..."
php artisan config:clear
php artisan config:cache
php artisan route:cache || true # route cache 在开发环境可能会失败
fi
log "Reloading PHP-FPM service..."
sudo systemctl reload "$PHP_FPM_SERVICE"
log "Deployment completed successfully."
用法:
chmod +x deploy.sh
./deploy.sh # 部署 main 分支
./deploy.sh production # 如果你想用名为 'production' 的分支
这已经比手动运行每条命令好多了:
- 步骤在脚本里清晰可见
- 如果出问题会快速失败
- 任何有权限的人都能运行同样的流程
但我们可以大幅改进它。
增强安全性:备份、检查与回滚
上面的简单脚本有个大问题:如果迁移挂了或者部署半途出问题,你唯一的回滚方式是"希望你有备份"。
让我们开始加安全网。
开头的健全性检查
在脚本顶部,在做任何危险操作之前,检查:
- 你没有意外地在错误的服务器上运行
- 必需的二进制文件存在(git、composer、php、systemctl)
示例:
check_requirements() {
local bins=("git" "composer" "php" "systemctl")
for bin in "${bins[@]}"; do
if ! command -v "$bin" >/dev/null 2>&1; then
echo "Error: required binary '$bin' not found in PATH."
exit 1
fi
done
}
尽早调用 check_requirements:
check_requirements
你也可以断言环境:
if [[ "$(hostname)" != "prod-app-1" ]]; then
echo "Warning: this does not look like the production server ($(hostname))."
# sleep 5 或者 exit;你自己选
fi
数据库备份(可选但推荐)
对于小型系统,你可以在迁移之前快速做个数据库备份:
backup_database() {
local backup_dir="/var/backups/myapp"
mkdir -p "$backup_dir"
local filename="${backup_dir}/db-$(date '+%Y%m%d-%H%M%S').sql.gz"
log "Creating database backup at $filename..."
# MySQL 示例 - 调整凭据
mysqldump -u myuser -p'mypassword' mydatabase | gzip > "$filename"
}
在迁移之前调用 backup_database。
(正式环境一般会用托管备份,这里只是展示思路。)
回滚策略(基础)
在非常简单的设置上(没有 releases 目录),回滚很棘手。这就是为什么很多团队会转向 releases + 符号链接的模式,我们接下来会讲。
现在只需要知道:最好的回滚策略是避免就地修改"当前"代码。相反,你把新代码部署到一个单独的目录,然后在一切通过健康检查后切换符号链接。
让我们进入那个模式。
基于版本目录的零停机部署
一个非常常见的部署模式(受 Capistrano、Envoyer、Deployer 等工具启发)是:
- 在
releases/中保留多个应用版本 - 有一个
current符号链接指向当前活跃的版本 - 部署时:
- 创建一个新的
releases/20251127-153000/目录 - 在那里安装代码 + 依赖
- 运行迁移、构建资源等
- 更新
current指向新版本 - 可选保留几个旧版本用于回滚
- 创建一个新的
目录结构:
/var/www/myapp/
├── releases/
│ ├── 2025-11-27-153000/
│ └── 2025-11-26-112030/
├── shared/
│ ├── .env
│ ├── storage/
│ └── uploads/
└── current -> releases/2025-11-27-153000/
Nginx 指向 /var/www/myapp/current/public。
使用 releases 的部署脚本
这是一个使用这种模式的更高级脚本:
#!/usr/bin/env bash
set -euo pipefail
APP_NAME="myapp"
BASE_DIR="/var/www/${APP_NAME}"
RELEASES_DIR="${BASE_DIR}/releases"
SHARED_DIR="${BASE_DIR}/shared"
CURRENT_LINK="${BASE_DIR}/current"
REPO_URL="git@github.com:yourname/your-app.git"
PHP_FPM_SERVICE="php8.2-fpm"
KEEP_RELEASES=5
TIMESTAMP="$(date '+%Y-%m-%d-%H%M%S')"
NEW_RELEASE_DIR="${RELEASES_DIR}/${TIMESTAMP}"
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"
}
run_composer() {
COMPOSER_ALLOW_SUPERUSER=1 composer install \
--no-dev \
--prefer-dist \
--optimize-autoloader
}
link_shared() {
log "Linking shared files and directories..."
# 链接 .env
if [[ -f "${SHARED_DIR}/.env" ]]; then
ln -s "${SHARED_DIR}/.env" "${NEW_RELEASE_DIR}/.env"
fi
# 链接 storage(Laravel 用)
if [[ -d "${SHARED_DIR}/storage" ]]; then
rm -rf "${NEW_RELEASE_DIR}/storage"
ln -s "${SHARED_DIR}/storage" "${NEW_RELEASE_DIR}/storage"
fi
# 链接 uploads 或其他共享资源
if [[ -d "${SHARED_DIR}/uploads" ]]; then
mkdir -p "${NEW_RELEASE_DIR}/public"
ln -s "${SHARED_DIR}/uploads" "${NEW_RELEASE_DIR}/public/uploads"
fi
}
run_laravel_tasks() {
if [[ -f artisan ]]; then
log "Running Laravel migrations..."
php artisan migrate --force
log "Optimizing Laravel caches..."
php artisan config:clear
php artisan config:cache
php artisan route:cache || true
php artisan view:cache || true
fi
}
update_symlink() {
log "Updating current symlink to ${NEW_RELEASE_DIR}..."
ln -sfn "${NEW_RELEASE_DIR}" "${CURRENT_LINK}"
}
cleanup_old_releases() {
log "Cleaning up old releases, keeping last ${KEEP_RELEASES}..."
cd "${RELEASES_DIR}"
ls -1dt */ | tail -n +$((KEEP_RELEASES + 1)) | xargs -r rm -rf
}
deploy() {
log "Starting deployment to ${BASE_DIR}..."
mkdir -p "${RELEASES_DIR}" "${SHARED_DIR}"
log "Creating new release directory at ${NEW_RELEASE_DIR}..."
git clone --depth=1 "${REPO_URL}" "${NEW_RELEASE_DIR}"
cd "${NEW_RELEASE_DIR}"
log "Installing composer dependencies..."
run_composer
link_shared
run_laravel_tasks
update_symlink
log "Reloading PHP-FPM..."
sudo systemctl reload "${PHP_FPM_SERVICE}"
cleanup_old_releases
log "Deployment finished successfully. New release: ${TIMESTAMP}"
}
deploy
这个脚本做了什么:
- 把你的仓库克隆到一个带时间戳的文件夹
- 在那里安装依赖
- 从
shared/目录链接.env、storage和uploads - 运行迁移和缓存(Laravel 用)
- 把
current切换指向新版本 - 清理旧版本以免撑爆磁盘
要手动回滚,你可以:
- 列出版本:
ls -1 /var/www/myapp/releases
- 把
current指向旧版本:
ln -sfn /var/www/myapp/releases/2025-11-26-112030 /var/www/myapp/current
sudo systemctl reload php8.2-fpm
你甚至可以把回滚脚本化(比如"回到上一个版本"),通过检查 releases/ 目录来实现。
多环境部署(staging 与 production)
大多数团队至少有:
- staging(或 test)
- production
你可以重用同一个脚本,但按环境参数化。
使用 ENV 参数
扩展变量:
ENVIRONMENT="${1:-production}"
case "$ENVIRONMENT" in
production)
BASE_DIR="/var/www/myapp"
PHP_FPM_SERVICE="php8.2-fpm"
REPO_URL="git@github.com:yourname/your-app.git"
;;
staging)
BASE_DIR="/var/www/myapp-staging"
PHP_FPM_SERVICE="php8.2-fpm"
REPO_URL="git@github.com:yourname/your-app.git"
;;
*)
echo "Unknown environment: $ENVIRONMENT"
exit 1
;;
esac
然后调用:
./deploy.sh staging
./deploy.sh production
在脚本内部,其他所有东西都用 $BASE_DIR、$REPO_URL 等。
为每个环境使用不同的 .env 文件
在 shared/ 里,你可以有:
/var/www/myapp/shared/
├── .env.production
└── .env.staging
然后在 link_shared() 里:
ENV_FILE="${SHARED_DIR}/.env.${ENVIRONMENT}"
if [[ -f "${ENV_FILE}" ]]; then
ln -s "${ENV_FILE}" "${NEW_RELEASE_DIR}/.env"
else
echo "Warning: env file ${ENV_FILE} not found."
fi
这让环境配置保持干净和明确。
集成 PHP 生态工具
你的 shell 脚本通常会编排你已经熟悉的工具:Composer、Artisan、cron、supervisord 等。
优化 Composer
你可以通过添加标志让 Composer 更快更可预测:
run_composer() {
COMPOSER_ALLOW_SUPERUSER=1 composer install \
--no-dev \
--prefer-dist \
--classmap-authoritative \
--no-interaction \
--no-progress
}
处理队列和 worker
如果你使用队列(比如 Laravel 队列 worker 或 Horizon),部署后你可能需要重启 worker。
用 supervisor 管理的 Laravel 队列 worker 示例:
restart_workers() {
log "Restarting queue workers via supervisor..."
sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl restart all
}
或者就:
php artisan queue:restart
把这加到你的 run_laravel_tasks() 或单独的步骤里。
Cron 任务
如果你依赖 cron 调用 php artisan schedule:run,不需要做特别的事——cron 会在下次运行时自动使用新的 current 符号链接。
只要确保你的 cron 条目指向 current 路径,而不是特定的版本:
* * * * * cd /var/www/myapp/current && php artisan schedule:run >> /dev/null 2>&1
接入 CI/CD 系统
一旦你的脚本在 SSH 上可靠运行,集成到 CI 就很简单。
GitHub Actions 通过 SSH 部署
一个非常简化的工作流:
# .github/workflows/deploy.yml
name: Deploy to Production
on:
push:
branches:
- main
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Add SSH key
uses: webfactory/ssh-agent@v0.9.0
with:
ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}
- name: Deploy via SSH
run: |
ssh -o StrictHostKeyChecking=no deploy@your-server.com \
"cd /var/www/myapp && ./deploy.sh production"
CI 不需要知道你的部署逻辑;它只需要运行你的脚本。
GitLab CI
# .gitlab-ci.yml
stages:
- deploy
deploy_production:
stage: deploy
only:
- main
script:
- ssh deploy@your-server.com "cd /var/www/myapp && ./deploy.sh production"
shell 脚本就是部署流程的唯一规范。
日志、通知与故障排查
一个静默失败的部署脚本和手动部署一样可怕。
记录日志到文件
你可以用一个简单的日志机制包装你的脚本:
LOG_DIR="${BASE_DIR}/logs"
LOG_FILE="${LOG_DIR}/deploy-$(date '+%Y-%m-%d').log"
mkdir -p "$LOG_DIR"
# 在最顶部(在其他所有东西之前):
exec > >(tee -a "$LOG_FILE") 2>&1
这会把 stdout 和 stderr 都重定向到日志文件(同时仍然打印到终端)。
现在每次运行都会被记录,包括错误信息。
Slack/Discord 通知
你可以用 curl 在部署成功或失败后发送一个简单的 webhook。
Slack webhook 调用示例:
notify_slack() {
local status="$1" # "success" 或 "failure"
local webhook_url="https://hooks.slack.com/services/XXX/YYY/ZZZ"
local emoji=":white_check_mark:"
if [[ "$status" == "failure" ]]; then
emoji=":x:"
fi
curl -X POST -H 'Content-type: application/json' \
--data "{
\"text\": \"${emoji} Deploy ${status} for ${APP_NAME} on $(hostname) at $(date '+%Y-%m-%d %H:%M:%S')\"
}" \
"$webhook_url" >/dev/null 2>&1 || true
}
然后使用 Bash trap:
trap 'notify_slack failure' ERR
trap 'notify_slack success' EXIT
现在每当部署运行时你的团队都会收到消息。
(实际上你可能需要比“EXIT 时总是 success”更精细的控制,不过这里先这样。)
Shell 脚本的局限与进阶方向
Shell 脚本是很好的第一步,但你应该知道它们的局限。
Shell 脚本的适用场景
- 单台或少量服务器
- 简单的部署拓扑(一个应用,一个数据库)
- 你的团队主要是后端开发和系统管理员
- 你想要快速、易懂的自动化,不需要额外工具
需要进阶的场景
- 很多服务器,复杂的环境
- 复杂的网络、负载均衡、蓝绿部署
- 基础设施即代码(Terraform、Ansible 等)
- Kubernetes 或容器编排
在那些世界里,shell 脚本仍然有用——但它们通常变成胶水代码,而不是主要的部署机制。
好消息是:你现在编码的部署逻辑(运行什么、按什么顺序、什么必须成功)如果你后来转向 Deployer、GitHub Actions 工作流、Ansible 或任何其他工具,仍然是有价值的。这些精力不会白费,你是在把部署流程文档化。
总结
让我们回顾一下我们构建了什么:
1. 你学习了专门用于部署的 shell 脚本基础:
#!/usr/bin/env bash、set -euo pipefail、函数、参数
2. 你从一个简单脚本开始:
git pullcomposer install- 运行迁移和缓存任务
- 重载 PHP-FPM
3. 然后你把它演进成了一个更健壮的系统,使用:
releases/目录和current符号链接- 用于
.env、storage和uploads的共享目录 - 自动清理旧版本
你让它具有环境感知,用单个脚本处理 staging 和 production。
你集成了 PHP 生态工具,如 Composer、Artisan、队列和 cron。
你把脚本接入了 CI,这样部署就变成了 push + 流水线,而不是"SSH 然后祈祷"。
你添加了日志和通知,这样部署就不是黑盒了。
4. 结果看起来很简单:
./deploy.sh production
但在这一条命令背后,是一套清晰的、受版本控制的流程,完整定义了你的 PHP 应用如何从 Git 到达线上服务器。
你不需要一次性采纳这篇文章里的每个想法。一个你可以遵循的不错的进阶路径:
- 从一个小小的
deploy.sh开始,只是把你当前的手动步骤包装起来 - 添加
set -euo pipefail和一些基本的日志 - 向
releases/+current/结构演进,以获得更安全的部署和回滚 - 按环境参数化
- 最后,把脚本接入 CI

浙公网安备 33010602011771号