如何创建和使用 Shell 脚本实现 PHP 部署自动化

如何创建和使用 Shell 脚本实现 PHP 部署自动化

传统部署方式的困境

如果你的 PHP 部署流程是这样的:

  • SSH 登录服务器
  • git pull
  • composer 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:cacheroute:cache

切换版本并重启服务

  • 更新符号链接指向新版本
  • 重载 PHP-FPM(systemctl reload php-fpm
  • 可能需要重启队列 worker

可选:回滚

  • 如果出问题,能够切换回上一个版本

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

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

如果你对 PHP 很熟悉但对 shell 脚本还不太了解,这里有足够的 Bash 基础让你能上手干活。

每个 shell 脚本都应该以一行开头,告诉系统用什么解释器:

bash 复制代码
#!/usr/bin/env bash

这让你的脚本可以像其他命令一样执行。

让脚本可执行

创建一个文件:

bash 复制代码
nano deploy.sh

写入:

bash 复制代码
#!/usr/bin/env bash
echo "Deploying PHP app..."

保存,然后:

bash 复制代码
chmod +x deploy.sh
./deploy.sh

你应该会看到:

复制代码
Deploying PHP app...

这样你的第一个 shell 脚本就跑起来了。

快速失败:set -euo pipefail

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

bash 复制代码
set -euo pipefail

这做了三件重要的事:

  • -e:如果任何命令返回非零退出码,脚本就退出
  • -u:把未设置的变量当作错误
  • -o pipefail:如果管道 cmd1 | cmd2cmd1 失败了,整个管道都算失败

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

变量和参数

基本变量:

bash 复制代码
APP_NAME="my-php-app"
REPO_URL="git@github.com:yourname/your-app.git"

访问位置参数:

bash 复制代码
ENVIRONMENT="${1:-production}"  # 如果没提供参数,默认是 production

运行:

bash 复制代码
./deploy.sh staging

在脚本里,$ENVIRONMENT 就是 staging

函数

你可以用函数来组织脚本:

bash 复制代码
deploy() {
    echo "Deploying to environment: $ENVIRONMENT"
}

rollback() {
    echo "Rolling back..."
}

调用它们:

bash 复制代码
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/
  └── ...

部署流程:

  1. git pull
  2. composer install
  3. 清缓存
  4. 重载 PHP-FPM

这是一个最小脚本:

bash 复制代码
#!/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."

用法:

bash 复制代码
chmod +x deploy.sh
./deploy.sh              # 部署 main 分支
./deploy.sh production   # 如果你想用名为 'production' 的分支

这已经比手动运行每条命令好多了:

  • 步骤在脚本里清晰可见
  • 如果出问题会快速失败
  • 任何有权限的人都能运行同样的流程

但我们可以大幅改进它。

增强安全性:备份、检查与回滚

上面的简单脚本有个大问题:如果迁移挂了或者部署半途出问题,你唯一的回滚方式是"希望你有备份"。

让我们开始加安全网。

开头的健全性检查

在脚本顶部,在做任何危险操作之前,检查:

  • 你没有意外地在错误的服务器上运行
  • 必需的二进制文件存在(git、composer、php、systemctl)

示例:

bash 复制代码
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

bash 复制代码
check_requirements

你也可以断言环境:

bash 复制代码
if [[ "$(hostname)" != "prod-app-1" ]]; then
    echo "Warning: this does not look like the production server ($(hostname))."
    # sleep 5 或者 exit;你自己选
fi

数据库备份(可选但推荐)

对于小型系统,你可以在迁移之前快速做个数据库备份:

bash 复制代码
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 符号链接指向当前活跃的版本
  • 部署时:
    1. 创建一个新的 releases/20251127-153000/ 目录
    2. 在那里安装代码 + 依赖
    3. 运行迁移、构建资源等
    4. 更新 current 指向新版本
    5. 可选保留几个旧版本用于回滚

目录结构:

复制代码
/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 的部署脚本

这是一个使用这种模式的更高级脚本:

bash 复制代码
#!/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

这个脚本做了什么:

  1. 把你的仓库克隆到一个带时间戳的文件夹
  2. 在那里安装依赖
  3. shared/ 目录链接 .envstorageuploads
  4. 运行迁移和缓存(Laravel 用)
  5. current 切换指向新版本
  6. 清理旧版本以免撑爆磁盘

要手动回滚,你可以:

  1. 列出版本:
bash 复制代码
ls -1 /var/www/myapp/releases
  1. current 指向旧版本:
bash 复制代码
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 参数

扩展变量:

bash 复制代码
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

然后调用:

bash 复制代码
./deploy.sh staging
./deploy.sh production

在脚本内部,其他所有东西都用 $BASE_DIR$REPO_URL 等。

为每个环境使用不同的 .env 文件

shared/ 里,你可以有:

复制代码
/var/www/myapp/shared/
  ├── .env.production
  └── .env.staging

然后在 link_shared() 里:

bash 复制代码
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 更快更可预测:

bash 复制代码
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 示例:

bash 复制代码
restart_workers() {
    log "Restarting queue workers via supervisor..."
    sudo supervisorctl reread
    sudo supervisorctl update
    sudo supervisorctl restart all
}

或者就:

bash 复制代码
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 部署

一个非常简化的工作流:

yaml 复制代码
# .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

yaml 复制代码
# .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 脚本就是部署流程的唯一规范。

日志、通知与故障排查

一个静默失败的部署脚本和手动部署一样可怕。

记录日志到文件

你可以用一个简单的日志机制包装你的脚本:

bash 复制代码
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 调用示例:

bash 复制代码
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:

bash 复制代码
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 bashset -euo pipefail、函数、参数

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

  • git pull
  • composer install
  • 运行迁移和缓存任务
  • 重载 PHP-FPM

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

  • releases/ 目录和 current 符号链接
  • 用于 .envstorageuploads 的共享目录
  • 自动清理旧版本

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

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

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

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

4. 结果看起来很简单:

bash 复制代码
./deploy.sh production

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

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

  1. 从一个小小的 deploy.sh 开始,只是把你当前的手动步骤包装起来
  2. 添加 set -euo pipefail 和一些基本的日志
  3. releases/ + current/ 结构演进,以获得更安全的部署和回滚
  4. 按环境参数化
  5. 最后,把脚本接入 CI
相关推荐
徐行code4 小时前
C++ 核心机制深度解析:完美转发、值类别与 decltype
后端
回家路上绕了弯4 小时前
技术团队高效协作:知识分享与协作的落地实践指南
分布式·后端
真正的醒悟4 小时前
图解网络8
开发语言·网络·php
qq_348231854 小时前
Spring Boot 项目集成模块- 2
spring boot·后端
方圆想当图灵4 小时前
聊聊我为什么要写一个 MCP Server: Easy Code Reader
后端
落霞的思绪4 小时前
基于Go开发的矢量瓦片服务器——pg_tileserv
开发语言·后端·golang
武子康4 小时前
大数据-177 Elasticsearch 聚合实战:指标聚合 + 桶聚合完整用法与 DSL 解析
大数据·后端·elasticsearch
巴塞罗那的风5 小时前
经典Agent架构实战之反思模型(Reflection)
后端·语言模型·golang
archko5 小时前
用rust写了一个桌面app,就不再想用kmp了
开发语言·后端·rust