mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-02-27 20:53:01 +00:00
chore(scripts): consolidate deployment tooling and cleanup artifacts
This commit is contained in:
parent
95336c6919
commit
1d25045c06
6 changed files with 242 additions and 153 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
20
scripts/common.sh
Executable 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
100
scripts/deploy_on_server.sh
Executable 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
45
scripts/package_local.sh
Executable 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
75
scripts/upload_to_server.sh
Executable 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}'"
|
||||
Loading…
Reference in a new issue