传统部署方式的困境

如果你的 PHP 部署流程还停留在这样一个阶段:

这个流程乍一看也能跑,但一旦遇到以下情况,就开始暴露问题了:

到了那个时候,部署就不再是一个简单的任务了——它变成了一种仪式,脆弱、没有文档,而且只有“知道确切步骤”的那个人才能搞定。

一个非常直接有效的解决方案,就是写一个 Shell 脚本。

你不需要 Kubernetes、Terraform,也不需要一整套 CI/CD 平台来实现真正的部署自动化。一个写得好的 shell 脚本可以做到:

这篇文章会从 PHP 开发者的角度出发,带你一步步吃透以下内容:

读完这篇文章,你将拥有一个可以直接适配到自己项目中的部署脚本——无论是 Lara vel 项目、自定义 PHP 后端,还是其他业务系统,都能用上。

PHP 应用部署的核心步骤

在写任何脚本之前,先搞清楚在你的场景下,“部署”具体意味着什么,这会非常有帮助。

在 Linux 服务器上,一个典型的 PHP 部署流程通常包含以下环节:

获取代码

安装依赖

准备环境

运行维护任务

切换版本并重启服务

可选:回滚

你的脚本的工作,就是把这一切用可靠、可重复的方式编码下来。

Shell 脚本入门(PHP 开发者视角)

如果你对 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 脚本就跑起来了。

快速失败:set -euo pipefail

在脚本顶部(shebang 之后),加上:

set -euo pipefail

这行代码做了三件重要的事:

这就像告诉你的脚本:“如果出了任何问题,就停下来。别继续跑然后假装一切正常。”

变量和参数

基本变量:

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

这种模式能让你的脚本更易读、更好维护。

退出码

了解了这些基础,你就可以开始自动化真正的工作了。

构建简单的 PHP 部署脚本(单服务器

从最常见的单服务器场景入手:

目录结构

先保持简单:

/var/www/myapp/

├── .git/

├── public/

├── vendor/

├── storage/

└── ...

部署流程:

这是一个最小脚本:

#!/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 符号链接指向当前活跃的版本

部署时:

目录结构:

/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

这个脚本的工作流程:

要手动回滚,你可以:

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/ 目录来实现。

多环境部署(staging 与 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

如果你使用队列(比如 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 任务

如果你依赖 cron 调用 php artisan schedule:run,不需要做特别的事——cron 会在下次运行时自动使用新的 current 符号链接。

只要确保你的 cron 条目指向 current 路径,而不是特定的版本:

* * * * * cd /var/www/myapp/current && php artisan schedule:run >> /dev/null 2>&1

接入 CI/CD 系统

脚本写好了,在服务器上能跑通,接下来就是接入 CI 了。这一步其实比想象中简单。

GitHub Actions 通过 SSH 部署

一个非常简化的工作流:

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

# .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 都重定向到日志文件(同时仍然打印到终端)。

现在每次运行都会被记录,包括错误信息。

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' ERRtrap 'notify_slack success' EXIT

现在每当部署运行时你的团队都会收到消息。

(实际上你可能需要比“EXIT 时总是 success”更精细的控制,不过这里先这样。)

Shell 脚本的局限与进阶方向

当然,Shell 脚本虽然是很好的起点,但你也需要清楚它的局限性。

Shell 脚本的适用场景

需要进阶的场景

在那些场景里,shell 脚本仍然有用——但它们通常变成胶水代码,而不是主要的部署机制。

好消息是:你现在编码的部署逻辑(运行什么、按什么顺序、什么必须成功)即使你后来转向 Deployer、GitHub Actions 工作流、Ansible 或任何其他工具,也依然有价值。这些精力不会白费,你是在把部署流程文档化。

总结

最后,我们来整体回顾一下,这篇文章到底构建了什么:

1. 你学习了专门用于部署的 shell 脚本基础:

#!/usr/bin/env bashset -euo pipefail、函数、参数

2. 你从一个简单脚本开始:

3. 然后你把它演进成了一个更健壮的系统,使用:

你让它具有环境感知,用单个脚本处理 staging 和 production。

你集成了 PHP 生态工具,如 Composer、Artisan、队列和 cron。

你把脚本接入了 CI,这样部署就变成了 push + 流水线,而不是“SSH 然后祈祷”。

你添加了日志和通知,这样部署就不是黑盒了。

4. 结果看起来很简单:

./deploy.sh production

但在这一条命令背后,是一套清晰的、受版本控制的流程,完整定义了你的 PHP 应用如何从 Git 到达线上服务器

你不需要一次性采纳这篇文章里的每个想法。一个你可以遵循的进阶路径:

本文转载于:https://www.jb51.net/program/354535z9n.htm 如有侵犯,请联系zhengruancom@outlook.com删除。
免责声明:正软商城发布此文仅为传递信息,不代表正软商城认同其观点或证实其描述。