Skip to content

Advanced Linux Service Deployment Scripts: From 3-Line Ad-Hoc Commands to Production-Grade General-Purpose Tools ​

The Evolution of Linux Service Deployment Scripts from Ad Hoc Commands to Production-Grade Tools

Introduction ​

Do you have to do it manually every time you update the service?systemctl stop,cp -r,systemctl start A triple combo seems simple, but carries hidden risks.

This article starts with a temporary command and gradually refactors it into a production-grade general-purpose deployment script that supports parameterization, automatic backup, status checks, and fast rollback. After reading, you will learn:

  • Bash scripts: evolving from ad-hoc to production-grade

  • The iterative process and design trade-offs across 5 versions

  • Soft links enable zero-downtime switching and rapid rollback.

  • Complete script source code ready for direct use

Phase 1: Minimum Viable (v1.0) ​

Original script ​

bash
#!/bin/bash
systemctl stop my-service.service && cp -r my-service-20260617-1455 /usr/local/bin/my-service && systemctl start my-service.service

Description: What is used here is && The logical AND operator, indicating that the next command is executed only after the previous command executes successfully, so the entire process is Strictly serial The copy operation will wait.systemctl stop Done. Service startup will also wait for the copy to complete, so there is no risk of "background copying causing the service to read incomplete files." Even so, this script still has the other issues listed below.

Problem Analysis ​

Probleminfluence
No backup mechanismWhen there is a problem with the new version, it cannot be rolled back quickly; you can only manually retrieve the old version.
Hardcoded version numberEach update requires manual modification of the script and cannot be reused.
relative pathCopy fails when executing directory changes, lacking robustness.
Stateless checkThere is no alert when replication fails or the service fails to start, so operations personnel cannot detect it in a timely manner.
No log outputThe operation process is invisible, making troubleshooting difficult.

Phase 2: Fix path issues (v1.1) ​

Improvement ​

bash
#!/bin/bash
systemctl stop my-service.service && cp -r /opt/releases/my-service-20260617-1455 /usr/local/bin/my-service && systemctl start my-service.service

Improvement points ​

  • Use absolute paths to avoid copy failures caused by changes in the execution directory.

Remaining issues ​

  • No backup; cannot roll back.

  • Version number is hardcoded and not universal.

  • Stateless check, no log output

Phase 3: Parametric Transformation (v2.0) ​

Starting from this version, the script truly takes the first step toward "generalization"—the service name is no longer hardcoded but passed in as a command-line argument, thereby supporting the deployment of any service.

Improve ​

bash
#!/bin/bash
set -e

show_usage() {
    echo "Usage: $0 <service name> <new version path> [target path]"
    echo ""
    echo "Example:"
    echo "  $0 my-service ./my-service-20260625-1442"
    echo "  $0 my-service ./my-service-20260625-1442 /usr/bin/my-service"
}

