chore(scripts): consolidate deployment tooling and cleanup artifacts

This commit is contained in:
Mohamadhosein Moazennia 2026-02-20 11:20:06 +03:30
parent 95336c6919
commit 1d25045c06
6 changed files with 242 additions and 153 deletions

3
.gitignore vendored
View file

@ -10,6 +10,7 @@
# Ignore temporary files
tmp/
*.tar.gz
3x-ui-custom-*.zip
# Ignore build and distribution directories
backup/
@ -37,4 +38,4 @@ x-ui.db
docker-compose.override.yml
# Ignore .env (Environment Variables) file
.env
.env

View file

@ -1,152 +0,0 @@
#!/usr/bin/env bash
set -Eeuo pipefail
# Replace a Docker-based 3x-ui instance with a custom build from current repo.
# - Backs up db/cert folders
# - Builds a custom image from current source
# - Replaces running container with docker compose
# - Saves rollback info
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"
COMPOSE_FILE="${COMPOSE_FILE:-docker-compose.yml}"
SERVICE_NAME="${SERVICE_NAME:-3xui}"
CONTAINER_NAME="${CONTAINER_NAME:-3xui_app}"
BACKUP_ROOT="${BACKUP_ROOT:-$SCRIPT_DIR/backups}"
USE_COMPOSE_BUILD="${USE_COMPOSE_BUILD:-0}"
timestamp="$(date +%F-%H%M%S)"
git_sha="$(git rev-parse --short HEAD 2>/dev/null || echo no-git)"
new_tag="3xui-custom:${git_sha}-${timestamp}"
log() {
printf '[%s] %s\n' "$(date '+%F %T')" "$*"
}
die() {
log "ERROR: $*"
exit 1
}
need_cmd() {
command -v "$1" >/dev/null 2>&1 || die "Missing required command: $1"
}
need_cmd docker
need_cmd cp
need_cmd mkdir
need_cmd date
if docker compose version >/dev/null 2>&1; then
COMPOSE_CMD=(docker compose -f "$COMPOSE_FILE")
elif command -v docker-compose >/dev/null 2>&1; then
COMPOSE_CMD=(docker-compose -f "$COMPOSE_FILE")
else
die "Neither 'docker compose' nor 'docker-compose' is available"
fi
[ -f "$COMPOSE_FILE" ] || die "Compose file not found: $COMPOSE_FILE"
mkdir -p "$BACKUP_ROOT"
backup_dir="$BACKUP_ROOT/$timestamp"
mkdir -p "$backup_dir"
log "Starting replacement using compose file: $COMPOSE_FILE"
log "Backup directory: $backup_dir"
if [ -d "$SCRIPT_DIR/db" ]; then
cp -a "$SCRIPT_DIR/db" "$backup_dir/db"
log "Backed up db to $backup_dir/db"
else
log "No ./db directory found, skipping db backup"
fi
if [ -d "$SCRIPT_DIR/cert" ]; then
cp -a "$SCRIPT_DIR/cert" "$backup_dir/cert"
log "Backed up cert to $backup_dir/cert"
else
log "No ./cert directory found, skipping cert backup"
fi
old_image="$(docker inspect -f '{{.Config.Image}}' "$CONTAINER_NAME" 2>/dev/null || true)"
if [ -n "$old_image" ]; then
rollback_tag="3xui-custom:rollback-${timestamp}"
docker image tag "$old_image" "$rollback_tag"
log "Tagged current running image for rollback: $rollback_tag (from $old_image)"
else
rollback_tag=""
log "No running container named $CONTAINER_NAME found, proceeding as fresh deploy"
fi
if [ "$USE_COMPOSE_BUILD" = "1" ]; then
log "Building via compose service '$SERVICE_NAME'"
"${COMPOSE_CMD[@]}" build "$SERVICE_NAME"
else
log "Building custom image from current repo: $new_tag"
docker build -t "$new_tag" .
fi
override_file="$backup_dir/docker-compose.override.generated.yml"
if [ "$USE_COMPOSE_BUILD" = "0" ]; then
cat > "$override_file" <<EOF
services:
$SERVICE_NAME:
image: $new_tag
EOF
COMPOSE_RUN_CMD=("${COMPOSE_CMD[@]}" -f "$override_file")
else
COMPOSE_RUN_CMD=("${COMPOSE_CMD[@]}")
fi
log "Stopping current stack"
"${COMPOSE_RUN_CMD[@]}" down
log "Starting new stack"
"${COMPOSE_RUN_CMD[@]}" up -d
log "Waiting for container to settle..."
sleep 2
if docker ps --format '{{.Names}}' | grep -qx "$CONTAINER_NAME"; then
log "Container is running: $CONTAINER_NAME"
else
die "Container $CONTAINER_NAME is not running after deployment"
fi
log "Recent logs:"
docker logs --tail 60 "$CONTAINER_NAME" || true
meta_file="$backup_dir/deploy-meta.txt"
{
echo "timestamp=$timestamp"
echo "git_sha=$git_sha"
echo "compose_file=$COMPOSE_FILE"
echo "service_name=$SERVICE_NAME"
echo "container_name=$CONTAINER_NAME"
echo "new_image_tag=$new_tag"
echo "rollback_image_tag=$rollback_tag"
echo "use_compose_build=$USE_COMPOSE_BUILD"
echo "override_file=$override_file"
} > "$meta_file"
log "Deployment metadata saved: $meta_file"
log "Replacement completed successfully."
echo
echo "Rollback quick reference:"
if [ -n "$rollback_tag" ]; then
cat <<EOF
1) Create a temporary override that points to rollback image:
cat > /tmp/3xui-rollback.yml <<ROLLBACK
services:
$SERVICE_NAME:
image: $rollback_tag
ROLLBACK
2) Redeploy rollback:
docker compose -f "$COMPOSE_FILE" -f /tmp/3xui-rollback.yml down
docker compose -f "$COMPOSE_FILE" -f /tmp/3xui-rollback.yml up -d
EOF
else
echo "No prior running image was found, so no rollback tag was created."
fi

