Bash 스크립트를 사용한 Dockerized 앱 배포 자동화

발행: (2025년 12월 13일 오후 03:06 GMT+9)
7 min read
원문: Dev.to

Source: Dev.to

DevOps는 자동화, 신뢰성, 효율성에 관한 모든 것입니다. DevOps 인턴십 1단계에서 저는 원격 Linux 서버에 Docker화된 애플리케이션을 배포하는 강력한 Bash 스크립트를 만들었습니다.

개발자이든, 시스템 관리자이든, DevOps 애호가이든, 이 가이드는 저장소 복제부터 Nginx를 역방향 프록시로 설정하는 것까지 모든 과정을 단계별로 안내하여 프로덕션 수준의 배포 스크립트를 만드는 방법을 보여줍니다.

왜 배포를 자동화할까요?

  • 일관성 – 배포가 매번 동일합니다.
  • 속도 – 인간 개입을 줄이고 시간을 절약합니다.
  • 신뢰성 – 오류를 조기에 감지하고 문제 해결을 위해 로그를 남깁니다.

이 가이드를 끝까지 따라 하면, 원격 서버에 Docker화된 앱을 완전히 배포할 수 있는 단일 Bash 스크립트(deploy.sh)를 얻게 됩니다.

Step 1: 사용자 입력으로부터 매개변수 수집

  • Git 저장소 URL
  • Personal Access Token (PAT)
  • 브랜치 이름 (기본값 main)
  • SSH 자격 증명 (사용자명, 서버 IP, 키 경로)
  • 애플리케이션 포트

입력값을 검증하여 이후 스크립트 오류를 방지합니다.

# Git Repository URL
while [[ -z "${GIT_REPO_URL:-}" ]]; do
    read -rp "Enter the git repository URL: " GIT_REPO_URL
    [[ -z "$GIT_REPO_URL" ]] && log_error "Repository URL cannot be empty."
done

# Personal Access Token (PAT) - hidden input
while [[ -z "${GIT_PAT:-}" ]]; do
  read -rsp "Enter your Git Personal Access Token (PAT): " GIT_PAT
  echo ""
  [[ -z "$GIT_PAT" ]] && log_error "Personal Access Token cannot be empty."
done

# Branch name (default = main)
read -rp "Enter branch name [default: main]: " GIT_BRANCH
GIT_BRANCH=${GIT_BRANCH:-main}

# SSH username
while [[ -z "${SSH_USER:-}" ]]; do
    read -rp "Enter remote server SSH username: " SSH_USER
    [[ -z "$SSH_USER" ]] && log_error "SSH username cannot be empty."
done

