English
Advanced Linux Service Deployment Scripts: From 3-Line Ad-Hoc Commands to Production-Grade General-Purpose 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.serviceDescription: 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
| Problem | influence |
|---|---|
| No backup mechanism | When there is a problem with the new version, it cannot be rolled back quickly; you can only manually retrieve the old version. |
| Hardcoded version number | Each update requires manual modification of the script and cannot be reused. |
| relative path | Copy fails when executing directory changes, lacking robustness. |
| Stateless check | There is no alert when replication fails or the service fails to start, so operations personnel cannot detect it in a timely manner. |
| No log output | The 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.serviceImprovement 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
fiUsage 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-1455Improvement points
Service name parameterization: the hard-coded
SERVICE_NAMEChange 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 -eEnsure 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.shExample 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/nginxQuick 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.serviceFeature comparison across versions
| Characteristics | v1.0 | v1.1 | v2.0 | v3.0 | v4.0 |
|---|---|---|---|---|---|
| absolute path | ❌ | ✅ | ✅ | ✅ | ✅ |
| parameterization | ❌ | ❌ | ✅ | ✅ | ✅ |
| Automatic backup | ❌ | ❌ | ✅ | ✅ | ✅ |
| Status check | ❌ | ❌ | ✅ | ✅ | ✅ |
| Color output | ❌ | ❌ | ❌ | ✅ | ❌ |
| Interactive confirmation | ❌ | ❌ | ❌ | ✅ | ❌ |
| Quick rollback | ❌ | ❌ | ❌ | ❌ | ✅ |
| Lines of code | 1 | 1 | ~40 | ~90 | ~50 |
Selection recommendations
| Scene | Recommended version | Reason |
|---|---|---|
| Temporary test | v2.0 | Lightweight and fast, with flexible parameters |
| production environment | v3.0 | Safe and controllable, with observable status |
| Frequent updates | v4.0 | Fast switchover, rollback in seconds |
| Team collaboration | v3.0 or v4.0 | Improve 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:
v2.0 Lite Version— Daily test
v3.0 Full Version— Production deployment
v4.0 Symlink Version— Frequent updates
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.