feat: finalize v0.7.0 Docker deployment and image routing
This commit is contained in:
1 parent
2cd3c2d965
commit
25577b4b83
5 files changed
+530
-233
No files matched your search
+52
-32
@@ -1,73 +1,93 @@
|
|||||||
# ==========================================
|
# ==========================================
|
||||||
# Stage 1: Builder (with optimized caching)
|
# Stage 1: Builder
|
||||||
# ==========================================
|
# ==========================================
|
||||||
FROM rust:1.89-bookworm AS builder
|
FROM rust:1.89-bookworm AS builder
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Install build dependencies
|
# Build dependencies
|
||||||
RUN apt-get update && apt-get install -y \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
pkg-config \
|
pkg-config \
|
||||||
libssl-dev \
|
libssl-dev \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Copy only Cargo files first (best caching)
|
# Dependency metadata first for Docker layer caching
|
||||||
COPY Cargo.toml Cargo.lock ./
|
COPY Cargo.toml Cargo.lock ./
|
||||||
|
|
||||||
# Create dummy source for dependency caching
|
# Dummy build to cache Rust dependencies
|
||||||
RUN mkdir -p src && \
|
RUN mkdir -p src && \
|
||||||
echo "fn main() { println!(\"dummy\"); }" > src/main.rs && \
|
printf 'fn main() {}\n' > src/main.rs && \
|
||||||
cargo build --release && \
|
cargo build --release --locked && \
|
||||||
rm -rf src target/release/deps/bzod*
|
rm -rf src
|
||||||
|
|
||||||
# Copy real source code + assets
|
# Actual application source and runtime assets
|
||||||
COPY src ./src
|
COPY src ./src
|
||||||
COPY templates ./templates
|
COPY templates ./templates
|
||||||
COPY www ./www
|
COPY www ./www
|
||||||
|
|
||||||
# Build the real application
|
# Reproducible production build
|
||||||
RUN cargo build --release
|
RUN cargo build --release --locked
|
||||||
|
|
||||||
|
|
||||||
# ==========================================
|
# ==========================================
|
||||||
# Stage 2: Runtime (slim)
|
# Stage 2: Runtime
|
||||||
# ==========================================
|
# ==========================================
|
||||||
FROM debian:bookworm-slim
|
FROM debian:bookworm-slim AS runtime
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Runtime dependencies
|
# Runtime dependencies only
|
||||||
RUN apt-get update && apt-get install -y \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
openssl \
|
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
curl \
|
curl \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Copy binary from builder
|
# Create unprivileged runtime user
|
||||||
|
RUN groupadd --gid 1000 bzod && \
|
||||||
|
useradd --uid 1000 --gid 1000 \
|
||||||
|
--create-home \
|
||||||
|
--shell /usr/sbin/nologin \
|
||||||
|
bzod
|
||||||
|
|
||||||
|
# Application binary
|
||||||
COPY --from=builder /app/target/release/bzod /usr/local/bin/bzod
|
COPY --from=builder /app/target/release/bzod /usr/local/bin/bzod
|
||||||
|
|
||||||
# Copy assets
|
# Application-owned immutable assets
|
||||||
COPY --from=builder /app/templates ./templates
|
COPY --from=builder /app/templates /app/templates
|
||||||
COPY --from=builder /app/www ./www
|
COPY --from=builder /app/www /app/www
|
||||||
|
|
||||||
# Create non-root user
|
# Persistent runtime directories.
|
||||||
RUN groupadd -g 1000 bzod && \
|
# /app/images is intentionally external/persistent in Compose.
|
||||||
useradd -u 1000 -g bzod -m -s /bin/bash bzod
|
RUN mkdir -p \
|
||||||
|
/app/data \
|
||||||
# Create data directory
|
/app/config \
|
||||||
RUN mkdir -p /app/data && \
|
/app/images && \
|
||||||
chown -R bzod:bzod /app
|
chown -R bzod:bzod \
|
||||||
|
/app/data \
|
||||||
USER bzod
|
/app/config \
|
||||||
|
/app/images \
|
||||||
|
/app/templates \
|
||||||
|
/app/www \
|
||||||
|
/usr/local/bin/bzod
|
||||||
|
|
||||||
|
# Runtime configuration
|
||||||
ENV DATA_DIR=/app/data \
|
ENV DATA_DIR=/app/data \
|
||||||
|
CONFIG_DIR=/app/config \
|
||||||
|
IMAGES_DIR=/app/images \
|
||||||
PORT=8654 \
|
PORT=8654 \
|
||||||
HOST=0.0.0.0 \
|
HOST=0.0.0.0 \
|
||||||
COOKIE_SECURE=true
|
COOKIE_SECURE=true
|
||||||
|
|
||||||
EXPOSE 8654
|
EXPOSE 8654
|
||||||
|
|
||||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \
|
HEALTHCHECK \
|
||||||
CMD curl -f http://localhost:${PORT}/status || exit 1
|
--interval=30s \
|
||||||
|
--timeout=5s \
|
||||||
|
--start-period=10s \
|
||||||
|
--retries=3 \
|
||||||
|
CMD curl -fsS "http://127.0.0.1:${PORT}/status" || exit 1
|
||||||
|
|
||||||
ENTRYPOINT ["bzod"]
|
USER bzod
|
||||||
|
|
||||||
|
ENTRYPOINT ["/usr/local/bin/bzod"]
|
||||||
CMD ["serve"]
|
CMD ["serve"]
|
||||||
@@ -1,251 +1,458 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# BZOD Production Deployment Script
|
#
|
||||||
|
# BZOD Production Docker Deployment
|
||||||
|
#
|
||||||
|
# Privacy-First URL Shortener & Landing Page Platform
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
# curl -fsSL https://bzo.in/deploy.sh | sudo bash
|
# curl -fsSL https://bzo.in/deploy.sh | sudo bash
|
||||||
|
#
|
||||||
|
# Or:
|
||||||
|
# sudo bash deploy.sh
|
||||||
|
#
|
||||||
|
# Environment overrides:
|
||||||
|
# BZOD_VERSION=0.7.0
|
||||||
|
# BZOD_IMAGE=ghcr.io/thakares/nx9-url-shortener
|
||||||
|
# BZOD_ROOT=/DATA/AppData/bzod
|
||||||
|
# BZOD_PORT=8654
|
||||||
|
#
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
BZOD_VERSION="0.7.0"
|
# ============================================================
|
||||||
|
# Configuration
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
SERVICE_USER="bzod"
|
BZOD_VERSION="${BZOD_VERSION:-0.7.0}"
|
||||||
INSTALL_PATH="/usr/local/bin/bzod"
|
BZOD_IMAGE="${BZOD_IMAGE:-ghcr.io/thakares/nx9-url-shortener}"
|
||||||
CONFIG_DIR="/etc/bzod"
|
BZOD_ROOT="${BZOD_ROOT:-/DATA/AppData/bzod}"
|
||||||
DATA_DIR="/var/lib/bzod/data"
|
BZOD_PORT="${BZOD_PORT:-8654}"
|
||||||
ENV_FILE="${CONFIG_DIR}/bzod.env"
|
|
||||||
SYSTEMD_UNIT="/etc/systemd/system/bzod.service"
|
CONTAINER_NAME="${CONTAINER_NAME:-bzod}"
|
||||||
|
|
||||||
|
DATA_DIR="${BZOD_ROOT}/data"
|
||||||
|
CONFIG_DIR="${BZOD_ROOT}/config"
|
||||||
|
IMAGES_DIR="${BZOD_ROOT}/images"
|
||||||
|
COMPOSE_DIR="${BZOD_ROOT}/compose"
|
||||||
|
|
||||||
|
COMPOSE_FILE="${COMPOSE_DIR}/docker-compose.yml"
|
||||||
|
ENV_FILE="${COMPOSE_DIR}/bzod.env"
|
||||||
|
BACKUP_ROOT="${BZOD_ROOT}/backups"
|
||||||
|
|
||||||
|
IMAGE="${BZOD_IMAGE}:${BZOD_VERSION}"
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Output
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
RED='\033[0;31m'
|
RED='\033[0;31m'
|
||||||
GREEN='\033[0;32m'
|
GREEN='\033[0;32m'
|
||||||
BLUE='\033[0;34m'
|
BLUE='\033[0;34m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
NC='\033[0m'
|
NC='\033[0m'
|
||||||
|
|
||||||
# Temporary file cleanup
|
info() {
|
||||||
TMP_BINARY=""
|
echo -e "${BLUE}$*${NC}"
|
||||||
cleanup() {
|
|
||||||
rm -f "${TMP_BINARY:-}" "${TMP_GHCR:-}"
|
|
||||||
}
|
}
|
||||||
trap cleanup EXIT
|
|
||||||
|
|
||||||
echo -e "${BLUE}=== BZOD - Privacy-First URL Shortener & Landing Page Platform ===${NC}"
|
success() {
|
||||||
echo -e "Production deployment started...\n"
|
echo -e "${GREEN}$*${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
if [ "$EUID" -ne 0 ]; then
|
warning() {
|
||||||
echo -e "${RED}Error: This script must be run as root (use sudo).${NC}"
|
echo -e "${YELLOW}$*${NC}"
|
||||||
exit 1
|
}
|
||||||
fi
|
|
||||||
|
|
||||||
# 1. Install Base Dependencies
|
error() {
|
||||||
echo -e "${BLUE}[1/8] Installing base system dependencies...${NC}"
|
echo -e "${RED}$*${NC}" >&2
|
||||||
apt-get update -qq
|
}
|
||||||
apt-get install -y openssl sqlite3 ca-certificates curl tar gzip
|
|
||||||
|
|
||||||
# 2. Install Binary (safe atomic download)
|
die() {
|
||||||
echo -e "\n${BLUE}[2/8] Installing BZOD binary...${NC}"
|
error "$*"
|
||||||
|
|
||||||
ARCH="$(uname -m)"
|
|
||||||
case $ARCH in
|
|
||||||
x86_64) BINARY_NAME="bzod-x86_64-unknown-linux-gnu" ;;
|
|
||||||
aarch64|arm64) BINARY_NAME="bzod-aarch64-unknown-linux-gnu" ;;
|
|
||||||
armv7l) BINARY_NAME="bzod-armv7-unknown-linux-gnueabihf" ;;
|
|
||||||
*) echo -e "${RED}Unsupported architecture: $ARCH${NC}"; exit 1 ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
REPO="thakares/nx9-url-shortener"
|
|
||||||
RELEASE_URL="https://github.com/${REPO}/releases/download/v${BZOD_VERSION}/${BINARY_NAME}"
|
|
||||||
|
|
||||||
TMP_BINARY=$(mktemp)
|
|
||||||
|
|
||||||
echo "Trying GitHub Releases..."
|
|
||||||
if curl --retry 5 --retry-delay 2 --retry-connrefused \
|
|
||||||
-L -f -o "${TMP_BINARY}" "${RELEASE_URL}" 2>/dev/null; then
|
|
||||||
echo -e "${GREEN}✓ Downloaded from GitHub Releases${NC}"
|
|
||||||
else
|
|
||||||
echo -e "${BLUE}GitHub Releases not available. Trying GHCR...${NC}"
|
|
||||||
if command -v docker >/dev/null 2>&1; then
|
|
||||||
TMP_GHCR=$(mktemp)
|
|
||||||
docker pull ghcr.io/${REPO}:latest >/dev/null 2>&1 || true
|
|
||||||
if docker run --rm --entrypoint cat ghcr.io/${REPO}:latest /usr/local/bin/bzod > "${TMP_GHCR}" 2>/dev/null && [ -s "${TMP_GHCR}" ]; then
|
|
||||||
mv "${TMP_GHCR}" "${TMP_BINARY}"
|
|
||||||
echo -e "${GREEN}✓ Extracted from GHCR${NC}"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ ! -s "${TMP_BINARY}" ]; then
|
|
||||||
echo -e "${BLUE}Falling back to local build...${NC}"
|
|
||||||
if ! command -v cargo >/dev/null 2>&1; then
|
|
||||||
echo -e "${RED}Neither pre-built binary nor cargo available.${NC}"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
apt-get install -y pkg-config build-essential
|
|
||||||
cargo build --release
|
|
||||||
cp target/release/bzod "${TMP_BINARY}"
|
|
||||||
echo -e "${GREEN}✓ Built from source${NC}"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Atomic replace with backup
|
|
||||||
if [ -f "${INSTALL_PATH}" ]; then
|
|
||||||
cp "${INSTALL_PATH}" "${INSTALL_PATH}.bak" 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
install -m 755 "${TMP_BINARY}" "${INSTALL_PATH}"
|
|
||||||
|
|
||||||
# Verify
|
|
||||||
if [ ! -x "${INSTALL_PATH}" ]; then
|
|
||||||
echo -e "${RED}Binary installation failed${NC}"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
"${INSTALL_PATH}" --version >/dev/null && echo -e "${GREEN}✓ Binary verified (--version)${NC}" || {
|
|
||||||
echo -e "${RED}Binary verification failed${NC}"
|
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
# Verify -V also works
|
# ============================================================
|
||||||
"${INSTALL_PATH}" -V >/dev/null && echo -e "${GREEN}✓ Binary verified (-V)${NC}" || {
|
# Root check
|
||||||
echo -e "${RED}Binary -V verification failed${NC}"
|
# ============================================================
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# Show installed version and verify it matches requested version
|
if [[ "${EUID}" -ne 0 ]]; then
|
||||||
VERSION=$("${INSTALL_PATH}" --version 2>/dev/null | head -n1 || echo "unknown")
|
die "This script must be run as root. Use: sudo bash deploy.sh"
|
||||||
EXPECTED_VERSION="bzod ${BZOD_VERSION}"
|
|
||||||
if [ "${VERSION}" != "${EXPECTED_VERSION}" ]; then
|
|
||||||
echo -e "${RED}Version mismatch: expected '${EXPECTED_VERSION}', got '${VERSION}'${NC}"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo -e "${GREEN}✓ Installed ${VERSION} (${ARCH})${NC}"
|
|
||||||
|
|
||||||
# 3. Create System User
|
|
||||||
echo -e "\n${BLUE}[3/8] Creating system user '${SERVICE_USER}'...${NC}"
|
|
||||||
if ! id -u "${SERVICE_USER}" &>/dev/null; then
|
|
||||||
useradd -r -s /usr/sbin/nologin -m -d /var/lib/bzod "${SERVICE_USER}"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 4. Setup Directories
|
echo
|
||||||
echo -e "\n${BLUE}[4/8] Setting up directories...${NC}"
|
echo -e "${BLUE}============================================================${NC}"
|
||||||
mkdir -p "${CONFIG_DIR}" "${DATA_DIR}"
|
echo -e "${BLUE} BZOD — Production Docker Deployment${NC}"
|
||||||
chown -R "${SERVICE_USER}:${SERVICE_USER}" "/var/lib/bzod"
|
echo -e "${BLUE}============================================================${NC}"
|
||||||
|
echo
|
||||||
|
echo "Version: ${BZOD_VERSION}"
|
||||||
|
echo "Image: ${IMAGE}"
|
||||||
|
echo "Application: ${BZOD_ROOT}"
|
||||||
|
echo "Data: ${DATA_DIR}"
|
||||||
|
echo "Config: ${CONFIG_DIR}"
|
||||||
|
echo "Images: ${IMAGES_DIR}"
|
||||||
|
echo "Port: ${BZOD_PORT}"
|
||||||
|
echo
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# 1. Install Docker
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
info "[1/8] Checking Docker..."
|
||||||
|
|
||||||
|
if ! command -v docker >/dev/null 2>&1; then
|
||||||
|
info "Docker is not installed. Installing Docker..."
|
||||||
|
|
||||||
|
apt-get update -qq
|
||||||
|
apt-get install -y \
|
||||||
|
ca-certificates \
|
||||||
|
curl
|
||||||
|
|
||||||
|
install -m 0755 -d /etc/apt/keyrings
|
||||||
|
|
||||||
|
if [[ ! -f /etc/apt/keyrings/docker.asc ]]; then
|
||||||
|
curl -fsSL \
|
||||||
|
https://download.docker.com/linux/debian/gpg \
|
||||||
|
-o /etc/apt/keyrings/docker.asc
|
||||||
|
|
||||||
|
chmod a+r /etc/apt/keyrings/docker.asc
|
||||||
|
fi
|
||||||
|
|
||||||
|
. /etc/os-release
|
||||||
|
|
||||||
|
echo \
|
||||||
|
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] \
|
||||||
|
https://download.docker.com/linux/debian \
|
||||||
|
${VERSION_CODENAME} stable" \
|
||||||
|
> /etc/apt/sources.list.d/docker.list
|
||||||
|
|
||||||
|
apt-get update -qq
|
||||||
|
|
||||||
|
apt-get install -y \
|
||||||
|
docker-ce \
|
||||||
|
docker-ce-cli \
|
||||||
|
containerd.io \
|
||||||
|
docker-buildx-plugin \
|
||||||
|
docker-compose-plugin
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! docker info >/dev/null 2>&1; then
|
||||||
|
systemctl enable --now docker
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! docker compose version >/dev/null 2>&1; then
|
||||||
|
die "Docker Compose plugin is unavailable."
|
||||||
|
fi
|
||||||
|
|
||||||
|
success "✓ Docker and Docker Compose available"
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# 2. Create persistent directories
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
info "[2/8] Creating persistent application directories..."
|
||||||
|
|
||||||
|
mkdir -p \
|
||||||
|
"${DATA_DIR}" \
|
||||||
|
"${CONFIG_DIR}" \
|
||||||
|
"${IMAGES_DIR}" \
|
||||||
|
"${COMPOSE_DIR}" \
|
||||||
|
"${BACKUP_ROOT}"
|
||||||
|
|
||||||
chmod 700 "${CONFIG_DIR}"
|
chmod 700 "${CONFIG_DIR}"
|
||||||
|
chmod 755 "${IMAGES_DIR}"
|
||||||
|
|
||||||
|
success "✓ Persistent directories ready"
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# 3. Configuration
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
info "[3/8] Preparing configuration..."
|
||||||
|
|
||||||
|
if [[ ! -f "${ENV_FILE}" ]]; then
|
||||||
|
|
||||||
|
cat > "${ENV_FILE}" <<EOF
|
||||||
|
BZOD_VERSION=${BZOD_VERSION}
|
||||||
|
BZOD_IMAGE=${BZOD_IMAGE}
|
||||||
|
|
||||||
# 5. Configuration (preserve on upgrades)
|
|
||||||
echo -e "\n${BLUE}[5/8] Configuration...${NC}"
|
|
||||||
if [ ! -f "${ENV_FILE}" ]; then
|
|
||||||
echo -e "${BLUE}Generating new secure configuration...${NC}"
|
|
||||||
cat <<EOF > "${ENV_FILE}"
|
|
||||||
HOST=0.0.0.0
|
HOST=0.0.0.0
|
||||||
PORT=8654
|
PORT=8654
|
||||||
DATA_DIR=${DATA_DIR}
|
|
||||||
|
DATA_DIR=/app/data
|
||||||
|
CONFIG_DIR=/app/config
|
||||||
|
IMAGES_DIR=/app/images
|
||||||
|
|
||||||
COOKIE_SECURE=true
|
COOKIE_SECURE=true
|
||||||
RUST_LOG=info
|
RUST_LOG=info
|
||||||
SESSION_SECRET=$(openssl rand -hex 32)
|
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
chmod 600 "${ENV_FILE}"
|
chmod 600 "${ENV_FILE}"
|
||||||
chown root:"${SERVICE_USER}" "${ENV_FILE}"
|
|
||||||
|
success "✓ New Docker configuration created"
|
||||||
|
|
||||||
else
|
else
|
||||||
echo -e "${GREEN}Existing configuration preserved${NC}"
|
|
||||||
|
warning "Existing Docker configuration preserved"
|
||||||
|
|
||||||
|
# Update image/version while preserving all other settings.
|
||||||
|
sed -i \
|
||||||
|
-E "s#^BZOD_VERSION=.*#BZOD_VERSION=${BZOD_VERSION}#" \
|
||||||
|
"${ENV_FILE}" || true
|
||||||
|
|
||||||
|
sed -i \
|
||||||
|
-E "s#^BZOD_IMAGE=.*#BZOD_IMAGE=${BZOD_IMAGE}#" \
|
||||||
|
"${ENV_FILE}" || true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 6. Systemd Service
|
# ============================================================
|
||||||
echo -e "\n${BLUE}[6/8] Installing hardened systemd service...${NC}"
|
# 4. Create Compose definition
|
||||||
cat <<EOF > "${SYSTEMD_UNIT}"
|
# ============================================================
|
||||||
[Unit]
|
|
||||||
Description=BZOD - Privacy-First URL Shortener & Landing Page Platform
|
|
||||||
After=network-online.target
|
|
||||||
Wants=network-online.target
|
|
||||||
|
|
||||||
[Service]
|
info "[4/8] Writing Docker Compose configuration..."
|
||||||
Type=simple
|
|
||||||
User=${SERVICE_USER}
|
|
||||||
Group=${SERVICE_USER}
|
|
||||||
WorkingDirectory=/var/lib/bzod
|
|
||||||
EnvironmentFile=${ENV_FILE}
|
|
||||||
ExecStart=${INSTALL_PATH} serve
|
|
||||||
|
|
||||||
Restart=on-failure
|
cat > "${COMPOSE_FILE}" <<'EOF'
|
||||||
RestartSec=5s
|
services:
|
||||||
|
|
||||||
# Security Hardening
|
bzod:
|
||||||
ProtectSystem=strict
|
image: ${BZOD_IMAGE}:${BZOD_VERSION}
|
||||||
ProtectHome=yes
|
container_name: bzod
|
||||||
PrivateTmp=yes
|
|
||||||
PrivateDevices=yes
|
|
||||||
ProtectKernelTunables=yes
|
|
||||||
ProtectKernelModules=yes
|
|
||||||
ProtectControlGroups=yes
|
|
||||||
ProtectHostname=yes
|
|
||||||
RestrictSUIDSGID=yes
|
|
||||||
LockPersonality=yes
|
|
||||||
NoNewPrivileges=yes
|
|
||||||
ReadWritePaths=/var/lib/bzod
|
|
||||||
|
|
||||||
[Install]
|
restart: unless-stopped
|
||||||
WantedBy=multi-user.target
|
|
||||||
|
ports:
|
||||||
|
- "${PORT:-8654}:8654"
|
||||||
|
|
||||||
|
environment:
|
||||||
|
HOST: "${HOST:-0.0.0.0}"
|
||||||
|
PORT: "${PORT:-8654}"
|
||||||
|
|
||||||
|
DATA_DIR: "/app/data"
|
||||||
|
CONFIG_DIR: "/app/config"
|
||||||
|
IMAGES_DIR: "/app/images"
|
||||||
|
|
||||||
|
COOKIE_SECURE: "${COOKIE_SECURE:-true}"
|
||||||
|
RUST_LOG: "${RUST_LOG:-info}"
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
|
||||||
|
# Persistent application databases.
|
||||||
|
- ${BZOD_ROOT}/data:/app/data
|
||||||
|
|
||||||
|
# Persistent application configuration.
|
||||||
|
- ${BZOD_ROOT}/config:/app/config
|
||||||
|
|
||||||
|
# User-uploaded / application images.
|
||||||
|
#
|
||||||
|
# IMPORTANT:
|
||||||
|
# /app/images is required by the image router.
|
||||||
|
- ${BZOD_ROOT}/images:/app/images
|
||||||
|
|
||||||
|
healthcheck:
|
||||||
|
test:
|
||||||
|
[
|
||||||
|
"CMD",
|
||||||
|
"curl",
|
||||||
|
"-fsS",
|
||||||
|
"http://127.0.0.1:8654/status"
|
||||||
|
]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 5s
|
||||||
|
start_period: 10s
|
||||||
|
retries: 3
|
||||||
|
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
chmod 644 "${SYSTEMD_UNIT}"
|
# Append BZOD_ROOT because compose needs it.
|
||||||
systemctl daemon-reload
|
if ! grep -q '^BZOD_ROOT=' "${ENV_FILE}"; then
|
||||||
|
echo "BZOD_ROOT=${BZOD_ROOT}" >> "${ENV_FILE}"
|
||||||
# 7. Initialize & Start
|
|
||||||
echo -e "\n${BLUE}[7/8] Initializing and starting service...${NC}"
|
|
||||||
|
|
||||||
# Database creation and migration is handled automatically by 'bzod serve'
|
|
||||||
if [ -f "${DATA_DIR}/admin/admin.db" ] || [ -f "${DATA_DIR}/admin.db" ]; then
|
|
||||||
echo -e "${GREEN}✓ Existing database detected (upgrade mode)${NC}"
|
|
||||||
|
|
||||||
# Pre-upgrade: stop service and backup databases
|
|
||||||
if systemctl is-active --quiet bzod 2>/dev/null; then
|
|
||||||
echo -e "${BLUE} Stopping BZOD for safe database backup...${NC}"
|
|
||||||
systemctl stop bzod
|
|
||||||
fi
|
|
||||||
|
|
||||||
BACKUP_DIR="/var/lib/bzod/pre-upgrade-backup-v${BZOD_VERSION}"
|
|
||||||
mkdir -p "${BACKUP_DIR}"
|
|
||||||
cp -a "${DATA_DIR}" "${BACKUP_DIR}/data" 2>/dev/null || true
|
|
||||||
cp "${ENV_FILE}" "${BACKUP_DIR}/bzod.env" 2>/dev/null || true
|
|
||||||
echo -e "${GREEN} ✓ Pre-upgrade backup created at ${BACKUP_DIR}${NC}"
|
|
||||||
else
|
|
||||||
echo -e "${GREEN}✓ Fresh installation (databases will be created on first start)${NC}"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
systemctl enable --now bzod
|
# Port variable expected by compose.
|
||||||
|
if ! grep -q '^PORT=' "${ENV_FILE}"; then
|
||||||
|
echo "PORT=${BZOD_PORT}" >> "${ENV_FILE}"
|
||||||
|
fi
|
||||||
|
|
||||||
# 8. Validation + Rollback
|
success "✓ Docker Compose configuration written"
|
||||||
sleep 3
|
|
||||||
|
# ============================================================
|
||||||
|
# 5. Backup existing installation
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
info "[5/8] Creating pre-upgrade backup..."
|
||||||
|
|
||||||
|
TIMESTAMP="$(date '+%Y%m%d-%H%M%S')"
|
||||||
|
BACKUP_DIR="${BACKUP_ROOT}/pre-upgrade-${TIMESTAMP}-v${BZOD_VERSION}"
|
||||||
|
|
||||||
|
mkdir -p "${BACKUP_DIR}"
|
||||||
|
|
||||||
|
if [[ -d "${DATA_DIR}" ]]; then
|
||||||
|
cp -a "${DATA_DIR}" "${BACKUP_DIR}/data"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -d "${CONFIG_DIR}" ]]; then
|
||||||
|
cp -a "${CONFIG_DIR}" "${BACKUP_DIR}/config"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -d "${IMAGES_DIR}" ]]; then
|
||||||
|
cp -a "${IMAGES_DIR}" "${BACKUP_DIR}/images"
|
||||||
|
fi
|
||||||
|
|
||||||
|
cp -a "${COMPOSE_FILE}" "${BACKUP_DIR}/docker-compose.yml"
|
||||||
|
cp -a "${ENV_FILE}" "${BACKUP_DIR}/bzod.env"
|
||||||
|
|
||||||
|
success "✓ Backup created:"
|
||||||
|
echo " ${BACKUP_DIR}"
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# 6. Pull new image
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
info "[6/8] Pulling BZOD ${BZOD_VERSION} image..."
|
||||||
|
|
||||||
|
if ! docker pull "${IMAGE}"; then
|
||||||
|
die "Unable to pull ${IMAGE}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
success "✓ Docker image downloaded"
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# 7. Deploy
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
info "[7/8] Deploying BZOD..."
|
||||||
|
|
||||||
|
cd "${COMPOSE_DIR}"
|
||||||
|
|
||||||
|
# Stop/remove the existing container through Compose.
|
||||||
|
docker compose \
|
||||||
|
--env-file "${ENV_FILE}" \
|
||||||
|
-f "${COMPOSE_FILE}" \
|
||||||
|
down \
|
||||||
|
--remove-orphans
|
||||||
|
|
||||||
|
# Start the requested image.
|
||||||
|
docker compose \
|
||||||
|
--env-file "${ENV_FILE}" \
|
||||||
|
-f "${COMPOSE_FILE}" \
|
||||||
|
up -d
|
||||||
|
|
||||||
|
success "✓ BZOD container started"
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# 8. Validation
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
info "[8/8] Validating deployment..."
|
||||||
|
|
||||||
|
sleep 5
|
||||||
|
|
||||||
|
if ! docker inspect \
|
||||||
|
--format '{{.State.Running}}' \
|
||||||
|
"${CONTAINER_NAME}" 2>/dev/null | grep -q '^true$'; then
|
||||||
|
|
||||||
|
error "BZOD container failed to start."
|
||||||
|
echo
|
||||||
|
|
||||||
|
docker compose \
|
||||||
|
--env-file "${ENV_FILE}" \
|
||||||
|
-f "${COMPOSE_FILE}" \
|
||||||
|
logs --tail=100
|
||||||
|
|
||||||
|
error
|
||||||
|
error "Deployment failed. Existing data was not removed."
|
||||||
|
error "Backup: ${BACKUP_DIR}"
|
||||||
|
|
||||||
if ! systemctl is-active --quiet bzod; then
|
|
||||||
echo -e "${RED}Service failed to start! Rolling back...${NC}"
|
|
||||||
if [ -f "${INSTALL_PATH}.bak" ]; then
|
|
||||||
install -m 755 "${INSTALL_PATH}.bak" "${INSTALL_PATH}"
|
|
||||||
systemctl restart bzod || true
|
|
||||||
fi
|
|
||||||
journalctl -u bzod -n 50 --no-pager
|
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Clean up backup on success
|
success "✓ Container is running"
|
||||||
rm -f "${INSTALL_PATH}.bak" 2>/dev/null || true
|
|
||||||
|
|
||||||
# Soft health check
|
# ------------------------------------------------------------
|
||||||
if command -v curl >/dev/null 2>&1; then
|
# Health check
|
||||||
if curl -fsS http://127.0.0.1:8654/status >/dev/null 2>&1; then
|
# ------------------------------------------------------------
|
||||||
echo -e "${GREEN}✓ HTTP health check passed${NC}"
|
|
||||||
else
|
HEALTH_OK=0
|
||||||
echo -e "${BLUE}✓ Service is running (systemd healthy)${NC}"
|
|
||||||
|
for _ in {1..12}; do
|
||||||
|
if curl -fsS \
|
||||||
|
"http://127.0.0.1:${BZOD_PORT}/status" \
|
||||||
|
>/dev/null 2>&1; then
|
||||||
|
|
||||||
|
HEALTH_OK=1
|
||||||
|
break
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ "${HEALTH_OK}" -eq 1 ]]; then
|
||||||
|
success "✓ HTTP health check passed"
|
||||||
|
else
|
||||||
|
warning "⚠ HTTP health check did not respond yet"
|
||||||
|
warning "The container is running; inspect logs if necessary:"
|
||||||
|
echo
|
||||||
|
echo " docker compose -f ${COMPOSE_FILE} logs --tail=100"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Final Message
|
# ============================================================
|
||||||
IP=$(hostname -I | awk '{print $1}' | head -n1)
|
# Verify image and binary
|
||||||
echo -e "\n${GREEN}=== BZOD Deployed Successfully! ===${NC}"
|
# ============================================================
|
||||||
echo -e "🌐 Web UI: http://${IP}:8654"
|
|
||||||
echo -e "🔑 Admin: http://${IP}:8654/admin"
|
|
||||||
echo -e "🖥 Architecture: ${ARCH}"
|
|
||||||
echo -e "📦 Version: ${VERSION}"
|
|
||||||
echo -e "\nNext step (first install):"
|
|
||||||
echo -e " sudo -u bzod bzod create-admin"
|
|
||||||
echo -e "\nCommands:"
|
|
||||||
echo -e " journalctl -u bzod -f"
|
|
||||||
echo -e " bzod doctor"
|
|
||||||
echo -e " systemctl status bzod"
|
|
||||||
|
|
||||||
echo -e "\n${GREEN}Enjoy your lightweight, privacy-first, self-hosted URL shortener!${NC}"
|
echo
|
||||||
|
info "Installed image:"
|
||||||
|
docker image inspect "${IMAGE}" \
|
||||||
|
--format ' {{.RepoTags}} ({{.Id}})' \
|
||||||
|
2>/dev/null || true
|
||||||
|
|
||||||
|
echo
|
||||||
|
info "Container:"
|
||||||
|
docker inspect "${CONTAINER_NAME}" \
|
||||||
|
--format ' {{.Name}} {{.Config.Image}}' \
|
||||||
|
2>/dev/null || true
|
||||||
|
|
||||||
|
echo
|
||||||
|
info "Persistent mounts:"
|
||||||
|
docker inspect "${CONTAINER_NAME}" \
|
||||||
|
--format '{{range .Mounts}} {{.Source}} -> {{.Destination}}{{"\n"}}{{end}}' \
|
||||||
|
2>/dev/null || true
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Final status
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo -e "${GREEN}============================================================${NC}"
|
||||||
|
echo -e "${GREEN} BZOD ${BZOD_VERSION} deployed successfully${NC}"
|
||||||
|
echo -e "${GREEN}============================================================${NC}"
|
||||||
|
echo
|
||||||
|
|
||||||
|
echo "Web UI:"
|
||||||
|
echo " http://<server-ip>:${BZOD_PORT}"
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "Persistent data:"
|
||||||
|
echo " ${DATA_DIR}"
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "Persistent images:"
|
||||||
|
echo " ${IMAGES_DIR}"
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "Docker Compose:"
|
||||||
|
echo " ${COMPOSE_FILE}"
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "Backup:"
|
||||||
|
echo " ${BACKUP_DIR}"
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "Useful commands:"
|
||||||
|
echo " docker compose -f ${COMPOSE_FILE} ps"
|
||||||
|
echo " docker compose -f ${COMPOSE_FILE} logs -f bzod"
|
||||||
|
echo " docker compose -f ${COMPOSE_FILE} restart bzod"
|
||||||
|
|
||||||
|
echo
|
||||||
|
success "Deployment complete."
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
use axum::{
|
||||||
|
extract::Path,
|
||||||
|
http::{header, HeaderValue, StatusCode},
|
||||||
|
response::{IntoResponse, Response},
|
||||||
|
};
|
||||||
|
use std::path::{Component, PathBuf};
|
||||||
|
|
||||||
|
const IMAGES_DIR: &str = "/app/images";
|
||||||
|
|
||||||
|
pub async fn image_handler(Path(path): Path<String>) -> Response {
|
||||||
|
// preview.png is reserved for social-media metadata.
|
||||||
|
// It is served from /app/www/images/preview.png,
|
||||||
|
// not from the persistent application image directory.
|
||||||
|
if path == "preview.png" {
|
||||||
|
return crate::web::pages::social_preview().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
let relative = PathBuf::from(&path);
|
||||||
|
|
||||||
|
// Prevent path traversal.
|
||||||
|
if relative.components().any(|component| {
|
||||||
|
matches!(
|
||||||
|
component,
|
||||||
|
Component::ParentDir | Component::RootDir | Component::Prefix(_)
|
||||||
|
)
|
||||||
|
}) {
|
||||||
|
return (StatusCode::BAD_REQUEST, "Invalid image path").into_response();
|
||||||
|
}
|
||||||
|
|
||||||
|
let image_path = PathBuf::from(IMAGES_DIR).join(relative);
|
||||||
|
|
||||||
|
let content = match tokio::fs::read(&image_path).await {
|
||||||
|
Ok(content) => content,
|
||||||
|
Err(_) => {
|
||||||
|
return (StatusCode::NOT_FOUND, "Image not found").into_response();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let content_type = match image_path
|
||||||
|
.extension()
|
||||||
|
.and_then(|ext| ext.to_str())
|
||||||
|
.map(|ext| ext.to_ascii_lowercase())
|
||||||
|
.as_deref()
|
||||||
|
{
|
||||||
|
Some("png") => "image/png",
|
||||||
|
Some("jpg") | Some("jpeg") => "image/jpeg",
|
||||||
|
Some("gif") => "image/gif",
|
||||||
|
Some("webp") => "image/webp",
|
||||||
|
Some("svg") => "image/svg+xml",
|
||||||
|
Some("avif") => "image/avif",
|
||||||
|
_ => "application/octet-stream",
|
||||||
|
};
|
||||||
|
|
||||||
|
let content_type = HeaderValue::from_static(content_type);
|
||||||
|
|
||||||
|
(
|
||||||
|
[
|
||||||
|
(header::CONTENT_TYPE, content_type),
|
||||||
|
(
|
||||||
|
header::CACHE_CONTROL,
|
||||||
|
HeaderValue::from_static("public, max-age=86400"),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
content,
|
||||||
|
)
|
||||||
|
.into_response()
|
||||||
|
}
|
||||||
@@ -11,3 +11,4 @@ pub mod routes;
|
|||||||
pub mod system;
|
pub mod system;
|
||||||
|
|
||||||
pub use routes::create_router;
|
pub use routes::create_router;
|
||||||
|
pub mod images;
|
||||||
+4
-2
@@ -1,5 +1,7 @@
|
|||||||
use crate::state::AppState;
|
use crate::state::AppState;
|
||||||
use crate::web::{admin, api, bulk, multi_user, pages, password_gate, qr, redirect, system};
|
use crate::web::{
|
||||||
|
admin, api, bulk, images, multi_user, pages, password_gate, qr, redirect, system,
|
||||||
|
};
|
||||||
use axum::{
|
use axum::{
|
||||||
routing::{delete, get, post, put},
|
routing::{delete, get, post, put},
|
||||||
Router,
|
Router,
|
||||||
@@ -95,7 +97,7 @@ pub fn create_router(state: AppState) -> Router {
|
|||||||
.route("/admin/pages/delete/:id", post(admin::pages_delete))
|
.route("/admin/pages/delete/:id", post(admin::pages_delete))
|
||||||
.route("/admin/analytics/url/:id", get(admin::url_analytics_get))
|
.route("/admin/analytics/url/:id", get(admin::url_analytics_get))
|
||||||
.route("/deploy.sh", get(pages::deploy_script))
|
.route("/deploy.sh", get(pages::deploy_script))
|
||||||
.route("/images/preview.png", get(pages::social_preview))
|
.route("/images/*path", get(images::image_handler))
|
||||||
.route(
|
.route(
|
||||||
"/admin/analytics/url/:id/export/csv",
|
"/admin/analytics/url/:id/export/csv",
|
||||||
get(admin::url_analytics_csv_export),
|
get(admin::url_analytics_csv_export),
|
||||||
|
|||||||
Reference in new issue
Block a user