Add local-network deployment automation and tighten runtime defaults
This commit adds the first complete local-network deployment path for the project. It normalizes the runtime contract around a fixed container listener on 0.0.0.0:10000, binds the published compose port to 127.0.0.1, and keeps the image/build workflow aligned with the released container image. It also introduces an installation script, an nginx reverse-proxy template, and a safer SQLite backup flow based on sqlite3 .backup with retention and optional rclone upload support. Deployment-oriented configuration has been consolidated into .env.example, repository-local .env files are now ignored, and the deployment scripts are executable. In addition, the frontend mixed-content issue is fixed by switching the stylesheet reference to a root-relative static path, with tests updated to cover the regression. README guidance has been expanded to document the new install, nginx, backup, and restore conventions.
This commit is contained in:
+69
-13
@@ -1,27 +1,83 @@
|
||||
#!/usr/bin/env sh
|
||||
set -eu
|
||||
|
||||
PROJECT_ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
|
||||
cd "$PROJECT_ROOT"
|
||||
APP_DIR="__APP_DIR__"
|
||||
DEFAULT_BACKUP_DIR="__BACKUP_DIR__"
|
||||
ENV_FILE="$APP_DIR/.env"
|
||||
|
||||
if [ ! -f ".env" ] && [ -f ".env.example" ]; then
|
||||
echo "未找到 .env,先从 .env.example 复制一份:"
|
||||
echo " cp .env.example .env"
|
||||
require_command() {
|
||||
if ! command -v "$1" >/dev/null 2>&1; then
|
||||
echo "Missing required command: $1" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
resolve_path() {
|
||||
case "$1" in
|
||||
/*) printf '%s\n' "$1" ;;
|
||||
*) printf '%s/%s\n' "$APP_DIR" "$1" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
if [ ! -f "$ENV_FILE" ]; then
|
||||
echo "Deployed .env file not found: $ENV_FILE" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
DATA_DIR_VALUE=$(grep '^DATA_DIR=' .env 2>/dev/null | tail -n 1 | cut -d '=' -f 2- || true)
|
||||
DATA_DIR=${DATA_DIR_VALUE:-./data}
|
||||
DB_PATH="$DATA_DIR/app.db"
|
||||
set -a
|
||||
. "$ENV_FILE"
|
||||
set +a
|
||||
|
||||
require_command sqlite3
|
||||
|
||||
if [ -n "${BACKUP_REMOTE:-}" ]; then
|
||||
require_command rclone
|
||||
fi
|
||||
|
||||
BACKUP_DIR=${BACKUP_DIR:-$DEFAULT_BACKUP_DIR}
|
||||
DATA_DIR=${DATA_DIR:-./data}
|
||||
DB_PATH="$(resolve_path "$DATA_DIR")/app.db"
|
||||
|
||||
if [ ! -f "$DB_PATH" ]; then
|
||||
echo "未找到数据库文件:$DB_PATH"
|
||||
echo "Database file not found: $DB_PATH" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p backups
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
TIMESTAMP=$(date +"%Y%m%d-%H%M%S")
|
||||
DESTINATION="backups/app-$TIMESTAMP.db"
|
||||
TMP_BACKUP="$BACKUP_DIR/.app-$TIMESTAMP.db.tmp"
|
||||
FINAL_BACKUP="$BACKUP_DIR/app-$TIMESTAMP.db"
|
||||
|
||||
cp "$DB_PATH" "$DESTINATION"
|
||||
echo "备份已创建:$DESTINATION"
|
||||
cleanup() {
|
||||
rm -f "$TMP_BACKUP"
|
||||
}
|
||||
|
||||
trap cleanup EXIT INT TERM
|
||||
|
||||
# Prefer sqlite3 .backup so the snapshot stays transactionally consistent without
|
||||
# stopping the running container or racing with SQLite writes.
|
||||
sqlite3 "$DB_PATH" <<EOF
|
||||
.timeout 5000
|
||||
.backup $TMP_BACKUP
|
||||
EOF
|
||||
|
||||
mv "$TMP_BACKUP" "$FINAL_BACKUP"
|
||||
trap - EXIT INT TERM
|
||||
|
||||
count=0
|
||||
for backup_file in $(find "$BACKUP_DIR" -maxdepth 1 -type f -name 'app-*.db' | sort -r); do
|
||||
count=$((count + 1))
|
||||
if [ "$count" -gt 5 ]; then
|
||||
rm -f "$backup_file"
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -n "${BACKUP_REMOTE:-}" ]; then
|
||||
remote_target=${BACKUP_REMOTE%/}/$(basename "$FINAL_BACKUP")
|
||||
rclone copyto "$FINAL_BACKUP" "$remote_target"
|
||||
echo "Backup uploaded to remote: $remote_target"
|
||||
else
|
||||
echo "BACKUP_REMOTE is empty; skipping remote upload"
|
||||
fi
|
||||
|
||||
echo "Backup created: $FINAL_BACKUP"
|
||||
|
||||
+17
-11
@@ -5,33 +5,39 @@ PROJECT_ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
|
||||
cd "$PROJECT_ROOT"
|
||||
|
||||
if [ ! -f ".env" ] && [ -f ".env.example" ]; then
|
||||
echo "未找到 .env,先从 .env.example 复制一份:"
|
||||
echo ".env not found. Create it first from .env.example:"
|
||||
echo " cp .env.example .env"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
DATA_DIR_VALUE=$(grep '^DATA_DIR=' .env 2>/dev/null | tail -n 1 | cut -d '=' -f 2- || true)
|
||||
DATA_DIR=${DATA_DIR_VALUE:-./data}
|
||||
set -a
|
||||
. ./.env
|
||||
set +a
|
||||
|
||||
DATA_DIR=${DATA_DIR:-./data}
|
||||
APP_PORT=${APP_PORT:-10000}
|
||||
|
||||
mkdir -p "$DATA_DIR"
|
||||
|
||||
echo "[1/4] 拉取最新代码(如果当前目录是 git 仓库)"
|
||||
echo "[1/4] Pull latest code if this directory is a git repository"
|
||||
if [ -d ".git" ]; then
|
||||
git pull --ff-only
|
||||
else
|
||||
echo "跳过:当前目录不是 git 仓库"
|
||||
echo "Skipped: current directory is not a git repository"
|
||||
fi
|
||||
|
||||
echo "[2/4] 构建并更新容器"
|
||||
docker compose up -d --build
|
||||
echo "[2/4] Pull and update containers"
|
||||
docker compose pull web
|
||||
docker compose up -d
|
||||
|
||||
echo "[3/4] 当前容器状态"
|
||||
echo "[3/4] Current container status"
|
||||
docker compose ps
|
||||
|
||||
echo "[4/4] 最近日志"
|
||||
echo "[4/4] Recent logs"
|
||||
docker compose logs --tail=50 web
|
||||
|
||||
echo
|
||||
echo "部署完成。应用默认地址:"
|
||||
echo " http://localhost:$(grep '^PORT=' .env 2>/dev/null | tail -n 1 | cut -d '=' -f 2- || echo 10000)"
|
||||
echo "Deployment complete. Default application URLs:"
|
||||
echo " https://${HOST_DOMAIN:-localhost}"
|
||||
echo " Backend port mapping: localhost:$APP_PORT -> container:10000"
|
||||
|
||||
|
||||
Executable
+137
@@ -0,0 +1,137 @@
|
||||
#!/usr/bin/env sh
|
||||
set -eu
|
||||
|
||||
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||||
PROJECT_ROOT=$(CDPATH= cd -- "$SCRIPT_DIR/.." && pwd)
|
||||
SOURCE_ENV="$PROJECT_ROOT/.env"
|
||||
COMPOSE_SOURCE="$PROJECT_ROOT/docker-compose.yml"
|
||||
BACKUP_TEMPLATE="$SCRIPT_DIR/backup_db.sh"
|
||||
NGINX_TEMPLATE="$SCRIPT_DIR/nginx/moving-helper.nginx.template"
|
||||
NGINX_SITE_NAME="moving-helper-nginx"
|
||||
CRON_MARKER="# moving-helper-backup"
|
||||
|
||||
require_command() {
|
||||
if ! command -v "$1" >/dev/null 2>&1; then
|
||||
echo "Missing required command: $1" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
run_as_root() {
|
||||
if [ "$(id -u)" -eq 0 ]; then
|
||||
"$@"
|
||||
elif command -v sudo >/dev/null 2>&1; then
|
||||
sudo "$@"
|
||||
else
|
||||
echo "This step requires root privileges, but sudo is not available: $*" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
escape_sed_replacement() {
|
||||
printf '%s' "$1" | sed 's/[|&]/\\&/g'
|
||||
}
|
||||
|
||||
render_template() {
|
||||
src=$1
|
||||
dst=$2
|
||||
host_domain_escaped=$(escape_sed_replacement "$HOST_DOMAIN")
|
||||
ssl_path_escaped=$(escape_sed_replacement "$SSL_PATH")
|
||||
app_port_escaped=$(escape_sed_replacement "$APP_PORT")
|
||||
app_dir_escaped=$(escape_sed_replacement "$APP_DIR")
|
||||
backup_dir_escaped=$(escape_sed_replacement "$BACKUP_DIR")
|
||||
|
||||
sed \
|
||||
-e "s|__HOST_DOMAIN__|$host_domain_escaped|g" \
|
||||
-e "s|__SSL_PATH__|$ssl_path_escaped|g" \
|
||||
-e "s|__APP_PORT__|$app_port_escaped|g" \
|
||||
-e "s|__APP_DIR__|$app_dir_escaped|g" \
|
||||
-e "s|__BACKUP_DIR__|$backup_dir_escaped|g" \
|
||||
"$src" > "$dst"
|
||||
}
|
||||
|
||||
if [ ! -f "$SOURCE_ENV" ]; then
|
||||
echo "Missing $SOURCE_ENV" >&2
|
||||
echo "Create it first: cp .env.example .env" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
require_command docker
|
||||
require_command crontab
|
||||
|
||||
if ! docker compose version >/dev/null 2>&1; then
|
||||
echo "The docker compose plugin is not available in the current environment" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
set -a
|
||||
. "$SOURCE_ENV"
|
||||
set +a
|
||||
|
||||
HOST_DOMAIN=${HOST_DOMAIN:-}
|
||||
SSL_PATH=${SSL_PATH:-}
|
||||
APP_DIR=${APP_DIR:-$HOME/.local/share/moving-helper}
|
||||
BACKUP_DIR=${BACKUP_DIR:-$HOME/.local/backup/moving-helper}
|
||||
APP_PORT=${APP_PORT:-10000}
|
||||
DATA_DIR=${DATA_DIR:-./data}
|
||||
|
||||
if [ -z "$HOST_DOMAIN" ]; then
|
||||
echo "HOST_DOMAIN is not configured" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$SSL_PATH" ]; then
|
||||
echo "SSL_PATH is not configured" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$APP_DIR" "$BACKUP_DIR" "$APP_DIR/logs"
|
||||
|
||||
case "$DATA_DIR" in
|
||||
/*) mkdir -p "$DATA_DIR" ;;
|
||||
*) mkdir -p "$APP_DIR/$DATA_DIR" ;;
|
||||
esac
|
||||
|
||||
cp "$COMPOSE_SOURCE" "$APP_DIR/docker-compose.yml"
|
||||
cp "$SOURCE_ENV" "$APP_DIR/.env"
|
||||
|
||||
rendered_backup=$(mktemp)
|
||||
rendered_nginx=$(mktemp)
|
||||
trap 'rm -f "$rendered_backup" "$rendered_nginx"' EXIT INT TERM
|
||||
|
||||
render_template "$BACKUP_TEMPLATE" "$rendered_backup"
|
||||
install -m 0755 "$rendered_backup" "$APP_DIR/backup_db.sh"
|
||||
|
||||
render_template "$NGINX_TEMPLATE" "$rendered_nginx"
|
||||
run_as_root install -d /etc/nginx/sites-available /etc/nginx/sites-enabled
|
||||
run_as_root install -m 0644 "$rendered_nginx" "/etc/nginx/sites-available/$NGINX_SITE_NAME"
|
||||
run_as_root ln -sfn "/etc/nginx/sites-available/$NGINX_SITE_NAME" "/etc/nginx/sites-enabled/$NGINX_SITE_NAME"
|
||||
|
||||
run_as_root nginx -t
|
||||
if command -v systemctl >/dev/null 2>&1; then
|
||||
run_as_root systemctl reload nginx
|
||||
else
|
||||
run_as_root service nginx reload
|
||||
fi
|
||||
|
||||
(
|
||||
cd "$APP_DIR"
|
||||
docker compose pull web
|
||||
docker compose up -d
|
||||
)
|
||||
|
||||
cron_tmp=$(mktemp)
|
||||
existing_cron=$(mktemp)
|
||||
trap 'rm -f "$rendered_backup" "$rendered_nginx" "$cron_tmp" "$existing_cron"' EXIT INT TERM
|
||||
|
||||
crontab -l 2>/dev/null > "$existing_cron" || true
|
||||
grep -v "moving-helper-backup" "$existing_cron" > "$cron_tmp" || true
|
||||
printf '10 2 * * * %s/backup_db.sh >> %s/logs/backup.log 2>&1 %s\n' "$APP_DIR" "$APP_DIR" "$CRON_MARKER" >> "$cron_tmp"
|
||||
crontab "$cron_tmp"
|
||||
|
||||
echo "Installation complete."
|
||||
echo "- Application directory: $APP_DIR"
|
||||
echo "- Backup directory: $BACKUP_DIR"
|
||||
echo "- Nginx config: /etc/nginx/sites-available/$NGINX_SITE_NAME"
|
||||
echo "- URL: https://$HOST_DOMAIN"
|
||||
echo "- Scheduled backup: daily at 02:10 via the current user's crontab"
|
||||
@@ -0,0 +1,29 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name __HOST_DOMAIN__;
|
||||
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl;
|
||||
http2 on;
|
||||
server_name __HOST_DOMAIN__;
|
||||
|
||||
ssl_certificate __SSL_PATH__/fullchain.pem;
|
||||
ssl_certificate_key __SSL_PATH__/privkey.key;
|
||||
|
||||
client_max_body_size 0;
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:__APP_PORT__;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto https;
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
proxy_set_header X-Forwarded-Port 443;
|
||||
proxy_read_timeout 300;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user