배경
Docker, Jenkins 기반의 CI/CD 와는 달리, 몇몇 서버 환경은 여전히 파일 전송 기반의 배포 방식을 사용한다.
현재 회사의 경우, 몇몇 프로젝트들은 여전히 AWS가 아닌 IDC 서버에서 운영되고 있다.
이러한 프로젝트들은 보통 빌드된 jar 파일을 FileZilla를 통해 서버에 업로드하고, 이를 스크립트로 실행하는 방식으로 배포한다.
그러나 FileZilla의 경우, 사용성 측면에서 상당히 버벅였고, 디렉토리 조회 시 새로고침 버튼을 자주 눌러주어야 한다는 점에서 큰 피로감을 주었다.
이러한 불편을 계기로, scp 명령어를 활용하여 젠킨스의 stage를 정의하는 것처럼(?) 스크립트를 작성해 보았다.
jar 파일을 수동으로 직접 배포해야하고, github-action이나 gitlab-ci 등을 사용할 수 없고, 파이프라인을 구축하기 애매한 상황이라면, 이와같이 스크립트를 기반으로 로컬 상에서의 배포 자동화를 구축하는 것도 하나의 방법이 될 수 있다.
(CI 툴이 등장하기 전으로 돌아가보자)
scp란?
scp
는 Secure Copy Protocol의 약자로, 리눅스(또는 유닉스 계열 OS)에서 원격 서버 간 파일을 안전하게 복사(전송)할 수 있도록 도와주는 명령어이다. 내부적으로 SSH 프로토콜을 기반으로 하여, 암호화된 채널을 통해 파일을 복사한다.
(+ scp
는 점차적으로 최신 OpenSSH 버전에서 deprecated되고 있고, 대체로 sftp
또는 rsync over SSH
가 추천된다.)
환경
현재 회사의 IDC 서버 내 배포된 WAS의 디렉토리의 구조와 배포 플로우는 다음과 같다.

- 기존의 JAR 파일을 서버 내에서 shutdown 스크립트를 통해 종료한다.
- 로컬 빌드 후 생성된 JAR 파일을 FileZilla 등을 통해 타겟 디렉토리에 배치한다.
- 서버 내의 startup 스크립트 통해 새로 배포한 WAS를 실행한다.
이 과정을 자동화하기 위해 몇 가지 조건을 설정했다.
- 서버 내에서도 스크립트를 통해 WAS를 핸들링할 수 있어야 하므로, startup 및 shutdown 스크립트가 배치되어 있어야 한다.
- 새로운 jar를 띄우기 전, 기존의 jar파일을 백업 디렉토리에 버저닝을 진행해야한다.
- 배포 스크립트 내 환경변수는
/conf
디렉토리에 위치해야하며, 이를 참조할 수 있어야한다.
사전 작업
우선 로컬 프로젝트에 파일을 배치한다.

application.yml
에 주입할 환경변수 파일(.env
)는 프로젝트 루트에 배치- 스크립트 파일들(
deploy.sh
,startup_application.sh
,shutdown_application.sh
,check_status.sh
)과 스크립트에 사용할 환경변수 파일(.deploy.env
)은 /scripts 디렉토리에 배치
배포 스크립트 플로우
이후 deploy.sh
스크립트의 배포 플로우는 다음과 같다.
- 환경변수(
.deploy.env
) 읽기 및 경로 변수 선언 - sshpass 명령어 유무 확인
- jar clear build
.deploy.env
에 배치한 remote server로 scp를 통해 파일 전송 (타겟 폴더로 전체 전송)- startup 스크립트
- shutdown 스크립트
- check 스크립트
- env 파일
- deploy env 파일
- 기존 프로세스 종료
- 파일 마다의 타겟 포인트로 파일(환경변수 파일 및 스크립트 파일) 이동 후 덮어쓰기
- 파일 실행 및 읽기 권한 부여
- shutdown 스크립트 실행
- 환경변수 읽기 및 경로 설정
- 기존 프로세스 종료
- 버저닝 후 백업
- 기존 jar 파일 삭제
- 빌드한 JAR 파일 전송
- 새로운 프로세스 시작
- startup 스크립트 실행
- 환경변수 읽기 및 경로 설정 (jdk 경로 등)
- 로그 디렉토리 경로 지정
- 기존 프로세스 종료 (2차 체크)
- 실행
- 실행된 WAS에 health check
- ping 결과를 통해 최종 서버상태 점검 후 푸시알람 전송
- startup 스크립트 실행
결과
api-server deploy

status check

