Add bare-metal installer and in-app backup/restore
install.sh: no-Docker one-liner for cloud VPS - Installs Node.js 20 via NodeSource if missing (Ubuntu/Debian/RHEL/Rocky) - Clones to /opt/capsule, builds, creates a systemd service - Re-running upgrades in place (git pull + rebuild) Backup & Restore (Settings → Backup & Restore tab): - Download Backup: streams all data/ as a .tar.gz (pure Node zlib, no deps) - Restore: upload a .tar.gz to replace server data, auto sign-out after Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
08388431ac
commit
028265dd60
5 files changed
+386
-182
No files matched your search
+122
-181
@@ -1,200 +1,141 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Capsule Budget - One-Line Installer for ZimaOS/Podman/Docker
|
||||
# Capsule Budget — bare-metal installer (no Docker)
|
||||
# Usage: curl -fsSL https://raw.githubusercontent.com/nmemmert/budget/master/install.sh | bash
|
||||
|
||||
set -e
|
||||
|
||||
REPO_URL="https://github.com/nmemmert/budget.git"
|
||||
INSTALL_DIR="capsule-budget"
|
||||
INSTALL_DIR="/opt/capsule"
|
||||
SERVICE_USER="capsule"
|
||||
PORT=7654
|
||||
|
||||
echo "╔════════════════════════════════════════════════════════════╗"
|
||||
echo "║ ║"
|
||||
echo "║ 🌟 Capsule Budget Installer 🌟 ║"
|
||||
echo "║ by NeCloud ║"
|
||||
echo "║ ║"
|
||||
echo "╚════════════════════════════════════════════════════════════╝"
|
||||
echo ""
|
||||
|
||||
# Check for container engine
|
||||
echo "🔍 Checking prerequisites..."
|
||||
if command -v podman &> /dev/null; then
|
||||
CONTAINER_ENGINE="podman"
|
||||
echo "✅ Podman found"
|
||||
elif command -v docker &> /dev/null; then
|
||||
CONTAINER_ENGINE="docker"
|
||||
echo "✅ Docker found"
|
||||
else
|
||||
echo "❌ No supported container engine found"
|
||||
echo "Install Podman (recommended on Rocky Linux) or Docker"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check for compose command (Podman first)
|
||||
if command -v podman-compose &> /dev/null; then
|
||||
CONTAINER_COMPOSE="podman-compose"
|
||||
echo "✅ podman-compose found"
|
||||
elif command -v podman &> /dev/null && podman compose version &> /dev/null 2>&1; then
|
||||
CONTAINER_COMPOSE="podman compose"
|
||||
echo "✅ podman compose found"
|
||||
elif command -v docker-compose &> /dev/null; then
|
||||
CONTAINER_COMPOSE="docker-compose"
|
||||
echo "✅ docker-compose found"
|
||||
elif command -v docker &> /dev/null && docker compose version &> /dev/null 2>&1; then
|
||||
CONTAINER_COMPOSE="docker compose"
|
||||
echo "✅ docker compose found"
|
||||
else
|
||||
echo "❌ No supported compose command found"
|
||||
echo "Install one of: podman-compose, podman compose, docker-compose, docker compose"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if git is available
|
||||
if command -v git &> /dev/null; then
|
||||
USE_GIT=true
|
||||
echo "✅ Git found"
|
||||
else
|
||||
USE_GIT=false
|
||||
echo "⚠️ Git not found, will download files directly"
|
||||
fi
|
||||
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; CYAN='\033[0;36m'; NC='\033[0m'
|
||||
ok() { echo -e "${GREEN}✓${NC} $*"; }
|
||||
info() { echo -e "${CYAN}→${NC} $*"; }
|
||||
warn() { echo -e "${YELLOW}⚠${NC} $*"; }
|
||||
die() { echo -e "${RED}✗${NC} $*"; exit 1; }
|
||||
|
||||
echo ""
|
||||
echo "📦 Installing Capsule Budget..."
|
||||
echo "╔══════════════════════════════════════════╗"
|
||||
echo "║ Capsule Budget Installer ║"
|
||||
echo "╚══════════════════════════════════════════╝"
|
||||
echo ""
|
||||
|
||||
# Remove existing installation if present
|
||||
if [ -d "$INSTALL_DIR" ]; then
|
||||
echo "⚠️ Existing installation found"
|
||||
read -p "Do you want to remove it and reinstall? (y/N): " -n 1 -r
|
||||
echo
|
||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||
echo "🗑️ Removing old installation..."
|
||||
rm -rf "$INSTALL_DIR"
|
||||
else
|
||||
echo "❌ Installation cancelled"
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
# Must be root
|
||||
[ "$EUID" -eq 0 ] || die "Run as root: sudo bash install.sh (or pipe through sudo)"
|
||||
|
||||
# Clone or download repository
|
||||
if [ "$USE_GIT" = true ]; then
|
||||
echo "📥 Cloning repository..."
|
||||
git clone "$REPO_URL" "$INSTALL_DIR"
|
||||
# ── Node.js 20 ────────────────────────────────────────────────────────────────
|
||||
install_node() {
|
||||
info "Installing Node.js 20..."
|
||||
if command -v apt-get &>/dev/null; then
|
||||
curl -fsSL https://deb.nodesource.com/setup_20.x | bash -
|
||||
apt-get install -y nodejs
|
||||
elif command -v dnf &>/dev/null; then
|
||||
curl -fsSL https://rpm.nodesource.com/setup_20.x | bash -
|
||||
dnf install -y nodejs
|
||||
elif command -v yum &>/dev/null; then
|
||||
curl -fsSL https://rpm.nodesource.com/setup_20.x | bash -
|
||||
yum install -y nodejs
|
||||
else
|
||||
die "Unsupported package manager. Install Node.js 20 manually then re-run."
|
||||
fi
|
||||
}
|
||||
|
||||
if command -v node &>/dev/null; then
|
||||
NODE_MAJOR=$(node -e "process.stdout.write(process.versions.node.split('.')[0])")
|
||||
if [ "$NODE_MAJOR" -ge 20 ]; then
|
||||
ok "Node.js $(node --version) already installed"
|
||||
else
|
||||
warn "Node.js $(node --version) is too old (need 20+)"
|
||||
install_node
|
||||
fi
|
||||
else
|
||||
echo "📥 Downloading repository..."
|
||||
mkdir -p "$INSTALL_DIR"
|
||||
cd "$INSTALL_DIR"
|
||||
|
||||
# Download essential files
|
||||
curl -fsSL "https://raw.githubusercontent.com/nmemmert/budget/master/docker-compose.yml" -o docker-compose.yml
|
||||
curl -fsSL "https://raw.githubusercontent.com/nmemmert/budget/master/Dockerfile" -o Dockerfile
|
||||
curl -fsSL "https://raw.githubusercontent.com/nmemmert/budget/master/.env.example" -o .env.example
|
||||
curl -fsSL "https://raw.githubusercontent.com/nmemmert/budget/master/package.json" -o package.json
|
||||
|
||||
# Note: Full source code still needs to be downloaded
|
||||
echo "⚠️ For full installation, Git is recommended"
|
||||
echo "Downloading complete source archive..."
|
||||
curl -fsSL "https://github.com/nmemmert/budget/archive/refs/heads/master.tar.gz" | tar -xz --strip-components=1
|
||||
|
||||
cd ..
|
||||
install_node
|
||||
fi
|
||||
ok "Node.js $(node --version)"
|
||||
|
||||
# ── git ───────────────────────────────────────────────────────────────────────
|
||||
command -v git &>/dev/null || {
|
||||
info "Installing git..."
|
||||
if command -v apt-get &>/dev/null; then apt-get install -y git
|
||||
elif command -v dnf &>/dev/null; then dnf install -y git
|
||||
else yum install -y git; fi
|
||||
}
|
||||
ok "git $(git --version | awk '{print $3}')"
|
||||
|
||||
# ── Clone / update ────────────────────────────────────────────────────────────
|
||||
if [ -d "$INSTALL_DIR/.git" ]; then
|
||||
info "Updating existing installation at $INSTALL_DIR..."
|
||||
git -C "$INSTALL_DIR" pull --ff-only
|
||||
else
|
||||
info "Cloning to $INSTALL_DIR..."
|
||||
git clone "$REPO_URL" "$INSTALL_DIR"
|
||||
fi
|
||||
|
||||
# ── System user ───────────────────────────────────────────────────────────────
|
||||
if ! id "$SERVICE_USER" &>/dev/null; then
|
||||
info "Creating system user '$SERVICE_USER'..."
|
||||
useradd --system --no-create-home --shell /usr/sbin/nologin "$SERVICE_USER"
|
||||
fi
|
||||
|
||||
# ── Build ─────────────────────────────────────────────────────────────────────
|
||||
info "Installing dependencies and building..."
|
||||
cd "$INSTALL_DIR"
|
||||
npm ci --prefer-offline 2>&1 | tail -3
|
||||
npm run build 2>&1 | tail -5
|
||||
ok "Build complete"
|
||||
|
||||
# Setup environment
|
||||
echo ""
|
||||
echo "⚙️ Setting up environment..."
|
||||
# ── Data directory ────────────────────────────────────────────────────────────
|
||||
mkdir -p "$INSTALL_DIR/data"
|
||||
chown -R "$SERVICE_USER:$SERVICE_USER" "$INSTALL_DIR"
|
||||
ok "Data directory: $INSTALL_DIR/data"
|
||||
|
||||
if [ ! -f .env ]; then
|
||||
echo "📝 Creating .env file..."
|
||||
cp .env.example .env
|
||||
|
||||
# Generate encryption key
|
||||
echo "🔑 Generating encryption key..."
|
||||
|
||||
if command -v openssl &> /dev/null; then
|
||||
# Use openssl if available
|
||||
ENCRYPTION_KEY=$(openssl rand -base64 32)
|
||||
elif [ -r /dev/urandom ]; then
|
||||
# Fallback to /dev/urandom
|
||||
ENCRYPTION_KEY=$(head -c 32 /dev/urandom | base64 | tr -d '\n')
|
||||
else
|
||||
# Last resort: generate from random data
|
||||
ENCRYPTION_KEY=$(cat /dev/random | head -c 32 | base64 | tr -d '\n' 2>/dev/null || echo "PLEASE-CHANGE-THIS-$(date +%s)-$(( RANDOM * RANDOM ))")
|
||||
echo "⚠️ Warning: Could not generate optimal random key"
|
||||
echo " Please update ENCRYPTION_KEY in .env with a secure random value"
|
||||
fi
|
||||
|
||||
# Update .env with generated key (escape special chars for sed)
|
||||
ESCAPED_KEY=$(echo "$ENCRYPTION_KEY" | sed 's/[&/\\]/\\&/g')
|
||||
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
sed -i '' "s|change-this-to-a-secure-random-key|$ESCAPED_KEY|" .env
|
||||
else
|
||||
sed -i "s|change-this-to-a-secure-random-key|$ESCAPED_KEY|" .env
|
||||
fi
|
||||
echo "✅ Generated secure encryption key"
|
||||
# ── Systemd service ───────────────────────────────────────────────────────────
|
||||
SERVICE_FILE="/etc/systemd/system/capsule.service"
|
||||
cat > "$SERVICE_FILE" <<EOF
|
||||
[Unit]
|
||||
Description=Capsule Budget
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=$SERVICE_USER
|
||||
WorkingDirectory=$INSTALL_DIR
|
||||
ExecStart=$(command -v node) node_modules/.bin/next start -p $PORT
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
Environment=NODE_ENV=production
|
||||
Environment=PORT=$PORT
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl enable capsule
|
||||
systemctl restart capsule
|
||||
ok "systemd service 'capsule' enabled and started"
|
||||
|
||||
# ── Done ──────────────────────────────────────────────────────────────────────
|
||||
sleep 2
|
||||
if systemctl is-active --quiet capsule; then
|
||||
LOCAL_IP=$(hostname -I 2>/dev/null | awk '{print $1}' || echo "your-server-ip")
|
||||
echo ""
|
||||
echo -e "${GREEN}╔══════════════════════════════════════════╗"
|
||||
echo "║ Installation complete! ║"
|
||||
echo -e "╚══════════════════════════════════════════╝${NC}"
|
||||
echo ""
|
||||
echo " Access Capsule at:"
|
||||
echo " http://$LOCAL_IP:$PORT"
|
||||
echo ""
|
||||
echo " Useful commands:"
|
||||
echo " systemctl status capsule # check status"
|
||||
echo " systemctl restart capsule # restart"
|
||||
echo " journalctl -u capsule -f # view logs"
|
||||
echo ""
|
||||
echo " Data lives at: $INSTALL_DIR/data/"
|
||||
echo " Back up that directory to move to a new server."
|
||||
echo ""
|
||||
else
|
||||
echo "ℹ️ .env file already exists, keeping existing configuration"
|
||||
fi
|
||||
|
||||
# Create data directory
|
||||
mkdir -p data
|
||||
chmod 0777 data || true
|
||||
echo "✅ Data directory created"
|
||||
|
||||
# Build and start
|
||||
echo ""
|
||||
echo "🔨 Building container image..."
|
||||
$CONTAINER_COMPOSE build
|
||||
|
||||
echo ""
|
||||
echo "🚀 Starting Capsule Budget..."
|
||||
$CONTAINER_COMPOSE up -d
|
||||
|
||||
# Wait for container
|
||||
echo "⏳ Waiting for container to start..."
|
||||
sleep 5
|
||||
|
||||
# Verify container is running
|
||||
if $CONTAINER_COMPOSE ps | grep -q "Up"; then
|
||||
echo ""
|
||||
echo "╔════════════════════════════════════════════════════════════╗"
|
||||
echo "║ ║"
|
||||
echo "║ ✅ Installation Complete! ✅ ║"
|
||||
echo "║ ║"
|
||||
echo "╚════════════════════════════════════════════════════════════╝"
|
||||
echo ""
|
||||
echo "📊 Capsule Budget is running!"
|
||||
echo ""
|
||||
echo "🌐 Access your budget app at:"
|
||||
echo " • http://localhost:7654"
|
||||
if command -v hostname &> /dev/null; then
|
||||
LOCAL_IP=$(hostname -I 2>/dev/null | awk '{print $1}' || echo "your-device-ip")
|
||||
echo " • http://$LOCAL_IP:7654"
|
||||
fi
|
||||
echo ""
|
||||
echo "📁 Installation directory: $(pwd)"
|
||||
echo "💾 Data storage: $(pwd)/data"
|
||||
echo ""
|
||||
echo "🔧 Useful commands (run from $(pwd)):"
|
||||
echo " $CONTAINER_COMPOSE logs -f # View logs"
|
||||
echo " $CONTAINER_COMPOSE stop # Stop Capsule"
|
||||
echo " $CONTAINER_COMPOSE restart # Restart Capsule"
|
||||
echo " $CONTAINER_COMPOSE down # Stop and remove"
|
||||
echo ""
|
||||
echo "📚 Documentation:"
|
||||
echo " • README.md - Getting started guide"
|
||||
echo " • DEPLOYMENT.md - Deployment details"
|
||||
echo ""
|
||||
echo "🎉 Happy budgeting with Capsule!"
|
||||
echo ""
|
||||
else
|
||||
echo ""
|
||||
echo "❌ Container failed to start"
|
||||
echo ""
|
||||
echo "Troubleshooting:"
|
||||
echo "1. Check logs: $CONTAINER_COMPOSE logs"
|
||||
echo "2. Verify .env file has ENCRYPTION_KEY set"
|
||||
echo "3. Check if port 7654 is available"
|
||||
echo ""
|
||||
exit 1
|
||||
die "Service failed to start. Check logs: journalctl -u capsule -xe"
|
||||
fi
|
||||
Reference in new issue
Block a user