# Server IP address
while [[ -z "${SERVER_IP:-}" ]]; do
    read -rp "Enter remote server IP address: " SERVER_IP
    if [[ ! "$SERVER_IP" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then
        log_error "Invalid IP format. Please enter a valid IPV4 address."
        SERVER_IP=""
    fi
done

# SSH key path
while [[ -z "${SSH_KEY_PATH:-}" ]]; do
    read -rp "Enter path to SSH private key: " SSH_KEY_PATH
    if [[ ! -f "$SSH_KEY_PATH" ]]; then
        log_error "SSH key file not found at $SSH_KEY_PATH"
        SSH_KEY_PATH=""
    fi
done

# Application port (internal container port)
while [[ -z "${APP_PORT:-}" ]]; do
    read -rp "Enter internal application (container) port (e.g., 8080): " APP_PORT
    if ! [[ "$APP_PORT" =~ ^[0-9]+$ ]]; then
        log_error "Invalid port. Please enter a numeric value."
        APP_PORT=""
    fi
done

Tip: 항상 조건문을 사용해 사용자 입력을 검증하고, URL이 올바른지와 파일이 존재하는지를 확인하세요.

Step 2: 저장소 복제 또는 Pull

REPO_NAME=$(basename -s .git "$GIT_REPO_URL")
WORK_DIR="$HOME/deployment/$REPO_NAME"

# Create deployment directory if not exists
mkdir -p "$(dirname "$WORK_DIR")"

# Authenticated URL (PAT safely embedded)
GIT_USERNAME=$(echo "$GIT_REPO_URL" | awk -F[/:] '{print $(NF-1)}')
AUTH_REPO_URL=$(echo "$GIT_REPO_URL" | sed "s#https://#https://$GIT_USERNAME:$GIT_PAT@#")

if [[ -d "$WORK_DIR/.git" ]]; then
    log_info "Repository already exists. Pulling latest changes..."
    cd "$WORK_DIR"
    git reset --hard
    git clean -fd
    git fetch origin "$GIT_BRANCH"
    git checkout "$GIT_BRANCH"
    git pull origin "$GIT_BRANCH" || {
        log_error "Failed to pull latest changes from $GIT_BRANCH"
        exit $EXIT_UNKNOWN
    }
else
    log_info "Cloning repository into $WORK_DIR..."
    git clone --branch "$GIT_BRANCH" "$AUTH_REPO_URL" "$WORK_DIR" || {
        log_error "Failed to clone repository. Please check your URL or PAT"
        exit $EXIT_UNKNOWN
    }
    cd "$WORK_DIR"
fi

log_success "Repository is ready at: $WORK_DIR"

이 단계는 로컬 프로젝트 폴더가 원격 저장소와 항상 최신 상태가 되도록 보장합니다.

Step 3: Docker 설정 파일 검증

# Check for Docker configuration files
if [[ -f "Dockerfile" ]]; then
    log_success "Found Dockerfile – ready for Docker build."
elif [[ -f "compose.yaml" || -f "compose.yml" || -f "docker-compose.yaml" || -f "docker-compose.yml" ]]; then
    log_success "Found docker-compose.yml – ready for multi‑service deployment."
else
    log_error "No Dockerfile or docker-compose.yml found. Cannot continue deployment."
    exit $EXIT_UNKNOWN
fi

Step 4: SSH 연결 테스트

# Validate SSH key exists
if [ ! -f "$SSH_KEY_PATH" ]; then
    log_error "SSH key not found at: $SSH_KEY_PATH"
    exit $EXIT_SSH_FAIL
fi

# Test SSH connection (non‑interactive)
ssh -i "$SSH_KEY_PATH" -o StrictHostKeyChecking=no -o BatchMode=yes \
    "$SSH_USER@$SERVER_IP" "echo 'SSH connection successful!'" >/dev/null 2>&1

if [ $? -ne 0 ]; then
    log_error "Unable to connect to remote server via SSH. Verify credentials, key permissions, and IP address."
    exit $EXIT_SSH_FAIL
else
    log_success "SSH connection verified successfully."
fi

Step 5: 원격 환경 준비

스크립트는 패키지를 업데이트하고, Docker, Docker Compose, Nginx를 설치하며, SSH 사용자를 Docker 그룹에 추가하고, 서비스가 활성화 및 시작되도록 합니다.

ssh -i "$SSH_KEY_PATH" -o StrictHostKeyChecking=no "$SSH_USER@$SERVER_IP" bash /dev/null; then
    echo "Docker not found. Installing Docker..."
    curl -fsSL https://get.docker.com | sudo bash
else
    echo "Docker already installed."
fi

# Install Docker Compose if not present
if ! command -v docker-compose &>/dev/null; then
    echo "Installing Docker Compose..."
    sudo curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" \
        -o /usr/local/bin/docker-compose
    sudo chmod +x /usr/local/bin/docker-compose
else
    echo "Docker Compose already installed."
fi

# Install Nginx if not present
if ! command -v nginx &>/dev/null; then
    echo "Installing nginx..."
    sudo apt-get install -y nginx
else
    echo "nginx already installed."
fi

# Add SSH user to docker group
if ! groups $SSH_USER | grep -q docker; then
    echo "Adding user '$SSH_USER' to docker group..."
    sudo usermod -aG docker $SSH_USER
    echo "You may need to log out and back in for this to take effect."
else
    echo "User '$SSH_USER' already in docker group."
fi

# Enable and start services
sudo systemctl enable docker
sudo systemctl start docker
sudo systemctl enable nginx
sudo systemctl start nginx

# Confirm installation versions
docker --version
docker-compose --version
nginx -v

echo "Remote environment setup complete."
EOF

Best practice: 프로비저닝 후 항상 설치된 버전과 서비스 상태를 확인하세요.

Step 6: Docker화된 애플리케이션 배포

# Sync source code to remote server (excluding unnecessary files)
rsync -avz -e "ssh -i $SSH_KEY_PATH -o StrictHostKeyChecking=no" \
    --exclude '.git' --exclude 'node_modules' --exclude '.env' \
    "$WORK_DIR/" "$SSH_USER@$SERVER_IP:$REMOTE_APP_DIR" >/dev/null 2>&1

# Run deployment commands on the remote host
ssh -i "$SSH_KEY_PATH" -o StrictHostKeyChecking=no "$SSH_USER@$SERVER_IP" bash <<'EOF'
set -e

cd $REMOTE_APP_DIR

# Detect Docker configuration
if [[ -f "docker-compose.yml" ]]; then
    echo "docker-compose.yml found. Starting with Docker Compose..."
    sudo docker-compose pull
    sudo docker-compose build
    sudo docker-compose up -d
elif [[ -f "Dockerfile" ]]; then
    echo "Dockerfile found. Building and running manually..."
    APP_NAME=$(basename $(pwd))
    sudo docker build -t $APP_NAME .
    sudo docker run -d -p $APP_PORT:$APP_PORT --name $APP_NAME $APP_NAME
else
    echo "No Dockerfile or docker-compose.yml found in $REMOTE_APP_DIR"
    exit 1
fi
EOF

이 시점에서 애플리케이션은 원격 서버에서 실행 중이며, 설정된 Nginx 역방향 프록시를 통해 접근할 수 있습니다.

Back to Blog

관련 글

더 보기 »