发布于2026-07-23 阅读(0)
扫一扫,手机访问
如果你的 PHP 部署流程还停留在这样一个阶段:
git pullcomposer installphp artisan migrate这个流程乍一看也能跑,但一旦遇到以下情况,就开始暴露问题了:
到了那个时候,部署就不再是一个简单的任务了——它变成了一种仪式,脆弱、没有文档,而且只有“知道确切步骤”的那个人才能搞定。
一个非常直接有效的解决方案,就是写一个 Shell 脚本。
你不需要 Kubernetes、Terraform,也不需要一整套 CI/CD 平台来实现真正的部署自动化。一个写得好的 shell 脚本可以做到:
这篇文章会从 PHP 开发者的角度出发,带你一步步吃透以下内容:
releases/ 和 current/ 符号链接组织部署读完这篇文章,你将拥有一个可以直接适配到自己项目中的部署脚本——无论是 Lara vel 项目、自定义 PHP 后端,还是其他业务系统,都能用上。
在写任何脚本之前,先搞清楚在你的场景下,“部署”具体意味着什么,这会非常有帮助。
在 Linux 服务器上,一个典型的 PHP 部署流程通常包含以下环节:
composer install --no-dev --optimize-autoloadernpm ci && npm run build.env 文件存在php artisan migrate --forcephp artisan config:cache、route:cache 等systemctl reload php-fpm)你的脚本的工作,就是把这一切用可靠、可重复的方式编码下来。
如果你对 PHP 很熟悉但对 shell 脚本还不太了解,别担心,下面这些 Bash 基础足够让你上手干活。
每个 shell 脚本的开头,都要有一行声明,告诉系统该用哪个解释器来执行。这行代码就是:
#!/usr/bin/env bash
有了它,你的脚本就可以像其他命令一样直接执行了。
创建一个文件:
nano deploy.sh
写入:
#!/usr/bin/env bashecho "Deploying PHP app..."
保存,然后:
chmod +x deploy.sh./deploy.sh
你应该会看到:
Deploying PHP app...
这样你的第一个 shell 脚本就跑起来了。
在脚本顶部(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 → 通用失败了解了这些基础,你就可以开始自动化真正的工作了。
从最常见的单服务器场景入手:
目录结构
先保持简单:
/var/www/myapp/
├── .git/
├── public/
├── vendor/
├── storage/
└── ...
部署流程:
git pullcomposer install这是一个最小脚本:
#!/usr/bin/env bashset -euo pipefailAPP_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 --allgit checkout "$BRANCH"git pull origin "$BRANCH" --ff-onlylog "Installing PHP dependencies with Composer..."COMPOSER_ALLOW_SUPERUSER=1 composer install \ --no-dev \ --prefer-dist \ --optimize-autoloader# 如果你用的是 Lara vel 或其他框架,添加框架特定的步骤:if [[ -f artisan ]]; then log "Running database migrations..." php artisan migrate --force log "Clearing and caching Lara vel configuration..." php artisan config:clear php artisan config:cache php artisan route:cache || true # route cache 在开发环境可能会失败filog "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' 的分支
这已经比手动运行每条命令好多了:
但它还可以大幅改进。
回过头来看,前面那个简单脚本其实存在一个很大的隐患:如果迁移挂了或者部署半途出问题,你唯一的回滚方式是“希望你有备份”。
现在开始给它加一些安全网。
在脚本顶部,在做任何危险操作之前,先检查:
示例:
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 bashset -euo pipefailAPP_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=5TIMESTAMP="$(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(Lara vel 用) 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_lara vel_tasks() { if [[ -f artisan ]]; then log "Running Lara vel migrations..." php artisan migrate --force log "Optimizing Lara vel 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_lara vel_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 和 uploadscurrent 切换指向新版本要手动回滚,你可以:
1.列出版本:
ls -1 /var/www/myapp/releases
2.把 current 指向旧版本:
ln -sfn /var/www/myapp/releases/2025-11-26-112030 /var/www/myapp/currentsudo systemctl reload php8.2-fpm
你甚至可以把回滚脚本化(比如“回到上一个版本”),通过检查 releases/ 目录来实现。
一般来说,一个完整的项目流程至少会涉及两个环境:
你可以重用同一个脚本,但按环境参数化。
扩展变量:
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 等。
在 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
这让环境配置保持干净和明确。
说起 shell 脚本,其实它更像一个调度中心,把各种你熟悉的工具按顺序安排起来:Composer、Artisan、cron、supervisord 等等。
你可以通过添加一些标志,让 Composer 运行得更快、更可预测:
run_composer() { COMPOSER_ALLOW_SUPERUSER=1 composer install \ --no-dev \ --prefer-dist \ --classmap-authoritative \ --no-interaction \ --no-progress}
如果你使用队列(比如 Lara vel 队列 worker 或 Horizon),部署后你可能需要重启 worker。
用 supervisor 管理的 Lara vel 队列 worker 示例:
restart_workers() { log "Restarting queue workers via supervisor..." sudo supervisorctl reread sudo supervisorctl update sudo supervisorctl restart all}
或者就:
php artisan queue:restart
把这加到你的 run_lara vel_tasks() 或单独的步骤里。
如果你依赖 cron 调用 php artisan schedule:run,不需要做特别的事——cron 会在下次运行时自动使用新的 current 符号链接。
只要确保你的 cron 条目指向 current 路径,而不是特定的版本:
* * * * * cd /var/www/myapp/current && php artisan schedule:run >> /dev/null 2>&1
脚本写好了,在服务器上能跑通,接下来就是接入 CI 了。这一步其实比想象中简单。
一个非常简化的工作流:
# .github/workflows/deploy.ymlname: Deploy to Productionon: push: branches: - mainjobs: 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.ymlstages: - deploydeploy_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 都重定向到日志文件(同时仍然打印到终端)。
现在每次运行都会被记录,包括错误信息。
你可以用 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' ERRtrap 'notify_slack success' EXIT
现在每当部署运行时你的团队都会收到消息。
(实际上你可能需要比“EXIT 时总是 success”更精细的控制,不过这里先这样。)
当然,Shell 脚本虽然是很好的起点,但你也需要清楚它的局限性。
在那些场景里,shell 脚本仍然有用——但它们通常变成胶水代码,而不是主要的部署机制。
好消息是:你现在编码的部署逻辑(运行什么、按什么顺序、什么必须成功)即使你后来转向 Deployer、GitHub Actions 工作流、Ansible 或任何其他工具,也依然有价值。这些精力不会白费,你是在把部署流程文档化。
最后,我们来整体回顾一下,这篇文章到底构建了什么:
1. 你学习了专门用于部署的 shell 脚本基础:
#!/usr/bin/env bash、set -euo pipefail、函数、参数
2. 你从一个简单脚本开始:
git pullcomposer install3. 然后你把它演进成了一个更健壮的系统,使用:
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/ 结构演进,以获得更安全的部署和回滚
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
正版软件
正版软件
正版软件
正版软件
正版软件
1
2
3
7
8