Compare commits
34 Commits
4538ad5909
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 97733cf7b8 | |||
| 5c1fedd379 | |||
| bb18672bfc | |||
| 48a005cfbc | |||
| 94d8e290bf | |||
| 3232d6568d | |||
| 1117fb178b | |||
| e678120572 | |||
| 92f9209dcd | |||
| 33b84be0a5 | |||
| 45e40bf273 | |||
| 8acb098918 | |||
| dd413d1342 | |||
| 7ea127f9cb | |||
| 9232aeccc5 | |||
| 0200afdc0f | |||
| e0262dc88b | |||
| 107e37cb3e | |||
| 5311f0069a | |||
| af8ce0ef2b | |||
| 5f9e4d23fb | |||
| 6e7c729c5e | |||
| 37f0dcb1e7 | |||
| 402553a674 | |||
| c04c00143e | |||
| 3e6a4d1704 | |||
| 362f838f7c | |||
| a8e02ae063 | |||
| 538ee01b72 | |||
| 25885ea4f0 | |||
| a586d60682 | |||
| 59f406d3b7 | |||
| f08c715d75 | |||
| 8f1565b1af |
10
.github/workflows/deploy.yml
vendored
10
.github/workflows/deploy.yml
vendored
@@ -23,9 +23,13 @@ jobs:
|
|||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Setup Nomad CLI
|
- name: Setup Nomad CLI
|
||||||
uses: hashicorp/setup-nomad@v2
|
run: |
|
||||||
with:
|
NOMAD_VERSION="1.10.5"
|
||||||
version: '1.10.5'
|
curl -sSL https://releases.hashicorp.com/nomad/${NOMAD_VERSION}/nomad_${NOMAD_VERSION}_linux_amd64.zip -o nomad.zip
|
||||||
|
unzip nomad.zip
|
||||||
|
sudo mv nomad /usr/local/bin/
|
||||||
|
rm nomad.zip
|
||||||
|
nomad version
|
||||||
|
|
||||||
- name: Set Container Version
|
- name: Set Container Version
|
||||||
id: container_version
|
id: container_version
|
||||||
|
|||||||
@@ -18,6 +18,9 @@ RUN chmod +x /usr/local/bin/entrypoint.sh
|
|||||||
# Copy LiteFS configuration
|
# Copy LiteFS configuration
|
||||||
COPY litefs.yml /etc/litefs.yml
|
COPY litefs.yml /etc/litefs.yml
|
||||||
|
|
||||||
|
# Create mount points and data directories
|
||||||
|
RUN mkdir -p /litefs /data
|
||||||
|
|
||||||
# LiteFS becomes the supervisor.
|
# LiteFS becomes the supervisor.
|
||||||
|
|
||||||
# It will mount the FUSE fs and then execute the command defined in litefs.yml's exec section.
|
# It will mount the FUSE fs and then execute the command defined in litefs.yml's exec section.
|
||||||
|
|||||||
139
entrypoint.sh
139
entrypoint.sh
@@ -1,14 +1,10 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -e
|
|
||||||
|
|
||||||
# Configuration from environment
|
# Configuration from environment
|
||||||
SERVICE_NAME="navidrome"
|
SERVICE_NAME="navidrome"
|
||||||
# Use Nomad allocation ID for a unique service ID
|
|
||||||
SERVICE_ID="${SERVICE_NAME}-${NOMAD_ALLOC_ID:-$(hostname)}"
|
SERVICE_ID="${SERVICE_NAME}-${NOMAD_ALLOC_ID:-$(hostname)}"
|
||||||
PORT=4533
|
PORT=4533
|
||||||
CONSUL_HTTP_ADDR="${CONSUL_URL:-http://localhost:8500}"
|
CONSUL_HTTP_ADDR="${CONSUL_URL:-http://localhost:8500}"
|
||||||
NODE_IP="${ADVERTISE_IP}"
|
NODE_IP="${ADVERTISE_IP}"
|
||||||
DB_LOCK_FILE="/data/.primary"
|
|
||||||
NAVIDROME_PID=0
|
NAVIDROME_PID=0
|
||||||
|
|
||||||
# Tags for the Primary service (Traefik enabled)
|
# Tags for the Primary service (Traefik enabled)
|
||||||
@@ -16,29 +12,43 @@ PRIMARY_TAGS='["navidrome","web","traefik.enable=true","urlprefix-/navidrome","t
|
|||||||
|
|
||||||
# --- Helper Functions ---
|
# --- Helper Functions ---
|
||||||
|
|
||||||
# Backup Database (Only on Primary)
|
# Check if this node is the LiteFS Primary
|
||||||
run_backup() {
|
check_primary() {
|
||||||
local backup_dir="/shared_data/backup"
|
local status=$(curl -s http://localhost:20202/info || echo "{}")
|
||||||
local timestamp=$(date +%Y%m%d_%H%M%S)
|
local is_primary=$(echo "$status" | jq -r 'if type == "object" then (.isPrimary // false) else false end' 2>/dev/null || echo "false")
|
||||||
local backup_file="${backup_dir}/navidrome.db_${timestamp}.bak"
|
|
||||||
|
|
||||||
echo "Backing up database to ${backup_file}..."
|
if [ "$is_primary" = "true" ]; then
|
||||||
mkdir -p "$backup_dir"
|
return 0 # We are the primary
|
||||||
|
|
||||||
if litefs export -name navidrome.db "$backup_file"; then
|
|
||||||
echo "Backup successful."
|
|
||||||
# Keep only last 7 days
|
|
||||||
find "$backup_dir" -name "navidrome.db_*.bak" -mtime +7 -delete
|
|
||||||
echo "Old backups cleaned."
|
|
||||||
else
|
|
||||||
echo "ERROR: Backup failed!"
|
|
||||||
fi
|
fi
|
||||||
|
return 1 # We are a replica
|
||||||
|
}
|
||||||
|
|
||||||
|
# Wait for LiteFS to settle and determine its role
|
||||||
|
wait_for_litefs() {
|
||||||
|
echo "Waiting for LiteFS to settle..."
|
||||||
|
local timeout=60
|
||||||
|
local count=0
|
||||||
|
while [ $count -lt $timeout ]; do
|
||||||
|
local status=$(curl -s http://localhost:20202/info || echo "null")
|
||||||
|
local is_primary_val=$(echo "$status" | jq -r 'if type == "object" then (.isPrimary // "null") else "null" end' 2>/dev/null || echo "null")
|
||||||
|
|
||||||
|
if [ "$is_primary_val" != "null" ]; then
|
||||||
|
local role="replica"
|
||||||
|
if [ "$is_primary_val" = "true" ]; then role="primary"; fi
|
||||||
|
echo "LiteFS initialized. Role: $role"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
sleep 2
|
||||||
|
count=$((count + 2))
|
||||||
|
echo -n "."
|
||||||
|
done
|
||||||
|
echo "ERROR: LiteFS failed to settle after ${timeout}s"
|
||||||
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
# Register Service with TTL Check
|
# Register Service with TTL Check
|
||||||
register_service() {
|
register_service() {
|
||||||
echo "Promoted! Registering service ${SERVICE_ID}..."
|
echo "Registering service ${SERVICE_ID} with Consul..."
|
||||||
# Convert bash list string to JSON array if needed, but PRIMARY_TAGS is already JSON-like
|
|
||||||
curl -s -X PUT "${CONSUL_HTTP_ADDR}/v1/agent/service/register" -d "{
|
curl -s -X PUT "${CONSUL_HTTP_ADDR}/v1/agent/service/register" -d "{
|
||||||
\"ID\": \"${SERVICE_ID}\",
|
\"ID\": \"${SERVICE_ID}\",
|
||||||
\"Name\": \"${SERVICE_NAME}\",
|
\"Name\": \"${SERVICE_NAME}\",
|
||||||
@@ -59,7 +69,7 @@ pass_ttl() {
|
|||||||
|
|
||||||
# Deregister Service
|
# Deregister Service
|
||||||
deregister_service() {
|
deregister_service() {
|
||||||
echo "Demoted/Stopping. Deregistering service ${SERVICE_ID}..."
|
echo "Deregistering service ${SERVICE_ID} from Consul..."
|
||||||
curl -s -X PUT "${CONSUL_HTTP_ADDR}/v1/agent/service/deregister/${SERVICE_ID}"
|
curl -s -X PUT "${CONSUL_HTTP_ADDR}/v1/agent/service/deregister/${SERVICE_ID}"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,11 +78,48 @@ start_app() {
|
|||||||
echo "Node is Primary. Starting Navidrome..."
|
echo "Node is Primary. Starting Navidrome..."
|
||||||
|
|
||||||
# Ensure shared directories exist
|
# Ensure shared directories exist
|
||||||
mkdir -p /shared_data/plugins /shared_data/cache /shared_data/backup
|
mkdir -p /shared_data/plugins /shared_data/cache /shared_data/backup /shared_data/artist_images /shared_data/artwork
|
||||||
|
|
||||||
|
# SEEDING LOGIC: If DB doesn't exist in cluster, restore from backup
|
||||||
|
if [ ! -f /data/navidrome.db ]; then
|
||||||
|
echo "Database /data/navidrome.db not found. Looking for backups to seed..."
|
||||||
|
local latest_backup=$(ls -t /shared_data/backup/navidrome.db_*.bak 2>/dev/null | head -n 1)
|
||||||
|
if [ -n "$latest_backup" ]; then
|
||||||
|
echo "Seeding from $latest_backup..."
|
||||||
|
litefs import -name navidrome.db "$latest_backup"
|
||||||
|
else
|
||||||
|
echo "No backups found. Navidrome will start with a fresh database."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Wait for LiteFS to expose the DB file
|
||||||
|
echo "Waiting for /data/navidrome.db to be exposed by LiteFS..."
|
||||||
|
local db_timeout=30
|
||||||
|
local db_count=0
|
||||||
|
while [ ! -f /data/navidrome.db ] && [ $db_count -lt $db_timeout ]; do
|
||||||
|
sleep 1
|
||||||
|
db_count=$((db_count + 1))
|
||||||
|
done
|
||||||
|
|
||||||
|
# Setup local data folder with BIND MOUNT for the DB
|
||||||
|
# This allows SQLite to create -wal/-shm files in the local writable directory
|
||||||
|
# while the main DB file is managed by LiteFS.
|
||||||
|
rm -rf /local/navidrome_data
|
||||||
|
mkdir -p /local/navidrome_data
|
||||||
|
|
||||||
|
touch /local/navidrome_data/navidrome.db
|
||||||
|
mount --bind /data/navidrome.db /local/navidrome_data/navidrome.db
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
export ND_DATAFOLDER="/local/navidrome_data"
|
||||||
|
export ND_CACHEFOLDER="/shared_data/cache"
|
||||||
|
export ND_BACKUP_PATH="/shared_data/backup"
|
||||||
|
export ND_PLUGINS_FOLDER="/shared_data/plugins"
|
||||||
|
export ND_ARTISTIMAGEFOLDER="artist_images"
|
||||||
|
|
||||||
/app/navidrome &
|
/app/navidrome &
|
||||||
NAVIDROME_PID=$!
|
NAVIDROME_PID=$!
|
||||||
echo "Navidrome started with PID ${NAVIDROME_PID}"
|
echo "Navidrome running (PID: $NAVIDROME_PID) with DataFolder at /local/navidrome_data (DB bind-mounted)"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Stop Navidrome
|
# Stop Navidrome
|
||||||
@@ -82,13 +129,13 @@ stop_app() {
|
|||||||
kill -SIGTERM "${NAVIDROME_PID}"
|
kill -SIGTERM "${NAVIDROME_PID}"
|
||||||
wait "${NAVIDROME_PID}" 2>/dev/null || true
|
wait "${NAVIDROME_PID}" 2>/dev/null || true
|
||||||
NAVIDROME_PID=0
|
NAVIDROME_PID=0
|
||||||
|
umount /local/navidrome_data/navidrome.db 2>/dev/null || true
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- Signal Handling (The Safety Net) ---
|
# --- Cleanup ---
|
||||||
# If Nomad stops the container, we stop the app and deregister.
|
|
||||||
cleanup() {
|
cleanup() {
|
||||||
echo "Caught signal, shutting down..."
|
echo "Shutting down..."
|
||||||
stop_app
|
stop_app
|
||||||
deregister_service
|
deregister_service
|
||||||
exit 0
|
exit 0
|
||||||
@@ -99,55 +146,23 @@ trap cleanup TERM INT
|
|||||||
# --- Main Loop ---
|
# --- Main Loop ---
|
||||||
|
|
||||||
echo "Starting Supervisor. Waiting for leadership settle..."
|
echo "Starting Supervisor. Waiting for leadership settle..."
|
||||||
echo "Node IP: $NODE_IP"
|
wait_for_litefs || exit 1
|
||||||
echo "Consul: $CONSUL_HTTP_ADDR"
|
|
||||||
|
|
||||||
# Small sleep to let LiteFS settle and leadership election complete
|
|
||||||
sleep 5
|
|
||||||
|
|
||||||
LAST_BACKUP_TIME=0
|
|
||||||
BACKUP_INTERVAL=86400 # 24 hours
|
|
||||||
|
|
||||||
while true; do
|
while true; do
|
||||||
# In LiteFS 0.5, .primary file exists ONLY on replicas.
|
if check_primary; then
|
||||||
if [ ! -f "$DB_LOCK_FILE" ]; then
|
|
||||||
# === WE ARE PRIMARY ===
|
# === WE ARE PRIMARY ===
|
||||||
|
|
||||||
# 1. If App is not running, start it and register
|
|
||||||
if [ "${NAVIDROME_PID}" -eq 0 ] || ! kill -0 "${NAVIDROME_PID}" 2>/dev/null; then
|
if [ "${NAVIDROME_PID}" -eq 0 ] || ! kill -0 "${NAVIDROME_PID}" 2>/dev/null; then
|
||||||
if [ "${NAVIDROME_PID}" -gt 0 ]; then
|
|
||||||
echo "CRITICAL: Navidrome crashed! Restarting..."
|
|
||||||
fi
|
|
||||||
start_app
|
start_app
|
||||||
register_service
|
register_service
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 2. Maintain the heartbeat (TTL)
|
|
||||||
pass_ttl
|
pass_ttl
|
||||||
|
|
||||||
# 3. Handle periodic backup
|
|
||||||
CURRENT_TIME=$(date +%s)
|
|
||||||
if [ $((CURRENT_TIME - LAST_BACKUP_TIME)) -ge $BACKUP_INTERVAL ]; then
|
|
||||||
run_backup
|
|
||||||
LAST_BACKUP_TIME=$CURRENT_TIME
|
|
||||||
fi
|
|
||||||
|
|
||||||
else
|
else
|
||||||
# === WE ARE REPLICA ===
|
# === WE ARE REPLICA ===
|
||||||
|
|
||||||
# If App is running (we were just demoted), stop it
|
|
||||||
if [ "${NAVIDROME_PID}" -gt 0 ]; then
|
if [ "${NAVIDROME_PID}" -gt 0 ]; then
|
||||||
echo "Lost leadership. Demoting..."
|
echo "Lost leadership. Demoting..."
|
||||||
stop_app
|
stop_app
|
||||||
deregister_service
|
deregister_service
|
||||||
# Reset backup timer so the next primary can start fresh or we start fresh if promoted again
|
|
||||||
LAST_BACKUP_TIME=0
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# No service registration exists for replicas to keep Consul clean.
|
|
||||||
fi
|
fi
|
||||||
|
sleep 10
|
||||||
# Sleep short enough to update TTL (every 5s is safe for 15s TTL)
|
|
||||||
sleep 5 &
|
|
||||||
wait $! # Wait allows the 'trap' to interrupt the sleep instantly
|
|
||||||
done
|
done
|
||||||
|
|||||||
20
litefs.yml
20
litefs.yml
@@ -8,29 +8,19 @@ data:
|
|||||||
# Use Consul for leader election
|
# Use Consul for leader election
|
||||||
lease:
|
lease:
|
||||||
type: "consul"
|
type: "consul"
|
||||||
|
candidate: true
|
||||||
|
promote: true
|
||||||
advertise-url: "http://${ADVERTISE_IP}:20202"
|
advertise-url: "http://${ADVERTISE_IP}:20202"
|
||||||
consul:
|
consul:
|
||||||
url: "${CONSUL_URL}"
|
url: "${CONSUL_URL}"
|
||||||
key: "litefs/navidrome"
|
key: "litefs/navidrome-v8"
|
||||||
|
ttl: "30s"
|
||||||
|
lock-delay: "5s"
|
||||||
|
|
||||||
# Internal HTTP API for replication
|
# Internal HTTP API for replication
|
||||||
http:
|
http:
|
||||||
addr: "0.0.0.0:20202"
|
addr: "0.0.0.0:20202"
|
||||||
|
|
||||||
# The HTTP Proxy routes traffic to handle write-forwarding
|
|
||||||
proxy:
|
|
||||||
addr: ":8080"
|
|
||||||
target: "localhost:4533"
|
|
||||||
db: "navidrome.db"
|
|
||||||
passthrough:
|
|
||||||
- "*.js"
|
|
||||||
- "*.css"
|
|
||||||
- "*.png"
|
|
||||||
- "*.jpg"
|
|
||||||
- "*.jpeg"
|
|
||||||
- "*.gif"
|
|
||||||
- "*.svg"
|
|
||||||
|
|
||||||
# Commands to run only on the primary node.
|
# Commands to run only on the primary node.
|
||||||
exec:
|
exec:
|
||||||
- cmd: "/usr/local/bin/entrypoint.sh"
|
- cmd: "/usr/local/bin/entrypoint.sh"
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
|
variable "container_sha" {
|
||||||
|
type = string
|
||||||
|
default = "045fc6e82b9ecb6bebc1f095f62498935df70bbf"
|
||||||
|
}
|
||||||
|
|
||||||
job "navidrome-litefs" {
|
job "navidrome-litefs" {
|
||||||
datacenters = ["dc1"]
|
datacenters = ["dc1"]
|
||||||
type = "service"
|
type = "service"
|
||||||
|
|
||||||
variable "container_sha" {
|
|
||||||
type = string
|
|
||||||
default = "045fc6e82b9ecb6bebc1f095f62498935df70bbf"
|
|
||||||
}
|
|
||||||
|
|
||||||
constraint {
|
constraint {
|
||||||
attribute = "${attr.kernel.name}"
|
attribute = "${attr.kernel.name}"
|
||||||
value = "linux"
|
value = "linux"
|
||||||
@@ -63,10 +63,11 @@ job "navidrome-litefs" {
|
|||||||
PORT = "8080" # Internal proxy port (unused but kept)
|
PORT = "8080" # Internal proxy port (unused but kept)
|
||||||
|
|
||||||
# Navidrome Config
|
# Navidrome Config
|
||||||
ND_DATAFOLDER = "/data"
|
ND_DATAFOLDER = "/shared_data"
|
||||||
ND_PLUGINS_FOLDER = "/shared_data/plugins"
|
ND_PLUGINS_FOLDER = "/shared_data/plugins"
|
||||||
ND_CACHEFOLDER = "/shared_data/cache"
|
ND_CACHEFOLDER = "/shared_data/cache"
|
||||||
ND_BACKUP_PATH = "/shared_data/backup"
|
ND_BACKUP_PATH = "/shared_data/backup"
|
||||||
|
ND_ARTISTIMAGEFOLDER = "artist_images"
|
||||||
ND_BACKUPSCHEDULE = ""
|
ND_BACKUPSCHEDULE = ""
|
||||||
|
|
||||||
ND_SCANSCHEDULE = "0"
|
ND_SCANSCHEDULE = "0"
|
||||||
|
|||||||
Reference in New Issue
Block a user