if [ $# -lt 2 ]; then
    show_usage
    exit 1
fi

# Parameter parsing - service name as the first parameter
SERVICE_NAME="$1"
SRC="$2"
DEST="${3:-/usr/local/bin/$SERVICE_NAME}"

# Handle relative paths
[[ "$SRC" != /* ]] && SRC="$(pwd)/$SRC"

# 验证源路径存在
if [ ! -e "$SRC" ]; then
    echo "Error: Source path does not exist: $SRC"
    exit 1
fi

echo "Deploy: $SERVICE_NAME $SRC → $DEST"

# Stop service
systemctl stop "$SERVICE_NAME" 2>/dev/null || true

# backup
if [ -e "$DEST" ]; then
    mv "$DEST" "${DEST}.bak.$(date +%Y%m%d%H%M%S)"
fi

# Copy
if [ -d "$SRC" ]; then
    cp -r "$SRC" "$DEST"
elif [ -f "$SRC" ]; then
    cp "$SRC" "$DEST"
fi

# Set execute permission
if [ -d "$DEST" ]; then
    chmod +x "$DEST"/* 2>/dev/null || true
elif [ -f "$DEST" ]; then
    chmod +x "$DEST"
fi

# Start service
systemctl start "$SERVICE_NAME"

# Status check
sleep 1
if systemctl is-active --quiet "$SERVICE_NAME"; then
    echo "✅ Deployment successful"
else
    echo "❌ Deployment failed"
    exit 1
fi

Usage example ​

bash
# Basic usage: deploy to the default path /usr/local/bin/<service name>
./deploy.sh my-service ./my-service-20260625-1442

# Specify the target path
./deploy.sh my-service ./my-service-20260625-1442 /usr/bin/my-service

# Deploy any service (the script is truly universal)
./deploy.sh my-service ./my-service-20260625-1442
./deploy.sh nginx /opt/nginx-1.24.0 /usr/sbin/nginx
./deploy.sh myapp ./myapp-v2.0.1 /opt/myapp/bin/myapp

# Use relative paths (automatically converted to absolute paths)
cd /opt/releases && ./deploy.sh my-service my-service-20260617-1455

Improvement points ​

  • Service name parameterization: the hard-coded SERVICE_NAME Change it to the first command-line argument; from then on, the script supports any service.

  • Version number parameterization The version number is passed in as a parameter, so the script can be reused.

  • Target path is optional: Automatically generated when not specified /usr/local/bin/<服务名>

  • Supports automatic conversion between relative and absolute paths.

  • Automatically back up the current version

  • set -e Ensure the script exits when any command fails.

  • Add a simple status check

Phase 4: Production-Grade Enhancements (v3.0) ​

Building on the parameterization in v2.0, v3.0 focuses on security and observability in production environments, adding features such as colored logs, interactive confirmation, and improved status checks.

Improve ​

bash
#!/bin/bash
set -e

RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'

log_info()  { echo -e "${GREEN}[INFO]${NC} $1"; }
log_warn()  { echo -e "${YELLOW}[WARN]${NC} $1"; }
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }

show_usage() {
    echo "Usage: $0 <service name> <new version path> [target path]"
    echo ""
    echo "Parameter description:"
    echo "  Service name      systemd service name (required)"
    echo "  New version path    Source file or directory path (required)"
    echo "  Target path      Deployment target path (optional, default: /usr/local/bin/<service name>)"
    echo ""
    echo "Example:"
    echo "  $0 my-service ./my-service-20260625-1442"
    echo "  $0 my-service ./my-service-20260625-1442 /usr/bin/my-service"
    echo "  $0 my-service /opt/releases/my-service-20260617-1455 /usr/local/bin/my-service"
}

if [ $# -lt 2 ]; then
    log_error "Insufficient parameters!"
    show_usage
    exit 1
fi

# Parameter parsing
SERVICE_NAME="$1"
SRC="$2"
DEST="${3:-/usr/local/bin/$SERVICE_NAME}"

# Handle relative paths
[[ "$SRC" != /* ]] && SRC="$(pwd)/$SRC"

# Check whether the source path exists.
if [ ! -e "$SRC" ]; then
    log_error "Source path does not exist: $SRC"
    exit 1
fi

# Display deployment information
log_info "========== Deployment Information =========="
log_info "Service name: $SERVICE_NAME"
log_info "Source path:   $SRC"
log_info "Target path: $DEST"
log_info "=============================="

# Confirm deployment
read -p "Confirm continue deployment? (y/N) " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
    log_warn "Deployment cancelled"
    exit 0
fi

# Stop service
log_info "Stopping service: $SERVICE_NAME"
if systemctl is-active --quiet "$SERVICE_NAME" 2>/dev/null; then
    systemctl stop "$SERVICE_NAME"
    log_info "Service stopped"
else
    log_warn "Service is not running or does not exist"
fi

# Back up the existing version
if [ -e "$DEST" ]; then
    BACKUP="${DEST}.bak.$(date +%Y%m%d%H%M%S)"
    log_info "Backing up current version to: $BACKUP"
    mv "$DEST" "$BACKUP"
fi

# Copy the new version
log_info "Copying new version..."
if [ -d "$SRC" ]; then
    cp -r "$SRC" "$DEST"
elif [ -f "$SRC" ]; then
    cp "$SRC" "$DEST"
else
    log_error "Source path is neither a file nor a directory: $SRC"
    exit 1
fi

# Set execute permission
if [ -d "$DEST" ]; then
    chmod +x "$DEST"/* 2>/dev/null || true
elif [ -f "$DEST" ]; then
    chmod +x "$DEST"
fi

# Start service
log_info "Starting service: $SERVICE_NAME"
systemctl start "$SERVICE_NAME" 2>&1 || {
    log_error "Failed to start service!"
    systemctl status "$SERVICE_NAME" --no-pager -l
    log_error "Please check the logs: journalctl -u $SERVICE_NAME -n 50"
    exit 1
}

# Waiting for the service to start
sleep 2

# Check service status
if systemctl is-active --quiet "$SERVICE_NAME"; then
    log_info "✅ Service started successfully!"
    systemctl status "$SERVICE_NAME" --no-pager -l
else
    log_error "❌ Service failed to start!"
    systemctl status "$SERVICE_NAME" --no-pager -l
    log_error "Please check the logs: journalctl -u $SERVICE_NAME -n 50"
    exit 1
fi

log_info "========== Deployment completed =========="

Usage example ​

bash
# Basic usage: Deploy the service to the default path
./deploy.sh my-service ./my-service-20260625-1442

# Specify the target path
./deploy.sh my-service ./my-service-20260625-1442 /usr/bin/my-service

# Deploy other services
./deploy.sh nginx /opt/nginx-1.24.0 /usr/sbin/nginx

# Use relative paths (automatically converted to absolute paths)
cd /opt/releases && ./deploy.sh my-service my-service-20260617-1455

# View help information
./deploy.sh

Example deployment process output ​

bash
[INFO] ========== Deployment Information ==========
[INFO] Service Name: my-service
[INFO] Source path:   /root/my-service-20260625-1442
[INFO] Target path: /usr/bin/my-service
[INFO] ==============================
Confirm continue deployment? (y/N) y
[INFO] Stopping service: my-service
[INFO] Service stopped
[INFO] Backing up current version to: /usr/bin/my-service.bak.20260625150356
[INFO] Copying new version...
[INFO] Starting service: my-service
[INFO] ✅ Service started successfully!
[INFO] my-service.service - my service
  Loaded: loaded (/usr/lib/systemd/system/my-service.service; enabled)
  Active: active (running) since Thu 2026-06-25 15:03:56 CST; 2s ago
[INFO] ========== Deployment complete ==========

New features ​

  • Color output, clear information

  • Interactive confirmation to prevent misoperation

  • Automatically check service status after startup

  • Display error log troubleshooting commands on failure

  • Supports two source types: files and directories.

Phase 5: Symbolic Links and Quick Rollback (v4.0) ​

For services that are frequently updated, copying the entire directory each time is inefficient and takes up disk space. Use soft link Solution: version switching only requires changing the link target, enabling second-level rollback.

Improvement ​

bash
#!/bin/bash
set -e

show_usage() {
    echo "Usage: $0 <service name> <new version directory> [link path]"
    echo ""
    echo "Example:"
    echo "  $0 my-service /opt/releases/my-service-20260617-1455"
    echo "  $0 my-service /opt/releases/my-service-20260617-1455 /usr/bin/my-service"
}

if [ $# -lt 2 ]; then
    show_usage
    exit 1
fi

SERVICE_NAME="$1"
SRC="$2"
[[ "$SRC" != /* ]] && SRC="$(pwd)/$SRC"

if [ ! -d "$SRC" ]; then
    echo "Error: directory does not exist: $SRC"
    exit 1
fi

LINK_PATH="${3:-/usr/local/bin/$SERVICE_NAME}"
BACKUP_DIR="/opt/backups/$SERVICE_NAME"

mkdir -p "$BACKUP_DIR"

echo "Deploy: $SERVICE_NAME $SRC → $LINK_PATH"

# Stop service
systemctl stop "$SERVICE_NAME" 2>/dev/null || true

# Back up the version pointed to by the current link
if [ -L "$LINK_PATH" ]; then
    CURRENT=$(readlink "$LINK_PATH")
    if [ -d "$CURRENT" ]; then
        BACKUP_NAME="$(basename "$CURRENT").$(date +%Y%m%d%H%M%S)"
        cp -r "$CURRENT" "$BACKUP_DIR/$BACKUP_NAME"
        echo "Back up current version to: $BACKUP_DIR/$BACKUP_NAME"
    fi
fi

# Update symlink
rm -f "$LINK_PATH"
ln -s "$SRC" "$LINK_PATH"

# Start service
systemctl start "$SERVICE_NAME"
sleep 2

# Check status
if systemctl is-active --quiet "$SERVICE_NAME"; then
    echo "✅ Deployment successful"
    systemctl status "$SERVICE_NAME" --no-pager
else
    echo "❌ Deployment failed"
    systemctl status "$SERVICE_NAME" --no-pager
    exit 1
fi

echo ""
echo "🔙 Rollback command:"
echo "rm -f $LINK_PATH && ln -s <old version path> $LINK_PATH && systemctl restart $SERVICE_NAME"

Usage example ​

bash
# Deploy to the default link path
./deploy.sh my-service /opt/releases/my-service-20260617-1455

# Specify link path
./deploy.sh my-service /opt/releases/my-service-20260617-1455 /usr/bin/my-service

# Deploy any service
./deploy.sh my-service /opt/releases/my-service-20260625-1442
./deploy.sh nginx /opt/nginx-1.24.0 /usr/sbin/nginx

Quick rollback example ​

bash
# The current link points to the new version
ls -l /usr/local/bin/my-service
# /usr/local/bin/my-service -> /opt/releases/my-service-20260617-1455

# Issue found, roll back to the previous version.
rm -f /usr/local/bin/my-service
ln -s /opt/releases/my-service-20260617-1440 /usr/local/bin/my-service
systemctl restart my-service.service

# Verify that the rollback was successful
systemctl status my-service.service

Feature comparison across versions ​

Characteristicsv1.0v1.1v2.0v3.0v4.0
absolute path❌✅✅✅✅
parameterization❌❌✅✅✅
Automatic backup❌❌✅✅✅
Status check❌❌✅✅✅
Color output❌❌❌✅❌
Interactive confirmation❌❌❌✅❌
Quick rollback❌❌❌❌✅
Lines of code11~40~90~50

Selection recommendations ​

SceneRecommended versionReason
Temporary testv2.0Lightweight and fast, with flexible parameters
production environmentv3.0Safe and controllable, with observable status
Frequent updatesv4.0Fast switchover, rollback in seconds
Team collaborationv3.0 or v4.0Improve logs and standardize operations

Summary ​

From an ad-hoc command to a production-grade tool, the core evolution approach:

  • Get it running first.: Simplest feature implementation

  • Find risks: No backup, hardcoded, path dependency

  • Make it universal: Parameterized, supports arbitrary paths and service names

  • Add security: Backup, confirmation, status check

  • Go experience it: Color output, rollback capability

Appendix: Complete Source Code ​

The complete scripts for all versions have been organized and can be copied and used directly:

The scripts in this article have been tested and verified on CentOS 7/8 and Ubuntu 20.04/22.04 environments. If you have any questions, feel free to leave a comment.

Last updated on11/07/2026, 02:16 PM
@fishfinal | @fishfinals | [email protected]