diff --git a/README.md b/README.md
index 739c252..7513c78 100644
--- a/README.md
+++ b/README.md
@@ -19,11 +19,15 @@ docker compose --profile production up -d
# Using Bun
bun run setup && bun run dev
-# Using LXC on Proxmox
+# Using LXC Containers
+# For Proxmox VE (online)
curl -fsSL https://raw.githubusercontent.com/arunavo4/gitea-mirror/main/scripts/gitea-mirror-lxc-installer.sh | bash
+
+# For local testing (offline-friendly)
+sudo LOCAL_REPO_DIR=~/Development/gitea-mirror ./scripts/gitea-mirror-lxc-local.sh
````
-See the [LXC Container Deployment Guide](scripts/README-lxc.md).
+See the [LXC Container Deployment Guide](docs/LXC.md).
@@ -163,23 +167,39 @@ docker compose --profile production up -d
See [Docker build documentation](./scripts/README-docker.md) for more details.
-##### Using LXC Containers (for Proxmox Homelab Setups)
+##### Using LXC Containers
-Gitea Mirror can be deployed on Proxmox LXC containers, which is ideal for homelab setups:
+Gitea Mirror offers two deployment options for LXC containers:
+
+**1. Proxmox VE (online, recommended for production)**
```bash
-# One-command installation on an Ubuntu 22.04 LXC container
+# One-command installation on Proxmox VE
+# Optional env overrides: CTID HOSTNAME STORAGE DISK_SIZE CORES MEMORY BRIDGE IP_CONF
curl -fsSL https://raw.githubusercontent.com/arunavo4/gitea-mirror/main/scripts/gitea-mirror-lxc-installer.sh | bash
```
-The installer script:
-- Downloads the Gitea Mirror repository
-- Installs all dependencies including Bun
-- Builds the application
-- Sets up a systemd service
-- Starts the application
+**2. Local testing (offline-friendly, works on developer laptops)**
-See the [LXC Container Deployment Guide](scripts/README-lxc.md) for detailed instructions.
+```bash
+# Download the script
+curl -fsSL https://raw.githubusercontent.com/arunavo4/gitea-mirror/main/scripts/gitea-mirror-lxc-local.sh -o gitea-mirror-lxc-local.sh
+chmod +x gitea-mirror-lxc-local.sh
+
+# Run with your local repo directory
+sudo LOCAL_REPO_DIR=~/Development/gitea-mirror ./gitea-mirror-lxc-local.sh
+```
+
+Both scripts:
+- Set up a privileged Ubuntu 22.04 LXC container
+- Install Bun runtime environment
+- Build the application
+- Configure a systemd service
+- Start the service automatically
+
+The application includes a health check endpoint at `/api/health` for monitoring.
+
+See the [LXC Container Deployment Guide](docs/LXC.md) for detailed instructions.
##### Building Your Own Image
@@ -379,7 +399,7 @@ docker compose -f docker-compose.dev.yml up -d
- **Backend**: Bun
- **Database**: SQLite (handles both data storage and event notifications)
- **API Integration**: GitHub API (Octokit), Gitea API
-- **Deployment Options**: Docker containers, Proxmox LXC containers
+- **Deployment Options**: Docker containers, LXC containers (Proxmox VE and local testing)
## Contributing
diff --git a/docs/LXC.md b/docs/LXC.md
new file mode 100644
index 0000000..482d89b
--- /dev/null
+++ b/docs/LXC.md
@@ -0,0 +1,131 @@
+# LXC Container Deployment Guide
+
+## Overview
+Run **Gitea Mirror** in an isolated LXC container, either:
+
+1. **Online, on a Proxmox VE host** – script pulls everything from GitHub
+2. **Offline / LAN-only, on a developer laptop** – script pushes your local checkout + Bun ZIP
+
+---
+
+## 1. Proxmox VE (online, recommended for prod)
+
+### Prerequisites
+* Proxmox VE node with the default `vmbr0` bridge
+* Root shell on the node
+* Ubuntu 22.04 LXC template present (`pveam update && pveam download ...`)
+
+### One-command install
+
+```bash
+# optional env overrides: CTID HOSTNAME STORAGE DISK_SIZE CORES MEMORY BRIDGE IP_CONF
+sudo bash -c "$(curl -fsSL https://raw.githubusercontent.com/arunavo4/gitea-mirror/main/scripts/gitea-mirror-lxc-installer.sh)"
+```
+
+What it does:
+
+* Creates **privileged** CT `$CTID` with nesting enabled
+* Installs curl / git / Bun (official installer)
+* Clones & builds `arunavo4/gitea-mirror`
+* Writes a root-run systemd service and starts it
+* Prints the container IP + random `JWT_SECRET`
+
+Browse to:
+
+```
+http://:4321
+```
+
+---
+
+## 2. Local testing (LXD on a workstation, works offline)
+
+### Prerequisites
+
+* `lxd` installed (`sudo apt install lxd`; `lxd init --auto`)
+* Your repo cloned locally – e.g. `~/Development/gitea-mirror`
+* Bun ZIP downloaded once:
+ `https://github.com/oven-sh/bun/releases/latest/download/bun-linux-x64.zip`
+
+### Offline installer script
+
+```bash
+git clone https://github.com/arunavo4/gitea-mirror.git # if not already
+curl -fsSL https://raw.githubusercontent.com/arunavo4/gitea-mirror/main/scripts/gitea-mirror-lxc-local.sh -o gitea-mirror-lxc-local.sh
+chmod +x gitea-mirror-lxc-local.sh
+
+sudo LOCAL_REPO_DIR=~/Development/gitea-mirror \
+ ./gitea-mirror-lxc-local.sh
+```
+
+What it does:
+
+* Launches privileged LXC `gitea-test` (`lxc launch ubuntu:22.04 ...`)
+* Pushes **Bun ZIP** + tarred **local repo** into `/opt`
+* Unpacks, builds, initializes DB
+* Symlinks both `bun` and `bunx` → `/usr/local/bin`
+* Creates a root systemd unit and starts it
+
+Access from host:
+
+```
+http://$(lxc exec gitea-test -- hostname -I | awk '{print $1}'):4321
+```
+
+(Optional) forward to host localhost:
+
+```bash
+sudo lxc config device add gitea-test mirror proxy \
+ listen=tcp:0.0.0.0:4321 connect=tcp:127.0.0.1:4321
+```
+
+---
+
+## Health-check endpoint
+
+Gitea Mirror includes a built-in health check endpoint at `/api/health` that provides:
+
+- System status and uptime
+- Database connectivity check
+- Memory usage statistics
+- Environment information
+
+You can use this endpoint for monitoring your deployment:
+
+```bash
+# Basic check (returns 200 OK if healthy)
+curl -I http://:4321/api/health
+
+# Detailed health information (JSON)
+curl http://:4321/api/health
+```
+
+---
+
+## Troubleshooting
+
+| Check | Command |
+| -------------- | ----------------------------------------------------- |
+| Service status | `systemctl status gitea-mirror` |
+| Live logs | `journalctl -u gitea-mirror -f` |
+| Verify Bun | `bun --version && bunx --version` |
+| DB perms | `chown -R root:root /opt/gitea-mirror/data` (Proxmox) |
+
+---
+
+## Connecting LXC and Docker Containers
+
+If you need your LXC container to communicate with Docker containers:
+
+1. On your host machine, create a bridge network:
+ ```bash
+ docker network create gitea-network
+ ```
+
+2. Find the bridge interface created by Docker:
+ ```bash
+ ip a | grep docker
+ # Look for something like docker0 or br-xxxxxxxx
+ ```
+
+3. In Proxmox, edit the LXC container's network configuration to use this bridge.
diff --git a/scripts/gitea-mirror-lxc-installer.sh b/scripts/gitea-mirror-lxc-installer.sh
index 4cf2d23..44568fa 100755
--- a/scripts/gitea-mirror-lxc-installer.sh
+++ b/scripts/gitea-mirror-lxc-installer.sh
@@ -1,266 +1,97 @@
-#!/bin/bash
-# Gitea Mirror LXC Container Installer
-# This is a self-contained script to install Gitea Mirror in an LXC container
-# Usage: curl -fsSL https://raw.githubusercontent.com/arunavo4/gitea-mirror/main/scripts/gitea-mirror-lxc-installer.sh | bash
+#!/usr/bin/env bash
+# gitea-mirror-proxmox.sh
+# Fully online installer for a Proxmox LXC guest running Gitea Mirror + Bun.
-set -e
+set -euo pipefail
+
+# ────── adjustable defaults ──────────────────────────────────────────────
+CTID=${CTID:-106} # container ID
+HOSTNAME=${HOSTNAME:-gitea-mirror}
+STORAGE=${STORAGE:-local-lvm} # where rootfs lives
+DISK_SIZE=${DISK_SIZE:-8G}
+CORES=${CORES:-2}
+MEMORY=${MEMORY:-2048} # MiB
+BRIDGE=${BRIDGE:-vmbr0}
+IP_CONF=${IP_CONF:-dhcp} # or "192.168.1.240/24,gw=192.168.1.1"
-# Configuration variables - change these as needed
-INSTALL_DIR="/opt/gitea-mirror"
-REPO_URL="https://github.com/arunavo4/gitea-mirror.git"
-SERVICE_USER="gitea-mirror"
PORT=4321
+JWT_SECRET=$(openssl rand -hex 32)
-# Color codes for better readability
-RED='\033[0;31m'
-GREEN='\033[0;32m'
-YELLOW='\033[1;33m'
-BLUE='\033[0;34m'
-NC='\033[0m' # No Color
+REPO="https://github.com/arunavo4/gitea-mirror.git"
+# ─────────────────────────────────────────────────────────────────────────
-# Print banner
-echo -e "${BLUE}"
-echo "╔════════════════════════════════════════════════════════════╗"
-echo "║ ║"
-echo "║ Gitea Mirror LXC Container Installer ║"
-echo "║ ║"
-echo "╚════════════════════════════════════════════════════════════╝"
-echo -e "${NC}"
+TEMPLATE='ubuntu-22.04-standard_22.04-1_amd64.tar.zst'
+TEMPLATE_PATH="/var/lib/vz/template/cache/${TEMPLATE}"
-# Ensure script is run as root
-if [ "$(id -u)" -ne 0 ]; then
- echo -e "${RED}This script must be run as root${NC}" >&2
- exit 1
+echo "▶️ Ensuring template exists…"
+if [[ ! -f $TEMPLATE_PATH ]]; then
+ pveam update >/dev/null
+ pveam download "$STORAGE" "$TEMPLATE"
fi
-echo -e "${GREEN}Starting Gitea Mirror installation...${NC}"
-
-# Check if we're in an LXC container
-if [ -d /proc/vz ] && [ ! -d /proc/bc ]; then
- echo -e "${YELLOW}Running in an OpenVZ container. Some features may not work.${NC}"
-elif [ -f /proc/1/environ ] && grep -q container=lxc /proc/1/environ; then
- echo -e "${GREEN}Running in an LXC container. Good!${NC}"
-else
- echo -e "${YELLOW}Not running in a container. This script is designed for LXC containers.${NC}"
- read -p "Continue anyway? (y/n) " -n 1 -r
- echo
- if [[ ! $REPLY =~ ^[Yy]$ ]]; then
- echo -e "${RED}Installation aborted.${NC}"
- exit 1
- fi
+echo "▶️ Creating container $CTID (if missing)…"
+if ! pct status "$CTID" &>/dev/null; then
+ pct create "$CTID" "$TEMPLATE_PATH" \
+ --rootfs "$STORAGE:$DISK_SIZE" \
+ --hostname "$HOSTNAME" \
+ --cores "$CORES" --memory "$MEMORY" \
+ --net0 "name=eth0,bridge=$BRIDGE,ip=$IP_CONF" \
+ --features nesting=1 \
+ --unprivileged 0
fi
-# Install dependencies
-echo -e "${BLUE}Step 1/7: Installing dependencies...${NC}"
-apt update
-apt install -y curl git sqlite3 build-essential openssl
+pct start "$CTID"
-# Create service user
-echo -e "${BLUE}Step 2/7: Creating service user...${NC}"
-if id "$SERVICE_USER" &>/dev/null; then
- echo -e "${YELLOW}User $SERVICE_USER already exists${NC}"
-else
- useradd -m -s /bin/bash "$SERVICE_USER"
- echo -e "${GREEN}Created user $SERVICE_USER${NC}"
-fi
+echo "▶️ Installing base packages inside CT $CTID…"
+pct exec "$CTID" -- bash -c 'apt update && apt install -y curl git build-essential openssl sqlite3 unzip'
-# Install Bun
-echo -e "${BLUE}Step 3/7: Installing Bun runtime...${NC}"
-if command -v bun >/dev/null 2>&1; then
- echo -e "${YELLOW}Bun is already installed${NC}"
+echo "▶️ Installing Bun runtime…"
+pct exec "$CTID" -- bash -c '
+ export BUN_INSTALL=/opt/bun
+ curl -fsSL https://bun.sh/install | bash -s -- --yes
+ ln -sf /opt/bun/bin/bun /usr/local/bin/bun
+ ln -sf /opt/bun/bin/bun /usr/local/bin/bunx
bun --version
-else
- echo -e "${GREEN}Installing Bun...${NC}"
- # Install Bun globally to make it accessible to all users
- curl -fsSL https://bun.sh/install | bash
- export BUN_INSTALL=${BUN_INSTALL:-"/root/.bun"}
- export PATH="$BUN_INSTALL/bin:$PATH"
+'
- # Make Bun accessible to all users by creating a symlink in /usr/local/bin
- echo -e "${GREEN}Making Bun accessible to all users...${NC}"
-
- # Check if /usr/local/bin is writable
- if [ ! -w /usr/local/bin ]; then
- echo -e "${RED}Error: /usr/local/bin is not writable. Please run the script as root or with sufficient permissions.${NC}"
- exit 1
- fi
-
- ln -sf "$BUN_INSTALL/bin/bun" /usr/local/bin/bun
-
- if [ $? -ne 0 ]; then
- echo -e "${RED}Error: Failed to create symlink for Bun in /usr/local/bin.${NC}"
- exit 1
- fi
-
- echo -e "${GREEN}Bun installed successfully${NC}"
- bun --version
-fi
-
-# Clone repository
-echo -e "${BLUE}Step 4/7: Downloading Gitea Mirror...${NC}"
-if [ -d "$INSTALL_DIR" ]; then
- echo -e "${YELLOW}Directory $INSTALL_DIR already exists${NC}"
- read -p "Update existing installation? (y/n) " -n 1 -r
- echo
- if [[ $REPLY =~ ^[Yy]$ ]]; then
- cd "$INSTALL_DIR"
- git pull
- echo -e "${GREEN}Repository updated${NC}"
- else
- echo -e "${YELLOW}Using existing installation${NC}"
- fi
-else
- echo -e "${GREEN}Cloning repository...${NC}"
- git clone "$REPO_URL" "$INSTALL_DIR"
- echo -e "${GREEN}Repository cloned to $INSTALL_DIR${NC}"
-fi
-
-# Set up application
-echo -e "${BLUE}Step 5/7: Setting up application...${NC}"
-cd "$INSTALL_DIR"
-
-# Create data directory with proper permissions
-mkdir -p data
-chown -R "$SERVICE_USER:$SERVICE_USER" data
-
-# Ensure the application directory has the right permissions
-echo -e "${GREEN}Setting correct permissions for application directory...${NC}"
-chown -R "$SERVICE_USER:$SERVICE_USER" "$INSTALL_DIR"
-chmod -R 755 "$INSTALL_DIR"
-
-# Install dependencies and build
-echo -e "${GREEN}Installing dependencies and building application...${NC}"
-bun install
-bun run build
-
-# Initialize database if it doesn't exist
-echo -e "${GREEN}Initializing database...${NC}"
-if [ ! -f "data/gitea-mirror.db" ]; then
+echo "▶️ Cloning & building Gitea Mirror…"
+pct exec "$CTID" -- bash -c "
+ git clone --depth=1 '$REPO' /opt/gitea-mirror || (cd /opt/gitea-mirror && git pull)
+ cd /opt/gitea-mirror
+ bun install
+ bun run build
bun run manage-db init
- chown "$SERVICE_USER:$SERVICE_USER" data/gitea-mirror.db
-fi
-
-# Generate a random JWT secret if not provided
-JWT_SECRET=${JWT_SECRET:-$(openssl rand -hex 32)}
-
-# Create systemd service
-echo -e "${BLUE}Step 6/7: Creating systemd service...${NC}"
-
-# Store Bun path in a variable for better maintainability
-# Use the global Bun path to ensure it's accessible to the service user
-BUN_PATH="/usr/local/bin/bun"
-echo -e "${GREEN}Using Bun from: $BUN_PATH${NC}"
-
-# Ensure the Bun executable is accessible to the service user
-if [ ! -f "$BUN_PATH" ]; then
- echo -e "${YELLOW}Bun not found at $BUN_PATH, creating symlink...${NC}"
-
- # Check if /usr/local/bin is writable
- if [ ! -w "$(dirname "$BUN_PATH")" ]; then
- echo -e "${RED}Error: $(dirname "$BUN_PATH") is not writable. Please run the script as root or with sufficient permissions.${NC}"
- exit 1
- fi
-
- ln -sf "$(command -v bun)" "$BUN_PATH"
-
- if [ $? -ne 0 ]; then
- echo -e "${RED}Error: Failed to create symlink for Bun in $(dirname "$BUN_PATH").${NC}"
- exit 1
- fi
-fi
-
-# Make sure the Bun executable has the right permissions
-if [ -f "$BUN_PATH" ]; then
- chmod 755 "$BUN_PATH"
-fi
+"
+echo "▶️ Creating systemd service…"
+pct exec "$CTID" -- bash -c "
cat >/etc/systemd/system/gitea-mirror.service </dev/null; then
- echo -e "${GREEN}Bun is accessible to $SERVICE_USER user${NC}"
-else
- echo -e "${YELLOW}Warning: $SERVICE_USER cannot access Bun. Fixing permissions...${NC}"
- # Make sure the Bun binary and its directory are accessible
- BUN_DIR="$(dirname "$BUN_PATH")"
- if [ -d "$BUN_DIR" ]; then
- chmod 755 "$BUN_DIR"
- fi
-
- if [ -f "$BUN_PATH" ]; then
- chmod 755 "$BUN_PATH"
- fi
-
- # Check again
- if su - "$SERVICE_USER" -c "$BUN_PATH --version" &>/dev/null; then
- echo -e "${GREEN}Fixed: Bun is now accessible to $SERVICE_USER user${NC}"
- else
- echo -e "${RED}Warning: $SERVICE_USER still cannot access Bun. Service may fail to start.${NC}"
- fi
-fi
-
-# Start service
-echo -e "${BLUE}Step 7/7: Starting service...${NC}"
systemctl daemon-reload
-systemctl enable gitea-mirror.service
-systemctl start gitea-mirror.service
+systemctl enable gitea-mirror
+systemctl restart gitea-mirror
+"
-# Check if service started successfully
-if systemctl is-active --quiet gitea-mirror.service; then
- echo -e "${GREEN}Gitea Mirror service started successfully!${NC}"
-else
- echo -e "${RED}Failed to start Gitea Mirror service. Check logs with: journalctl -u gitea-mirror${NC}"
- exit 1
-fi
+echo -e "\n🔍 Service status:"
+pct exec "$CTID" -- systemctl status gitea-mirror --no-pager | head -n15
-# Get IP address
-IP_ADDRESS=$(hostname -I | awk '{print $1}')
-
-# Print success message
-echo -e "${GREEN}"
-echo "╔════════════════════════════════════════════════════════════╗"
-echo "║ ║"
-echo "║ Gitea Mirror Installation Complete ║"
-echo "║ ║"
-echo "╚════════════════════════════════════════════════════════════╝"
-echo -e "${NC}"
-echo -e "${GREEN}Gitea Mirror is now running at: http://$IP_ADDRESS:$PORT${NC}"
-echo
-echo -e "${YELLOW}Important security information:${NC}"
-echo -e "JWT_SECRET: ${JWT_SECRET}"
-echo -e "${YELLOW}Please save this JWT_SECRET in a secure location.${NC}"
-echo
-echo -e "${BLUE}To check service status:${NC} systemctl status gitea-mirror"
-echo -e "${BLUE}To view logs:${NC} journalctl -u gitea-mirror -f"
-echo -e "${BLUE}Data directory:${NC} $INSTALL_DIR/data"
-echo
-echo -e "${YELLOW}Troubleshooting:${NC}"
-echo -e "If you encounter permission issues with Bun, try the following:"
-echo -e "1. Check if Bun is accessible: ${BLUE}su - $SERVICE_USER -c \"$BUN_PATH --version\"${NC}"
-echo -e "2. Fix permissions: ${BLUE}chmod 755 $BUN_PATH${NC}"
-echo -e "3. Create a symlink: ${BLUE}ln -sf \$(command -v bun) /usr/local/bin/bun${NC}"
-echo -e "4. Restart the service: ${BLUE}systemctl restart gitea-mirror${NC}"
-echo
-echo -e "${GREEN}Thank you for installing Gitea Mirror!${NC}"
+GUEST_IP=$(pct exec "$CTID" -- hostname -I | awk '{print $1}')
+echo -e "\n🌐 Browse to: http://$GUEST_IP:$PORT\n"
+echo "🗝️ JWT_SECRET = $JWT_SECRET"
+echo -e "\n✅ Done – Gitea Mirror is running in CT $CTID."
diff --git a/scripts/gitea-mirror-lxc-local.sh b/scripts/gitea-mirror-lxc-local.sh
new file mode 100755
index 0000000..339b62a
--- /dev/null
+++ b/scripts/gitea-mirror-lxc-local.sh
@@ -0,0 +1,86 @@
+#!/usr/bin/env bash
+# gitea-mirror-lxc-local.sh (offline, local repo, verbose)
+
+set -euo pipefail
+
+CONTAINER="gitea-test"
+IMAGE="ubuntu:22.04"
+INSTALL_DIR="/opt/gitea-mirror"
+PORT=4321
+JWT_SECRET="$(openssl rand -hex 32)"
+
+BUN_ZIP="/tmp/bun-linux-x64.zip"
+BUN_URL="https://github.com/oven-sh/bun/releases/latest/download/bun-linux-x64.zip"
+
+LOCAL_REPO_DIR="${LOCAL_REPO_DIR:-./gitea-mirror}"
+REPO_TAR="/tmp/gitea-mirror-local.tar.gz"
+
+need() { command -v "$1" >/dev/null || { echo "Missing $1"; exit 1; }; }
+need curl; need lxc; need tar; need unzip
+
+# ── build host artefacts ────────────────────────────────────────────────
+[[ -d $LOCAL_REPO_DIR ]] || { echo "❌ LOCAL_REPO_DIR not found"; exit 1; }
+[[ -f $LOCAL_REPO_DIR/package.json ]] || { echo "❌ package.json missing"; exit 1; }
+[[ -f $BUN_ZIP ]] || curl -L --retry 5 --retry-delay 5 -o "$BUN_ZIP" "$BUN_URL"
+tar -czf "$REPO_TAR" -C "$(dirname "$LOCAL_REPO_DIR")" "$(basename "$LOCAL_REPO_DIR")"
+
+# ── ensure container exists ─────────────────────────────────────────────
+lxd init --auto >/dev/null 2>&1 || true
+lxc info "$CONTAINER" >/dev/null 2>&1 || lxc launch "$IMAGE" "$CONTAINER"
+
+echo "🔧 installing base packages…"
+sudo lxc exec "$CONTAINER" -- bash -c 'set -ex; apt update; apt install -y unzip tar openssl sqlite3'
+
+echo "⬆️ pushing artefacts…"
+sudo lxc file push "$BUN_ZIP" "$CONTAINER/opt/"
+sudo lxc file push "$REPO_TAR" "$CONTAINER/opt/"
+
+echo "📦 unpacking Bun + repo…"
+sudo lxc exec "$CONTAINER" -- bash -ex <<'IN'
+cd /opt
+# Bun
+unzip -oq bun-linux-x64.zip -d bun
+BIN=$(find /opt/bun -type f -name bun -perm -111 | head -n1)
+ln -sf "$BIN" /usr/local/bin/bun # bun
+ln -sf "$BIN" /usr/local/bin/bunx # bunx shim
+# Repo
+rm -rf /opt/gitea-mirror
+mkdir -p /opt/gitea-mirror
+tar -xzf gitea-mirror-local.tar.gz --strip-components=1 -C /opt/gitea-mirror
+IN
+
+echo "🏗️ bun install / build…"
+sudo lxc exec "$CONTAINER" -- bash -ex <<'IN'
+cd /opt/gitea-mirror
+bun install
+bun run build
+bun run manage-db init
+IN
+
+echo "📝 systemd unit…"
+sudo lxc exec "$CONTAINER" -- bash -ex </etc/systemd/system/gitea-mirror.service <