20
scripts/common.sh Executable file
View file

@ -0,0 +1,20 @@
#!/usr/bin/env bash
set -euo pipefail
require_cmd() {
local cmd="$1"
if ! command -v "${cmd}" >/dev/null 2>&1; then
echo "Error: ${cmd} is required but not installed."
exit 1
fi
}
script_dir() {
cd "$(dirname "${BASH_SOURCE[0]}")" && pwd
}
repo_root() {
local dir
dir="$(script_dir)"
cd "${dir}/.." && pwd
}

100
scripts/deploy_on_server.sh Executable file
View file

@ -0,0 +1,100 @@
#!/usr/bin/env bash
set -euo pipefail
# Server script:
# 1) Unzip uploaded bundle
# 2) Copy code into target app directory
# 3) Rebuild + restart Docker compose stack
#
# Usage:
# ./deploy_on_server.sh <archive-name.zip> [app_dir]
# Example:
# ./deploy_on_server.sh 3x-ui-custom-20260219-120000.zip ~/panel
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
COMMON_SH="${SCRIPT_DIR}/common.sh"
if [[ -f "${COMMON_SH}" ]]; then
# shellcheck source=scripts/common.sh
. "${COMMON_SH}"
else
require_cmd() {
local cmd="$1"
if ! command -v "${cmd}" >/dev/null 2>&1; then
echo "Error: ${cmd} is required but not installed."
exit 1
fi
}
fi
APP_DIR="${2:-$HOME/panel}"
ARCHIVE_NAME="${1:-}"
if [[ -z "${ARCHIVE_NAME}" ]]; then
echo "Usage: $0 <archive-name.zip>"
exit 1
fi
if [[ ! -f "${ARCHIVE_NAME}" ]]; then
echo "Error: archive not found in current directory: ${ARCHIVE_NAME}"
exit 1
fi
require_cmd unzip
require_cmd docker
require_cmd mktemp
compose_cmd() {
if docker compose version >/dev/null 2>&1; then
echo "docker compose"
return
fi
if command -v docker-compose >/dev/null 2>&1; then
echo "docker-compose"
return
fi
echo ""
}
COMPOSE="$(compose_cmd)"
if [[ -z "${COMPOSE}" ]]; then
echo "Error: neither 'docker compose' nor 'docker-compose' is available."
exit 1
fi
WORK_DIR="$(pwd)"
TMP_EXTRACT_DIR="$(mktemp -d)"
trap 'rm -rf "${TMP_EXTRACT_DIR}"' EXIT
echo "Extracting bundle to temp dir..."
unzip -oq "${ARCHIVE_NAME}" -d "${TMP_EXTRACT_DIR}"
mkdir -p "${APP_DIR}"
echo "Syncing code to ${APP_DIR}..."
if command -v rsync >/dev/null 2>&1; then
rsync -a --delete \
--exclude "db/" \
--exclude "cert/" \
"${TMP_EXTRACT_DIR}/" "${APP_DIR}/"
else
find "${APP_DIR}" -mindepth 1 -maxdepth 1 \
! -name "db" \
! -name "cert" \
-exec rm -rf {} +
cp -a "${TMP_EXTRACT_DIR}/." "${APP_DIR}/"
fi
cd "${APP_DIR}"
mkdir -p db cert
# Stop old container if present, then rebuild and run
${COMPOSE} down || true
${COMPOSE} build
${COMPOSE} up -d
echo "Deployment complete."
${COMPOSE} ps
echo "App directory: ${APP_DIR}"
echo "Archive used: ${WORK_DIR}/${ARCHIVE_NAME}"

45
scripts/package_local.sh Executable file
View file