스크립트 상세
/api-server/scripts/deploy.sh
#!/bin/bash
set -e
# 1. Load deployment environment variables
DEPLOY_ENV="$(dirname "$0")/.deploy.env"
if [ ! -f "$DEPLOY_ENV" ]; then
echo "❌ Error: $DEPLOY_ENV file not found."
exit 1
fi
source "$DEPLOY_ENV"
# 2. Define paths
JAR_FILE="build/libs/${JAR_NAME}"
STARTUP_SCRIPT="scripts/startup_application.sh"
SHUTDOWN_SCRIPT="scripts/shutdown_application.sh"
CHECK_SCRIPT="scripts/check_status.sh"
ENV_FILE=".env"
CONF_DIR="$REMOTE_DIR/conf"
DEPLOY_ENV_PATH="scripts/.deploy.env"
# 3. Check sshpass availability
if ! command -v sshpass &> /dev/null; then
echo "❌ Error: sshpass is not installed."
exit 1
fi
echo "✅ 1. Environment loaded and sshpass is available."
# 4. Build project
cd ..
./gradlew clean build
echo "✅ 2. Project built successfully."
# 5. Transfer .env and scripts
sshpass -p "$REMOTE_PASS" scp \
"$STARTUP_SCRIPT" \
"$SHUTDOWN_SCRIPT" \
"$CHECK_SCRIPT" \
"$ENV_FILE" \
"$DEPLOY_ENV_PATH" \
"$REMOTE_USER@$REMOTE_HOST:$REMOTE_DIR/"
echo "✅ 3. .env and scripts transferred."
# 6. Prepare environment and shutdown existing app
sshpass -p "$REMOTE_PASS" ssh -T "$REMOTE_USER@$REMOTE_HOST" <<EOF
mkdir -p $CONF_DIR
mv $REMOTE_DIR/.env $CONF_DIR/.env
mv $REMOTE_DIR/.deploy.env $CONF_DIR/.deploy.env
chmod +x $REMOTE_DIR/startup_application.sh
chmod +x $REMOTE_DIR/shutdown_application.sh
chmod +x $REMOTE_DIR/check_status.sh
chmod 644 $CONF_DIR/.env
chmod 644 $CONF_DIR/.deploy.env
bash $REMOTE_DIR/shutdown_application.sh
EOF
echo "✅ 4. Shutdown completed and configuration applied."
# 7. Transfer JAR
sshpass -p "$REMOTE_PASS" scp \
"$JAR_FILE" \
"$REMOTE_USER@$REMOTE_HOST:$REMOTE_DIR/"
echo "✅ 5. JAR file transferred."
# 8. Start application
sshpass -p "$REMOTE_PASS" ssh -T "$REMOTE_USER@$REMOTE_HOST" <<EOF
bash $REMOTE_DIR/startup_application.sh
EOF
echo "✅ 6. Application started on remote server."
# 9. Health check and alert
echo "✅ 7. Checking server health on remote..."
sshpass -p "$REMOTE_PASS" ssh -T "$REMOTE_USER@$REMOTE_HOST" <<EOF
MAX_RETRIES=20
RETRY_DELAY=5
SUCCESS=false
for (( i=1; i<=MAX_RETRIES; i++ ))
do
if curl -s "$DEPLOY_URL/ping" | grep -q "pong"; then
echo "🚀 Server is alive. Deploy succeeded!"
curl -X POST -H "Content-Type: application/json" \
-d '{"title":"Deployment success","body":"The server responded to /ping ✅"}' \
"$PUSH_ALERT"
SUCCESS=true
break
fi
echo "Waiting for server... (\$i/$MAX_RETRIES)"
sleep "\$RETRY_DELAY"
done
if [ "\$SUCCESS" != true ]; then
echo "❌ Server failed to respond."
curl -X POST -H "Content-Type: application/json" \
-d '{"title":"Deployment failed","body":"The server did not respond to /ping ❌"}' \
"$PUSH_ALERT"
exit 1
fi
EOF
/api-server/scripts/startup_application.sh
#!/bin/bash
set -e
echo " > Starting server..."
DEPLOY_ENV="$(dirname "$0")/conf/.deploy.env"
if [ -f "$DEPLOY_ENV" ]; then
source "$DEPLOY_ENV"
fi
JAR_PATH="${BASE_DIR}/${JAR_NAME}"
ENV_FILE="${BASE_DIR}/conf/.env"
LOG_FILE="${BASE_DIR}/logs/app.log"
JAVA_HOME="/data/autobond/config/jdk17"
JAVA_CMD="$JAVA_HOME/bin/java"
mkdir -p "${BASE_DIR}/logs"
if [ ! -f "$ENV_FILE" ]; then
echo " > .env not found: $ENV_FILE"
exit 1
fi
set +o histexpand
source "$ENV_FILE"
echo " > ENV loaded: $SERVER_PORT"
PID=$(pgrep -f "$JAR_PATH" || true)
if [ -n "$PID" ]; then
echo " > Stopping existing process (PID: $PID)..."
kill -9 "$PID"
fi
nohup "$JAVA_CMD" -Xms1024m -Xmx1024m -jar "$JAR_PATH" >> "$LOG_FILE" 2>&1 < /dev/null &
echo " > Started. Logs: $LOG_FILE"
/api-server/scripts/shutdown_application.sh
#!/bin/bash
set -e
echo " > Stopping server and creating backup..."
# 0. Load environment variables
DEPLOY_ENV="$(dirname "$0")/conf/.deploy.env"
if [ -f "$DEPLOY_ENV" ]; then
source "$DEPLOY_ENV"
fi
JAR_PATH="$BASE_DIR/$JAR_NAME"
LOG_PATH="$BASE_DIR/logs/app.log"
BACKUP_DIR="$BASE_DIR/backup"
mkdir -p "$BACKUP_DIR"
# 1. Shutdown existing application gracefully
PID=$(pgrep -f "$JAR_PATH" || true)
if [ -n "$PID" ]; then
echo " > Sending SIGTERM to process (PID: $PID)..."
kill "$PID"
# Wait up to 30 seconds for graceful shutdown
for i in {1..30}; do
if ps -p "$PID" > /dev/null; then
echo " > Waiting for process to terminate... ($i/30)"
sleep 1
else
echo " > Graceful shutdown complete."
break
fi
done
# Force kill if still alive
if ps -p "$PID" > /dev/null; then
echo " > Process still alive. Forcing kill."
kill -9 "$PID"
fi
else
echo " > No running application found."
fi
# 2. Create timestamped archive backup
if [ -f "$JAR_PATH" ]; then
TIMESTAMP=$(date +%Y%m%d%H%M%S)
ARCHIVE_DIR="$BACKUP_DIR/$TIMESTAMP"
ARCHIVE_PATH="$BACKUP_DIR/${TIMESTAMP}.tar.gz"
mkdir -p "$ARCHIVE_DIR"
# Copy JAR file
cp "$JAR_PATH" "$ARCHIVE_DIR/$JAR_NAME"
# Copy and truncate log file if it exists
if [ -f "$LOG_PATH" ]; then
cp "$LOG_PATH" "$ARCHIVE_DIR/app.log"
> "$LOG_PATH"
fi
# Compress archive directory
tar -czf "$ARCHIVE_PATH" -C "$BACKUP_DIR" "$TIMESTAMP"
echo " > Backup archive created: $ARCHIVE_PATH"
# Clean up
rm -rf "$ARCHIVE_DIR"
rm -f "$JAR_PATH"
echo " > Original JAR deleted: $JAR_PATH"
else
echo " > No JAR file found to back up: $JAR_PATH"
fi
/api-server/scripts/check_status.sh
#!/bin/bash
set -e
echo "> Checking server status..."
# Load deployment environment
DEPLOY_ENV="$(dirname "$0")/conf/.deploy.env"
if [ ! -f "$DEPLOY_ENV" ]; then
echo "❌ Error: $DEPLOY_ENV not found"
exit 1
fi
source "$DEPLOY_ENV"
# Validate required variables
if [ -z "$BASE_DIR" ] || [ -z "$JAR_NAME" ] || [ -z "$SERVER_PORT" ]; then
echo "❌ BASE_DIR, JAR_NAME, or SERVER_PORT is not set in .deploy.env"
exit 1
fi
# Define paths
JAR_PATH="$BASE_DIR/$JAR_NAME"
LOG_FILE="$BASE_DIR/logs/app.log"
CONF_DIR="$BASE_DIR/conf"
# Check JAR process
SERVER_PID=$(pgrep -f "$JAR_NAME" || echo "")
if [ -n "$SERVER_PID" ]; then
echo "> ✅ Server is running (PID: $SERVER_PID)"
# Port check
if netstat -tlnp 2>/dev/null | grep -q ":$SERVER_PORT"; then
echo "> ✅ Serving on port $SERVER_PORT"
else
echo "> ⚠️ Port $SERVER_PORT not active"
fi
# Memory usage
if command -v ps >/dev/null 2>&1; then
MEMORY=$(ps -o rss= -p "$SERVER_PID" 2>/dev/null | awk '{print $1/1024 " MB"}' || echo "Unknown")
echo "> 📊 Memory usage: $MEMORY"
fi
else
echo "> ❌ Server is not running"
fi
# Log file check
if [ -f "$LOG_FILE" ]; then
LOG_SIZE=$(du -h "$LOG_FILE" | cut -f1)
echo "> 📝 Log file: $LOG_FILE (Size: $LOG_SIZE)"
echo "> 📋 Recent logs (last 5 lines):"
tail -5 "$LOG_FILE" 2>/dev/null || echo "Could not read logs"
else
echo "> 📝 Log file not found"
fi
# Config directory check
if [ -d "$CONF_DIR" ]; then
CONF_SIZE=$(du -sh "$CONF_DIR" | cut -f1)
echo "> ⚙️ Config directory: $CONF_DIR (Size: $CONF_SIZE)"
else
echo "> ⚙️ Config directory not found"
fi
/api-server/scripts/.deploy.env
REMOTE_HOST=
REMOTE_USER=
REMOTE_PASS=
REMOTE_DIR=
SERVER_PORT=
BASE_DIR=
JAR_NAME=
DEPLOY_URL=
PUSH_ALERT=