@ -0,0 +1,45 @@
#!/usr/bin/env bash
set -euo pipefail
# Package current repo into a timestamped zip bundle.
#
# Usage:
# ./scripts/package_local.sh [archive_prefix]
# Example:
# ./scripts/package_local.sh 3x-ui-custom
#
# Output:
# Prints the created archive file name on the last line.
ARCHIVE_PREFIX="${1:-3x-ui-custom}"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
# shellcheck source=scripts/common.sh
. "${SCRIPT_DIR}/common.sh"
require_cmd zip
TIMESTAMP="$(date +%Y%m%d-%H%M%S)"
ARCHIVE_NAME="${ARCHIVE_PREFIX}-${TIMESTAMP}.zip"
ARCHIVE_PATH="${REPO_ROOT}/${ARCHIVE_NAME}"
cd "${REPO_ROOT}"
echo "Creating archive: ${ARCHIVE_PATH}"
zip -r "${ARCHIVE_PATH}" . \
-x "./.git/*" \
-x "./.github/*" \
-x "./.opencode/*" \
-x "./.playwright-cli/*" \
-x "./.tmpdb/*" \
-x "./.tmplogs/*" \
-x "./node_modules/*" \
-x "./tmp/*" \
-x "./output/*" \
-x "./backups/*" \
-x "./*.zip" \
-x "./.DS_Store"
echo "Created: ${ARCHIVE_NAME}"
echo "${ARCHIVE_NAME}"

75
scripts/upload_to_server.sh Executable file
View file

@ -0,0 +1,75 @@
#!/usr/bin/env bash
set -euo pipefail
# Upload bundle + deploy script to remote server.
#
# Usage:
# ./scripts/upload_to_server.sh <archive_name_or_path> <remote_user> <remote_host> [remote_port] [remote_base_dir]
# Example:
# ./scripts/upload_to_server.sh 3x-ui-custom-20260219-120000.zip root 203.0.113.10 22 /opt/3x-ui-deploy
ARCHIVE_INPUT="${1:-}"
REMOTE_USER="${2:-}"
REMOTE_HOST="${3:-}"
REMOTE_PORT="${4:-22}"
REMOTE_BASE_DIR="${5:-/home/$REMOTE_USER/panel}"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
DEPLOY_SCRIPT_LOCAL="${SCRIPT_DIR}/deploy_on_server.sh"
COMMON_SCRIPT_LOCAL="${SCRIPT_DIR}/common.sh"
# shellcheck source=scripts/common.sh
. "${COMMON_SCRIPT_LOCAL}"
usage() {
echo "Usage: $0 <archive_name_or_path> <remote_user> <remote_host> [remote_port] [remote_base_dir]"
echo "Example: $0 3x-ui-custom-20260219-120000.zip root 203.0.113.10 22 /opt/3x-ui-deploy"
}
if [[ "${ARCHIVE_INPUT}" == "-h" || "${ARCHIVE_INPUT}" == "--help" ]]; then
usage
exit 0
fi
if [[ -z "${ARCHIVE_INPUT}" || -z "${REMOTE_USER}" || -z "${REMOTE_HOST}" ]]; then
echo "Error: archive, remote_user, and remote_host are required."
usage
exit 1
fi
if [[ ! -f "${DEPLOY_SCRIPT_LOCAL}" ]]; then
echo "Error: ${DEPLOY_SCRIPT_LOCAL} not found."
exit 1
fi
require_cmd scp
require_cmd ssh
if [[ "${ARCHIVE_INPUT}" = /* ]]; then
ARCHIVE_PATH="${ARCHIVE_INPUT}"
else
ARCHIVE_PATH="${REPO_ROOT}/${ARCHIVE_INPUT}"
fi
if [[ ! -f "${ARCHIVE_PATH}" ]]; then
echo "Error: archive not found: ${ARCHIVE_PATH}"
exit 1
fi
ARCHIVE_NAME="$(basename "${ARCHIVE_PATH}")"
echo "Ensuring remote directory exists: ${REMOTE_BASE_DIR}"
ssh -p "${REMOTE_PORT}" "${REMOTE_USER}@${REMOTE_HOST}" "mkdir -p '${REMOTE_BASE_DIR}'"
echo "Uploading archive: ${ARCHIVE_NAME}"
scp -P "${REMOTE_PORT}" "${ARCHIVE_PATH}" "${REMOTE_USER}@${REMOTE_HOST}:${REMOTE_BASE_DIR}/"
echo "Uploading deploy script..."
scp -P "${REMOTE_PORT}" "${DEPLOY_SCRIPT_LOCAL}" "${REMOTE_USER}@${REMOTE_HOST}:${REMOTE_BASE_DIR}/"
echo "Uploading common script..."
scp -P "${REMOTE_PORT}" "${COMMON_SCRIPT_LOCAL}" "${REMOTE_USER}@${REMOTE_HOST}:${REMOTE_BASE_DIR}/"
echo
echo "Upload complete."
echo "Next, run on server:"
echo "ssh -p ${REMOTE_PORT} ${REMOTE_USER}@${REMOTE_HOST} 'cd ${REMOTE_BASE_DIR} && chmod +x deploy_on_server.sh common.sh && ./deploy_on_server.sh ${ARCHIVE_NAME}'"