fix: deploy error visibility, trap cleanup, variable quoting, frontend resilience
- S10: Add warnings to silent health check failures in deploy scripts - S11: Add trap cleanup for temp dirs in deploy and tailscale scripts - S12: Quote 20+ critical unquoted variables across deploy scripts - S13: Extract hardcoded IPs to deploy-config-defaults.sh - S15: Add --memory=256m to UI container runs - F16: Remove in-memory JWT, use cookie-only auth in filebrowser client - F17: Add meta tag fallback for CSRF token in RPC client - F19: Track and clear setTimeout in AppSession on unmount Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
8e38342d53
commit
d8b753e1e4
@ -36,7 +36,7 @@ export function sanitizePath(path: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class FileBrowserClient {
|
class FileBrowserClient {
|
||||||
private token: string | null = null
|
private _authenticated = false
|
||||||
private baseUrl: string
|
private baseUrl: string
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
@ -44,7 +44,12 @@ class FileBrowserClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get isAuthenticated(): boolean {
|
get isAuthenticated(): boolean {
|
||||||
return this.token !== null
|
return this._authenticated
|
||||||
|
}
|
||||||
|
|
||||||
|
private getAuthCookie(): string | null {
|
||||||
|
const match = document.cookie.match(/(?:^|;\s*)auth=([^;]+)/)
|
||||||
|
return match ? match[1]! : null
|
||||||
}
|
}
|
||||||
|
|
||||||
async login(username = 'admin', password = 'admin'): Promise<boolean> {
|
async login(username = 'admin', password = 'admin'): Promise<boolean> {
|
||||||
@ -57,10 +62,11 @@ class FileBrowserClient {
|
|||||||
if (!res.ok) return false
|
if (!res.ok) return false
|
||||||
const text = await res.text()
|
const text = await res.text()
|
||||||
// FileBrowser returns the JWT as a plain string (possibly quoted)
|
// FileBrowser returns the JWT as a plain string (possibly quoted)
|
||||||
this.token = text.replace(/^"|"$/g, '')
|
const token = text.replace(/^"|"$/g, '')
|
||||||
// Store token as cookie for img/video/audio src requests (avoids token in URL)
|
// Store token as cookie — the only auth mechanism we use
|
||||||
const expires = new Date(Date.now() + 24 * 60 * 60 * 1000).toUTCString()
|
const expires = new Date(Date.now() + 24 * 60 * 60 * 1000).toUTCString()
|
||||||
document.cookie = `auth=${this.token}; path=/app/filebrowser; SameSite=Strict; Secure; expires=${expires}`
|
document.cookie = `auth=${token}; path=/app/filebrowser; SameSite=Strict; Secure; expires=${expires}`
|
||||||
|
this._authenticated = true
|
||||||
return true
|
return true
|
||||||
} catch {
|
} catch {
|
||||||
return false
|
return false
|
||||||
@ -69,13 +75,14 @@ class FileBrowserClient {
|
|||||||
|
|
||||||
private headers(): Record<string, string> {
|
private headers(): Record<string, string> {
|
||||||
const h: Record<string, string> = {}
|
const h: Record<string, string> = {}
|
||||||
if (this.token) h['X-Auth'] = this.token
|
const cookie = this.getAuthCookie()
|
||||||
|
if (cookie) h['X-Auth'] = cookie
|
||||||
return h
|
return h
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Ensure we're authenticated before making a request. Auto-logins if needed. */
|
/** Ensure we're authenticated before making a request. Auto-logins if needed. */
|
||||||
private async ensureAuth(): Promise<void> {
|
private async ensureAuth(): Promise<void> {
|
||||||
if (this.token) return
|
if (this._authenticated && this.getAuthCookie()) return
|
||||||
const ok = await this.login()
|
const ok = await this.login()
|
||||||
if (!ok) throw new Error('FileBrowser authentication failed — please open Cloud to log in')
|
if (!ok) throw new Error('FileBrowser authentication failed — please open Cloud to log in')
|
||||||
}
|
}
|
||||||
@ -175,7 +182,7 @@ class FileBrowserClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getUsage(): Promise<{ totalSize: number; folderCount: number; fileCount: number }> {
|
async getUsage(): Promise<{ totalSize: number; folderCount: number; fileCount: number }> {
|
||||||
if (!this.isAuthenticated) {
|
if (!this._authenticated || !this.getAuthCookie()) {
|
||||||
const ok = await this.login()
|
const ok = await this.login()
|
||||||
if (!ok) return { totalSize: 0, folderCount: 0, fileCount: 0 }
|
if (!ok) return { totalSize: 0, folderCount: 0, fileCount: 0 }
|
||||||
}
|
}
|
||||||
@ -205,7 +212,7 @@ class FileBrowserClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async readFileAsText(path: string, maxBytes = 102400): Promise<{ content: string; truncated: boolean; size: number }> {
|
async readFileAsText(path: string, maxBytes = 102400): Promise<{ content: string; truncated: boolean; size: number }> {
|
||||||
if (!this.isAuthenticated) {
|
if (!this._authenticated || !this.getAuthCookie()) {
|
||||||
const ok = await this.login()
|
const ok = await this.login()
|
||||||
if (!ok) throw new Error('FileBrowser authentication failed')
|
if (!ok) throw new Error('FileBrowser authentication failed')
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,7 +17,9 @@ export interface RPCResponse<T> {
|
|||||||
|
|
||||||
function getCsrfToken(): string | null {
|
function getCsrfToken(): string | null {
|
||||||
const match = document.cookie.match(/(?:^|;\s*)csrf_token=([^;]+)/)
|
const match = document.cookie.match(/(?:^|;\s*)csrf_token=([^;]+)/)
|
||||||
return match ? match[1]! : null
|
if (match) return match[1]!
|
||||||
|
// Fallback: check for a meta tag (useful when cookies are blocked or not yet set)
|
||||||
|
return document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') ?? null
|
||||||
}
|
}
|
||||||
|
|
||||||
class RPCClient {
|
class RPCClient {
|
||||||
|
|||||||
@ -226,6 +226,7 @@ const showModeMenu = ref(false)
|
|||||||
const autoRetryCount = ref(0)
|
const autoRetryCount = ref(0)
|
||||||
let loadTimeoutId: ReturnType<typeof setTimeout> | null = null
|
let loadTimeoutId: ReturnType<typeof setTimeout> | null = null
|
||||||
let autoRetryId: ReturnType<typeof setTimeout> | null = null
|
let autoRetryId: ReturnType<typeof setTimeout> | null = null
|
||||||
|
let iframeCheckId: ReturnType<typeof setTimeout> | null = null
|
||||||
|
|
||||||
/** Sites known to block iframes — skip the timeout and go straight to fallback */
|
/** Sites known to block iframes — skip the timeout and go straight to fallback */
|
||||||
const IFRAME_BLOCKED_APPS = new Set<string>([])
|
const IFRAME_BLOCKED_APPS = new Set<string>([])
|
||||||
@ -504,7 +505,7 @@ function onLoad() {
|
|||||||
isRefreshing.value = false
|
isRefreshing.value = false
|
||||||
autoRetryCount.value = 0
|
autoRetryCount.value = 0
|
||||||
// Check if iframe actually loaded content (same-origin only)
|
// Check if iframe actually loaded content (same-origin only)
|
||||||
setTimeout(() => {
|
iframeCheckId = setTimeout(() => {
|
||||||
try {
|
try {
|
||||||
const doc = iframeRef.value?.contentDocument
|
const doc = iframeRef.value?.contentDocument
|
||||||
if (doc) {
|
if (doc) {
|
||||||
@ -699,6 +700,7 @@ onMounted(() => {
|
|||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
if (loadTimeoutId) clearTimeout(loadTimeoutId)
|
if (loadTimeoutId) clearTimeout(loadTimeoutId)
|
||||||
if (autoRetryId) clearTimeout(autoRetryId)
|
if (autoRetryId) clearTimeout(autoRetryId)
|
||||||
|
if (iframeCheckId) clearTimeout(iframeCheckId)
|
||||||
window.removeEventListener('keydown', onKeyDown, true)
|
window.removeEventListener('keydown', onKeyDown, true)
|
||||||
window.removeEventListener('message', onMessage)
|
window.removeEventListener('message', onMessage)
|
||||||
document.removeEventListener('click', onClickOutside)
|
document.removeEventListener('click', onClickOutside)
|
||||||
|
|||||||
7
scripts/deploy-config-defaults.sh
Executable file
7
scripts/deploy-config-defaults.sh
Executable file
@ -0,0 +1,7 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Default deployment targets — override in deploy-config.sh (gitignored)
|
||||||
|
DEFAULT_PRIMARY="192.168.1.228"
|
||||||
|
DEFAULT_SECONDARY="192.168.1.198"
|
||||||
|
TAILSCALE_ARCH1="100.82.97.63"
|
||||||
|
TAILSCALE_ARCH2="100.122.84.60"
|
||||||
|
TAILSCALE_ARCH3="100.124.105.113"
|
||||||
@ -18,7 +18,10 @@ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|||||||
PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||||
TARGET_DIR="/home/archipelago/archy"
|
TARGET_DIR="/home/archipelago/archy"
|
||||||
|
|
||||||
# Load deploy config (gitignored)
|
# Load deploy config defaults (IP addresses etc.)
|
||||||
|
[ -f "$SCRIPT_DIR/deploy-config-defaults.sh" ] && . "$SCRIPT_DIR/deploy-config-defaults.sh"
|
||||||
|
|
||||||
|
# Load deploy config (gitignored — overrides defaults)
|
||||||
[ -f "$SCRIPT_DIR/deploy-config.sh" ] && . "$SCRIPT_DIR/deploy-config.sh"
|
[ -f "$SCRIPT_DIR/deploy-config.sh" ] && . "$SCRIPT_DIR/deploy-config.sh"
|
||||||
|
|
||||||
# Source pinned image versions (single source of truth)
|
# Source pinned image versions (single source of truth)
|
||||||
@ -26,14 +29,14 @@ TARGET_DIR="/home/archipelago/archy"
|
|||||||
|
|
||||||
SSH_KEY="${ARCHIPELAGO_SSH_KEY:-$HOME/.ssh/archipelago-deploy}"
|
SSH_KEY="${ARCHIPELAGO_SSH_KEY:-$HOME/.ssh/archipelago-deploy}"
|
||||||
SSH_OPTS="-o StrictHostKeyChecking=no -o ServerAliveInterval=15 -o ServerAliveCountMax=4 -o ConnectTimeout=10 -i $SSH_KEY"
|
SSH_OPTS="-o StrictHostKeyChecking=no -o ServerAliveInterval=15 -o ServerAliveCountMax=4 -o ConnectTimeout=10 -i $SSH_KEY"
|
||||||
BUILD_SOURCE="archipelago@192.168.1.228"
|
BUILD_SOURCE="archipelago@${DEFAULT_PRIMARY:-192.168.1.228}"
|
||||||
BUILD_DIR="/home/archipelago/archy"
|
BUILD_DIR="/home/archipelago/archy"
|
||||||
|
|
||||||
# Node registry
|
# Node registry
|
||||||
TAILSCALE_NODES=(
|
TAILSCALE_NODES=(
|
||||||
"archipelago@100.82.97.63"
|
"archipelago@${TAILSCALE_ARCH1:-100.82.97.63}"
|
||||||
"archipelago@archipelago-2.tail2b6225.ts.net"
|
"archipelago@archipelago-2.tail2b6225.ts.net"
|
||||||
"archipelago@100.124.105.113"
|
"archipelago@${TAILSCALE_ARCH3:-100.124.105.113}"
|
||||||
)
|
)
|
||||||
TAILSCALE_NAMES=("Arch 1" "Arch 2" "Arch 3")
|
TAILSCALE_NAMES=("Arch 1" "Arch 2" "Arch 3")
|
||||||
|
|
||||||
@ -49,6 +52,11 @@ ts() { echo "[$(date +%H:%M:%S)]"; }
|
|||||||
step_num=0
|
step_num=0
|
||||||
step() { step_num=$((step_num + 1)); echo ""; echo "$(ts) ━━━ Step $step_num: $1"; }
|
step() { step_num=$((step_num + 1)); echo ""; echo "$(ts) ━━━ Step $step_num: $1"; }
|
||||||
|
|
||||||
|
# Temp directory for intermediate files (cleaned up on exit)
|
||||||
|
TMPDIR="/tmp/archipelago-deploy-$$"
|
||||||
|
mkdir -p "$TMPDIR"
|
||||||
|
trap 'rm -rf "$TMPDIR"' EXIT
|
||||||
|
|
||||||
# ── Deploy a single node ─────────────────────────────────────────────────
|
# ── Deploy a single node ─────────────────────────────────────────────────
|
||||||
deploy_node() {
|
deploy_node() {
|
||||||
local TARGET="$1"
|
local TARGET="$1"
|
||||||
@ -232,7 +240,7 @@ deploy_node() {
|
|||||||
fi
|
fi
|
||||||
if [ -d "$SNIPPETS_DIR" ]; then
|
if [ -d "$SNIPPETS_DIR" ]; then
|
||||||
for f in "$SNIPPETS_DIR"/*.conf; do
|
for f in "$SNIPPETS_DIR"/*.conf; do
|
||||||
[ -f "$f" ] && scp $SSH_OPTS "$f" "$TARGET:/tmp/nginx-snippet-$(basename $f)" 2>/dev/null || true
|
[ -f "$f" ] && scp $SSH_OPTS "$f" "$TARGET:/tmp/nginx-snippet-$(basename "$f")" 2>/dev/null || true
|
||||||
done
|
done
|
||||||
ssh $SSH_OPTS "$TARGET" '
|
ssh $SSH_OPTS "$TARGET" '
|
||||||
for f in /tmp/nginx-snippet-*.conf; do
|
for f in /tmp/nginx-snippet-*.conf; do
|
||||||
@ -1050,7 +1058,7 @@ MANIFEST_EOF
|
|||||||
step "Post-deploy health check"
|
step "Post-deploy health check"
|
||||||
HEALTH_OK=false
|
HEALTH_OK=false
|
||||||
for i in $(seq 1 12); do
|
for i in $(seq 1 12); do
|
||||||
HEALTH=$(curl -s -o /dev/null -w '%{http_code}' --connect-timeout 5 "http://$TARGET_IP/health" 2>/dev/null || echo "000")
|
HEALTH=$(curl -s -o /dev/null -w '%{http_code}' --connect-timeout 5 "http://$TARGET_IP/health" 2>/dev/null || { echo "WARNING: Post-deploy health check failed for $TARGET_IP" >&2; echo "000"; })
|
||||||
if [ "$HEALTH" = "200" ]; then
|
if [ "$HEALTH" = "200" ]; then
|
||||||
echo " Health: OK (200) after $((i * 5))s"
|
echo " Health: OK (200) after $((i * 5))s"
|
||||||
HEALTH_OK=true
|
HEALTH_OK=true
|
||||||
|
|||||||
@ -100,10 +100,13 @@ if ! mkdir "$LOCK_DIR" 2>/dev/null; then
|
|||||||
echo "ERROR: Deploy already in progress for $TARGET_HOST (lock: $LOCK_DIR)"
|
echo "ERROR: Deploy already in progress for $TARGET_HOST (lock: $LOCK_DIR)"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
echo $$ > "$LOCK_DIR/pid"
|
echo $$ > "$LOCK_DIR"/pid
|
||||||
# Clean up lock on exit (normal, error, or signal)
|
# Temp directory for intermediate files (cleaned up on exit)
|
||||||
cleanup_lock() { rm -rf "$LOCK_DIR"; }
|
TMPDIR="/tmp/archipelago-deploy-$$"
|
||||||
trap cleanup_lock EXIT
|
mkdir -p "$TMPDIR"
|
||||||
|
# Clean up lock and temp files on exit (normal, error, or signal)
|
||||||
|
cleanup_deploy() { rm -rf "$LOCK_DIR" "$TMPDIR"; }
|
||||||
|
trap cleanup_deploy EXIT
|
||||||
|
|
||||||
# Dry run mode: show what would be deployed without executing
|
# Dry run mode: show what would be deployed without executing
|
||||||
if [[ "$DRY_RUN" == "true" ]]; then
|
if [[ "$DRY_RUN" == "true" ]]; then
|
||||||
@ -199,7 +202,7 @@ fi
|
|||||||
echo " Connected."
|
echo " Connected."
|
||||||
|
|
||||||
# Disk space pre-flight — abort if target is dangerously full
|
# Disk space pre-flight — abort if target is dangerously full
|
||||||
DISK_PCT=$(ssh $SSH_OPTS $TARGET_HOST "df / | tail -1 | awk '{print \$(NF-1)}' | tr -d '%'" 2>/dev/null)
|
DISK_PCT=$(ssh $SSH_OPTS "$TARGET_HOST" "df / | tail -1 | awk '{print \$(NF-1)}' | tr -d '%'" 2>/dev/null)
|
||||||
if [ -n "$DISK_PCT" ] && [ "$DISK_PCT" -gt 85 ] 2>/dev/null; then
|
if [ -n "$DISK_PCT" ] && [ "$DISK_PCT" -gt 85 ] 2>/dev/null; then
|
||||||
echo "ERROR: Target disk at ${DISK_PCT}% — need <85% for safe deploy. Free space and retry."
|
echo "ERROR: Target disk at ${DISK_PCT}% — need <85% for safe deploy. Free space and retry."
|
||||||
exit 1
|
exit 1
|
||||||
@ -227,7 +230,7 @@ ssh $SSH_OPTS "$TARGET_HOST" '
|
|||||||
# Pre-deploy health check (informational — warns but does not block)
|
# Pre-deploy health check (informational — warns but does not block)
|
||||||
progress "Pre-deploy health check"
|
progress "Pre-deploy health check"
|
||||||
TARGET_IP_ONLY="$(echo "$TARGET_HOST" | cut -d@ -f2)"
|
TARGET_IP_ONLY="$(echo "$TARGET_HOST" | cut -d@ -f2)"
|
||||||
PRE_HEALTH=$(curl -s -o /dev/null -w '%{http_code}' --connect-timeout 5 "http://$TARGET_IP_ONLY/health" 2>/dev/null || echo "000")
|
PRE_HEALTH=$(curl -s -o /dev/null -w '%{http_code}' --connect-timeout 5 "http://$TARGET_IP_ONLY/health" 2>/dev/null || { echo "WARNING: Pre-deploy health check failed for $TARGET_IP_ONLY" >&2; echo "000"; })
|
||||||
if [ "$PRE_HEALTH" = "200" ]; then
|
if [ "$PRE_HEALTH" = "200" ]; then
|
||||||
echo " Server health: OK (200)"
|
echo " Server health: OK (200)"
|
||||||
else
|
else
|
||||||
@ -273,7 +276,7 @@ if [ "$CANARY" = true ]; then
|
|||||||
CANARY_OK=false
|
CANARY_OK=false
|
||||||
for i in $(seq 1 12); do
|
for i in $(seq 1 12); do
|
||||||
sleep 5
|
sleep 5
|
||||||
CANARY_HEALTH=$(curl -s --max-time 5 "http://192.168.1.198/health" 2>/dev/null || echo "")
|
CANARY_HEALTH=$(curl -s --max-time 5 "http://192.168.1.198/health" 2>/dev/null || { echo "WARNING: Canary health check failed for 192.168.1.198" >&2; echo ""; })
|
||||||
if [ "$CANARY_HEALTH" = "OK" ]; then
|
if [ "$CANARY_HEALTH" = "OK" ]; then
|
||||||
echo " ✅ Canary .198 healthy after $((i * 5))s"
|
echo " ✅ Canary .198 healthy after $((i * 5))s"
|
||||||
CANARY_OK=true
|
CANARY_OK=true
|
||||||
@ -298,12 +301,12 @@ if [ "$BOTH" = true ]; then
|
|||||||
echo ""
|
echo ""
|
||||||
echo "📤 Copying to 192.168.1.198 (no rsync/cargo on that node)..."
|
echo "📤 Copying to 192.168.1.198 (no rsync/cargo on that node)..."
|
||||||
TARGET_198="archipelago@192.168.1.198"
|
TARGET_198="archipelago@192.168.1.198"
|
||||||
if ! scp $SSH_OPTS archipelago@192.168.1.228:$TARGET_DIR/core/target/release/archipelago /tmp/archipelago-both 2>/dev/null; then
|
if ! scp $SSH_OPTS "archipelago@192.168.1.228:$TARGET_DIR/core/target/release/archipelago" /tmp/archipelago-both 2>/dev/null; then
|
||||||
echo " ERROR: Failed to copy binary from .228 — is the build available?"
|
echo " ERROR: Failed to copy binary from .228 — is the build available?"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
scp $SSH_OPTS /tmp/archipelago-both "$TARGET_198:/tmp/archipelago-new"
|
scp $SSH_OPTS /tmp/archipelago-both "$TARGET_198:/tmp/archipelago-new"
|
||||||
ssh $SSH_OPTS archipelago@192.168.1.228 "cd $TARGET_DIR && tar cf - web/dist/neode-ui 2>/dev/null" | ssh $SSH_OPTS "$TARGET_198" "mkdir -p /tmp/web-deploy && cd /tmp/web-deploy && tar xf -"
|
ssh $SSH_OPTS "archipelago@192.168.1.228" "cd '$TARGET_DIR' && tar cf - web/dist/neode-ui 2>/dev/null" | ssh $SSH_OPTS "$TARGET_198" "mkdir -p /tmp/web-deploy && cd /tmp/web-deploy && tar xf -"
|
||||||
ssh $SSH_OPTS "$TARGET_198" '
|
ssh $SSH_OPTS "$TARGET_198" '
|
||||||
sudo systemctl stop archipelago
|
sudo systemctl stop archipelago
|
||||||
sudo cp /tmp/archipelago-new /usr/local/bin/archipelago
|
sudo cp /tmp/archipelago-new /usr/local/bin/archipelago
|
||||||
@ -342,7 +345,7 @@ if [ "$BOTH" = true ]; then
|
|||||||
echo " Syncing nginx snippets to 198..."
|
echo " Syncing nginx snippets to 198..."
|
||||||
ssh $SSH_OPTS "$TARGET_198" "sudo mkdir -p /etc/nginx/snippets" 2>/dev/null || true
|
ssh $SSH_OPTS "$TARGET_198" "sudo mkdir -p /etc/nginx/snippets" 2>/dev/null || true
|
||||||
for f in "$SNIPPETS_DIR"/*.conf; do
|
for f in "$SNIPPETS_DIR"/*.conf; do
|
||||||
[ -f "$f" ] && scp $SSH_OPTS "$f" "$TARGET_198:/tmp/nginx-snippet-$(basename $f)" 2>/dev/null || true
|
[ -f "$f" ] && scp $SSH_OPTS "$f" "$TARGET_198:/tmp/nginx-snippet-$(basename "$f")" 2>/dev/null || true
|
||||||
done
|
done
|
||||||
ssh $SSH_OPTS "$TARGET_198" '
|
ssh $SSH_OPTS "$TARGET_198" '
|
||||||
for f in /tmp/nginx-snippet-*.conf; do
|
for f in /tmp/nginx-snippet-*.conf; do
|
||||||
@ -432,7 +435,7 @@ MANIFEST_198_EOF
|
|||||||
HEALTH_198="fail"
|
HEALTH_198="fail"
|
||||||
for i in $(seq 1 12); do
|
for i in $(seq 1 12); do
|
||||||
sleep 5
|
sleep 5
|
||||||
HEALTH_198=$(curl -s --max-time 5 "http://192.168.1.198/health" 2>/dev/null || echo "")
|
HEALTH_198=$(curl -s --max-time 5 "http://192.168.1.198/health" 2>/dev/null || { echo "WARNING: Health check failed for 192.168.1.198" >&2; echo ""; })
|
||||||
if [ "$HEALTH_198" = "OK" ]; then
|
if [ "$HEALTH_198" = "OK" ]; then
|
||||||
echo " ✅ 192.168.1.198 deployed (health OK after $((i * 5))s)"
|
echo " ✅ 192.168.1.198 deployed (health OK after $((i * 5))s)"
|
||||||
break
|
break
|
||||||
@ -550,7 +553,7 @@ if [ "$LIVE" = true ]; then
|
|||||||
if [ -d "$SNIPPETS_DIR" ]; then
|
if [ -d "$SNIPPETS_DIR" ]; then
|
||||||
ssh $SSH_OPTS "$TARGET_HOST" "sudo mkdir -p /etc/nginx/snippets" 2>/dev/null || true
|
ssh $SSH_OPTS "$TARGET_HOST" "sudo mkdir -p /etc/nginx/snippets" 2>/dev/null || true
|
||||||
for f in "$SNIPPETS_DIR"/*.conf; do
|
for f in "$SNIPPETS_DIR"/*.conf; do
|
||||||
[ -f "$f" ] && scp $SSH_OPTS "$f" "$TARGET_HOST:/tmp/nginx-snippet-$(basename $f)" 2>/dev/null || true
|
[ -f "$f" ] && scp $SSH_OPTS "$f" "$TARGET_HOST:/tmp/nginx-snippet-$(basename "$f")" 2>/dev/null || true
|
||||||
done
|
done
|
||||||
ssh $SSH_OPTS "$TARGET_HOST" '
|
ssh $SSH_OPTS "$TARGET_HOST" '
|
||||||
for f in /tmp/nginx-snippet-*.conf; do
|
for f in /tmp/nginx-snippet-*.conf; do
|
||||||
@ -887,7 +890,7 @@ MANIFEST_EOF
|
|||||||
for c in $($DOCKER ps -a --format "{{.Names}}" 2>/dev/null | grep -i lnd-ui); do
|
for c in $($DOCKER ps -a --format "{{.Names}}" 2>/dev/null | grep -i lnd-ui); do
|
||||||
[ -n "$c" ] && $DOCKER stop "$c" 2>/dev/null; $DOCKER rm -f "$c" 2>/dev/null
|
[ -n "$c" ] && $DOCKER stop "$c" 2>/dev/null; $DOCKER rm -f "$c" 2>/dev/null
|
||||||
done
|
done
|
||||||
$DOCKER run -d --name archy-lnd-ui -p 8081:80 --restart unless-stopped lnd-ui:local
|
$DOCKER run -d --name archy-lnd-ui -p 8081:80 --memory=256m --restart unless-stopped lnd-ui:local
|
||||||
' 2>&1 | sed 's/^/ /' || true
|
' 2>&1 | sed 's/^/ /' || true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@ -901,7 +904,7 @@ MANIFEST_EOF
|
|||||||
for c in $($DOCKER ps -a --format "{{.Names}}" 2>/dev/null | grep -i electrs-ui); do
|
for c in $($DOCKER ps -a --format "{{.Names}}" 2>/dev/null | grep -i electrs-ui); do
|
||||||
[ -n "$c" ] && $DOCKER stop "$c" 2>/dev/null; $DOCKER rm -f "$c" 2>/dev/null
|
[ -n "$c" ] && $DOCKER stop "$c" 2>/dev/null; $DOCKER rm -f "$c" 2>/dev/null
|
||||||
done
|
done
|
||||||
$DOCKER run -d --name archy-electrs-ui --network host --restart unless-stopped electrs-ui:local
|
$DOCKER run -d --name archy-electrs-ui --network host --memory=256m --restart unless-stopped electrs-ui:local
|
||||||
' 2>&1 | sed 's/^/ /' || true
|
' 2>&1 | sed 's/^/ /' || true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@ -925,7 +928,7 @@ MANIFEST_EOF
|
|||||||
for c in $($DOCKER ps -a --format "{{.Names}}" 2>/dev/null | grep -i bitcoin-ui); do
|
for c in $($DOCKER ps -a --format "{{.Names}}" 2>/dev/null | grep -i bitcoin-ui); do
|
||||||
[ -n "$c" ] && $DOCKER stop "$c" 2>/dev/null; $DOCKER rm -f "$c" 2>/dev/null
|
[ -n "$c" ] && $DOCKER stop "$c" 2>/dev/null; $DOCKER rm -f "$c" 2>/dev/null
|
||||||
done
|
done
|
||||||
$DOCKER run -d --name archy-bitcoin-ui --network host --restart unless-stopped bitcoin-ui:local
|
$DOCKER run -d --name archy-bitcoin-ui --network host --memory=256m --restart unless-stopped bitcoin-ui:local
|
||||||
' 2>&1 | sed 's/^/ /' || true
|
' 2>&1 | sed 's/^/ /' || true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@ -1708,7 +1711,7 @@ LNDCONF
|
|||||||
progress "Post-deploy health check"
|
progress "Post-deploy health check"
|
||||||
HEALTH_OK=false
|
HEALTH_OK=false
|
||||||
for i in $(seq 1 12); do
|
for i in $(seq 1 12); do
|
||||||
POST_HEALTH=$(curl -s -o /dev/null -w '%{http_code}' --connect-timeout 5 "http://$TARGET_IP_ONLY/health" 2>/dev/null || echo "000")
|
POST_HEALTH=$(curl -s -o /dev/null -w '%{http_code}' --connect-timeout 5 "http://$TARGET_IP_ONLY/health" 2>/dev/null || { echo "WARNING: Post-deploy health check failed for $TARGET_IP_ONLY" >&2; echo "000"; })
|
||||||
if [ "$POST_HEALTH" = "200" ]; then
|
if [ "$POST_HEALTH" = "200" ]; then
|
||||||
echo " Health: OK (200) after $((i * 5))s"
|
echo " Health: OK (200) after $((i * 5))s"
|
||||||
HEALTH_OK=true
|
HEALTH_OK=true
|
||||||
@ -1775,7 +1778,7 @@ else
|
|||||||
echo "To test frontend dev server:"
|
echo "To test frontend dev server:"
|
||||||
echo " ssh $TARGET_HOST"
|
echo " ssh $TARGET_HOST"
|
||||||
echo " cd ~/archy/neode-ui && npm run dev -- --host 0.0.0.0"
|
echo " cd ~/archy/neode-ui && npm run dev -- --host 0.0.0.0"
|
||||||
echo " Then open: http://$(echo $TARGET_HOST | cut -d@ -f2):5173"
|
echo " Then open: http://$(echo "$TARGET_HOST" | cut -d@ -f2):5173"
|
||||||
echo ""
|
echo ""
|
||||||
echo "To deploy to live system:"
|
echo "To deploy to live system:"
|
||||||
echo " ./scripts/deploy-to-target.sh --live"
|
echo " ./scripts/deploy-to-target.sh --live"
|
||||||
|
|||||||
@ -287,7 +287,7 @@ else
|
|||||||
log "Bitcoin Knots already running"
|
log "Bitcoin Knots already running"
|
||||||
fi
|
fi
|
||||||
# Wait for Bitcoin Knots RPC to be responsive
|
# Wait for Bitcoin Knots RPC to be responsive
|
||||||
if wait_for_container "Bitcoin Knots RPC" "$DOCKER exec bitcoin-knots bitcoin-cli -rpcuser=$BITCOIN_RPC_USER -rpcpassword=$BITCOIN_RPC_PASS getblockchaininfo" 60; then
|
if wait_for_container "Bitcoin Knots RPC" "$DOCKER exec bitcoin-knots bitcoin-cli -rpcuser='$BITCOIN_RPC_USER' -rpcpassword='$BITCOIN_RPC_PASS' getblockchaininfo" 60; then
|
||||||
BITCOIN_READY=true
|
BITCOIN_READY=true
|
||||||
log "Bitcoin Knots is ready — dependent containers will proceed"
|
log "Bitcoin Knots is ready — dependent containers will proceed"
|
||||||
else
|
else
|
||||||
@ -297,9 +297,9 @@ fi
|
|||||||
track_container "bitcoin-knots"
|
track_container "bitcoin-knots"
|
||||||
|
|
||||||
# Ensure wallet exists (Bitcoin Knots no longer auto-creates a default wallet)
|
# Ensure wallet exists (Bitcoin Knots no longer auto-creates a default wallet)
|
||||||
if ! $DOCKER exec bitcoin-knots bitcoin-cli -rpcuser=$BITCOIN_RPC_USER -rpcpassword=$BITCOIN_RPC_PASS listwallets 2>/dev/null | grep -q "archipelago"; then
|
if ! $DOCKER exec bitcoin-knots bitcoin-cli "-rpcuser=$BITCOIN_RPC_USER" "-rpcpassword=$BITCOIN_RPC_PASS" listwallets 2>/dev/null | grep -q "archipelago"; then
|
||||||
$DOCKER exec bitcoin-knots bitcoin-cli -rpcuser=$BITCOIN_RPC_USER -rpcpassword=$BITCOIN_RPC_PASS loadwallet "archipelago" 2>/dev/null || \
|
$DOCKER exec bitcoin-knots bitcoin-cli "-rpcuser=$BITCOIN_RPC_USER" "-rpcpassword=$BITCOIN_RPC_PASS" loadwallet "archipelago" 2>/dev/null || \
|
||||||
$DOCKER exec bitcoin-knots bitcoin-cli -rpcuser=$BITCOIN_RPC_USER -rpcpassword=$BITCOIN_RPC_PASS createwallet "archipelago" 2>/dev/null
|
$DOCKER exec bitcoin-knots bitcoin-cli "-rpcuser=$BITCOIN_RPC_USER" "-rpcpassword=$BITCOIN_RPC_PASS" createwallet "archipelago" 2>/dev/null
|
||||||
log "Bitcoin Knots wallet 'archipelago' created/loaded"
|
log "Bitcoin Knots wallet 'archipelago' created/loaded"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@ -312,8 +312,8 @@ if ! $DOCKER ps -a --format '{{.Names}}' 2>/dev/null | grep -qE 'archy-mempool-d
|
|||||||
--health-cmd="mariadb -uroot -e 'SELECT 1' || exit 1" --health-interval=30s --health-timeout=5s --health-retries=3 \
|
--health-cmd="mariadb -uroot -e 'SELECT 1' || exit 1" --health-interval=30s --health-timeout=5s --health-retries=3 \
|
||||||
--memory=$(mem_limit archy-mempool-db) --network archy-net \
|
--memory=$(mem_limit archy-mempool-db) --network archy-net \
|
||||||
-v /var/lib/archipelago/mysql-mempool:/var/lib/mysql \
|
-v /var/lib/archipelago/mysql-mempool:/var/lib/mysql \
|
||||||
-e MYSQL_DATABASE=mempool -e MYSQL_USER=mempool -e MYSQL_PASSWORD=$MEMPOOL_DB_PASS \
|
-e MYSQL_DATABASE=mempool -e MYSQL_USER=mempool -e "MYSQL_PASSWORD=$MEMPOOL_DB_PASS" \
|
||||||
-e MYSQL_ROOT_PASSWORD=$MYSQL_ROOT_PASS \
|
-e "MYSQL_ROOT_PASSWORD=$MYSQL_ROOT_PASS" \
|
||||||
docker.io/mariadb:10.11 2>>"$LOG" || true
|
docker.io/mariadb:10.11 2>>"$LOG" || true
|
||||||
wait_for_container "Mempool MariaDB" "echo 'SELECT 1' | $DOCKER exec -i archy-mempool-db mariadb -uroot --password=\"$MYSQL_ROOT_PASS\"" 30
|
wait_for_container "Mempool MariaDB" "echo 'SELECT 1' | $DOCKER exec -i archy-mempool-db mariadb -uroot --password=\"$MYSQL_ROOT_PASS\"" 30
|
||||||
fi
|
fi
|
||||||
@ -351,7 +351,7 @@ if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q mempool-api; then
|
|||||||
-e ELECTRUM_TLS_ENABLED=false -e CORE_RPC_HOST="$TARGET_IP" -e CORE_RPC_PORT=8332 \
|
-e ELECTRUM_TLS_ENABLED=false -e CORE_RPC_HOST="$TARGET_IP" -e CORE_RPC_PORT=8332 \
|
||||||
-e "CORE_RPC_USERNAME=$BITCOIN_RPC_USER" -e "CORE_RPC_PASSWORD=$BITCOIN_RPC_PASS" \
|
-e "CORE_RPC_USERNAME=$BITCOIN_RPC_USER" -e "CORE_RPC_PASSWORD=$BITCOIN_RPC_PASS" \
|
||||||
-e DATABASE_ENABLED=true -e DATABASE_HOST="$MYSQL_CNT" -e DATABASE_DATABASE=mempool \
|
-e DATABASE_ENABLED=true -e DATABASE_HOST="$MYSQL_CNT" -e DATABASE_DATABASE=mempool \
|
||||||
-e DATABASE_USERNAME=mempool -e DATABASE_PASSWORD=$MEMPOOL_DB_PASS \
|
-e DATABASE_USERNAME=mempool -e "DATABASE_PASSWORD=$MEMPOOL_DB_PASS" \
|
||||||
docker.io/mempool/backend:v2.5.0 2>>"$LOG" || true
|
docker.io/mempool/backend:v2.5.0 2>>"$LOG" || true
|
||||||
fi
|
fi
|
||||||
track_container "mempool-api"
|
track_container "mempool-api"
|
||||||
@ -395,7 +395,7 @@ if ! $DOCKER ps -a --format '{{.Names}}' 2>/dev/null | grep -qE 'archy-btcpay-db
|
|||||||
--health-cmd="pg_isready -U postgres || exit 1" --health-interval=30s --health-timeout=5s --health-retries=3 \
|
--health-cmd="pg_isready -U postgres || exit 1" --health-interval=30s --health-timeout=5s --health-retries=3 \
|
||||||
--memory=$(mem_limit archy-btcpay-db) --network archy-net \
|
--memory=$(mem_limit archy-btcpay-db) --network archy-net \
|
||||||
-v /var/lib/archipelago/postgres-btcpay:/var/lib/postgresql/data \
|
-v /var/lib/archipelago/postgres-btcpay:/var/lib/postgresql/data \
|
||||||
-e POSTGRES_DB=btcpay -e POSTGRES_USER=btcpay -e POSTGRES_PASSWORD=$BTCPAY_DB_PASS \
|
-e POSTGRES_DB=btcpay -e POSTGRES_USER=btcpay -e "POSTGRES_PASSWORD=$BTCPAY_DB_PASS" \
|
||||||
docker.io/postgres:15-alpine 2>>"$LOG" || true
|
docker.io/postgres:15-alpine 2>>"$LOG" || true
|
||||||
wait_for_container "BTCPay PostgreSQL" "$DOCKER exec archy-btcpay-db pg_isready -U postgres" 30
|
wait_for_container "BTCPay PostgreSQL" "$DOCKER exec archy-btcpay-db pg_isready -U postgres" 30
|
||||||
fi
|
fi
|
||||||
@ -403,7 +403,7 @@ track_container "archy-btcpay-db"
|
|||||||
# Create nbxplorer DB only if postgres is running
|
# Create nbxplorer DB only if postgres is running
|
||||||
if $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -qE 'archy-btcpay-db|postgres-btcpay'; then
|
if $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -qE 'archy-btcpay-db|postgres-btcpay'; then
|
||||||
$DOCKER exec archy-btcpay-db psql -U postgres -tc "SELECT 1 FROM pg_database WHERE datname='nbxplorer'" 2>/dev/null | grep -q 1 || \
|
$DOCKER exec archy-btcpay-db psql -U postgres -tc "SELECT 1 FROM pg_database WHERE datname='nbxplorer'" 2>/dev/null | grep -q 1 || \
|
||||||
$DOCKER exec -e PGPASSWORD=$BTCPAY_DB_PASS archy-btcpay-db psql -U postgres -c "CREATE DATABASE nbxplorer;" 2>/dev/null || true
|
$DOCKER exec -e "PGPASSWORD=$BTCPAY_DB_PASS" archy-btcpay-db psql -U postgres -c "CREATE DATABASE nbxplorer;" 2>/dev/null || true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q archy-nbxplorer; then
|
if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q archy-nbxplorer; then
|
||||||
@ -418,7 +418,7 @@ if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q archy-nbxplorer; the
|
|||||||
-p 32838:32838 -v /var/lib/archipelago/nbxplorer:/data \
|
-p 32838:32838 -v /var/lib/archipelago/nbxplorer:/data \
|
||||||
-e NBXPLORER_DATADIR=/data -e NBXPLORER_NETWORK=mainnet -e NBXPLORER_CHAINS=btc \
|
-e NBXPLORER_DATADIR=/data -e NBXPLORER_NETWORK=mainnet -e NBXPLORER_CHAINS=btc \
|
||||||
-e NBXPLORER_BIND=0.0.0.0:32838 -e NBXPLORER_BTCRPCURL=http://bitcoin-knots:8332 \
|
-e NBXPLORER_BIND=0.0.0.0:32838 -e NBXPLORER_BTCRPCURL=http://bitcoin-knots:8332 \
|
||||||
-e NBXPLORER_BTCRPCUSER=$BITCOIN_RPC_USER -e NBXPLORER_BTCRPCPASSWORD=$BITCOIN_RPC_PASS \
|
-e "NBXPLORER_BTCRPCUSER=$BITCOIN_RPC_USER" -e "NBXPLORER_BTCRPCPASSWORD=$BITCOIN_RPC_PASS" \
|
||||||
-e NBXPLORER_POSTGRES='User ID=btcpay;Password=$BTCPAY_DB_PASS;Host=archy-btcpay-db;Port=5432;Database=nbxplorer;Include Error Detail=true' \
|
-e NBXPLORER_POSTGRES='User ID=btcpay;Password=$BTCPAY_DB_PASS;Host=archy-btcpay-db;Port=5432;Database=nbxplorer;Include Error Detail=true' \
|
||||||
docker.io/nicolasdorier/nbxplorer:2.6.0 2>>"$LOG" && sleep 5 || true
|
docker.io/nicolasdorier/nbxplorer:2.6.0 2>>"$LOG" && sleep 5 || true
|
||||||
fi
|
fi
|
||||||
@ -767,7 +767,7 @@ if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q immich_server; then
|
|||||||
--health-cmd="pg_isready -U postgres || exit 1" --health-interval=30s --health-timeout=5s --health-retries=3 \
|
--health-cmd="pg_isready -U postgres || exit 1" --health-interval=30s --health-timeout=5s --health-retries=3 \
|
||||||
--memory=$(mem_limit immich_postgres) --network immich-net \
|
--memory=$(mem_limit immich_postgres) --network immich-net \
|
||||||
-v /var/lib/archipelago/immich-db:/var/lib/postgresql/data \
|
-v /var/lib/archipelago/immich-db:/var/lib/postgresql/data \
|
||||||
-e POSTGRES_PASSWORD=$IMMICH_DB_PASS -e POSTGRES_USER=postgres -e POSTGRES_DB=immich \
|
-e "POSTGRES_PASSWORD=$IMMICH_DB_PASS" -e POSTGRES_USER=postgres -e POSTGRES_DB=immich \
|
||||||
ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0 2>>"$LOG" || true
|
ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0 2>>"$LOG" || true
|
||||||
sleep 3
|
sleep 3
|
||||||
for i in 1 2 3 4 5 6 7 8 9 10; do
|
for i in 1 2 3 4 5 6 7 8 9 10; do
|
||||||
@ -787,7 +787,7 @@ if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q immich_server; then
|
|||||||
--health-cmd="curl -sf http://localhost:2283/ || exit 1" --health-interval=30s --health-timeout=5s --health-retries=3 \
|
--health-cmd="curl -sf http://localhost:2283/ || exit 1" --health-interval=30s --health-timeout=5s --health-retries=3 \
|
||||||
--memory=$(mem_limit immich_server) --network immich-net \
|
--memory=$(mem_limit immich_server) --network immich-net \
|
||||||
-p 2283:2283 -v /var/lib/archipelago/immich:/usr/src/app/upload \
|
-p 2283:2283 -v /var/lib/archipelago/immich:/usr/src/app/upload \
|
||||||
-e DB_HOSTNAME=immich_postgres -e DB_USERNAME=postgres -e DB_PASSWORD=$IMMICH_DB_PASS \
|
-e DB_HOSTNAME=immich_postgres -e DB_USERNAME=postgres -e "DB_PASSWORD=$IMMICH_DB_PASS" \
|
||||||
-e DB_DATABASE_NAME=immich -e REDIS_HOSTNAME=immich_redis \
|
-e DB_DATABASE_NAME=immich -e REDIS_HOSTNAME=immich_redis \
|
||||||
-e UPLOAD_LOCATION=/usr/src/app/upload \
|
-e UPLOAD_LOCATION=/usr/src/app/upload \
|
||||||
ghcr.io/immich-app/immich-server:release 2>>"$LOG" || true
|
ghcr.io/immich-app/immich-server:release 2>>"$LOG" || true
|
||||||
@ -805,7 +805,7 @@ if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q penpot-frontend; the
|
|||||||
--health-cmd="pg_isready -U penpot || exit 1" --health-interval=30s --health-timeout=5s --health-retries=3 \
|
--health-cmd="pg_isready -U penpot || exit 1" --health-interval=30s --health-timeout=5s --health-retries=3 \
|
||||||
--memory=$(mem_limit penpot-postgres) --network penpot-net \
|
--memory=$(mem_limit penpot-postgres) --network penpot-net \
|
||||||
-v /var/lib/archipelago/penpot-postgres:/var/lib/postgresql/data \
|
-v /var/lib/archipelago/penpot-postgres:/var/lib/postgresql/data \
|
||||||
-e POSTGRES_DB=penpot -e POSTGRES_USER=penpot -e POSTGRES_PASSWORD=$PENPOT_DB_PASS \
|
-e POSTGRES_DB=penpot -e POSTGRES_USER=penpot -e "POSTGRES_PASSWORD=$PENPOT_DB_PASS" \
|
||||||
docker.io/postgres:15 2>>"$LOG" || true
|
docker.io/postgres:15 2>>"$LOG" || true
|
||||||
sleep 5
|
sleep 5
|
||||||
fi
|
fi
|
||||||
@ -825,7 +825,7 @@ if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q penpot-frontend; the
|
|||||||
-e PENPOT_PUBLIC_URI="http://${TARGET_IP}:9001" \
|
-e PENPOT_PUBLIC_URI="http://${TARGET_IP}:9001" \
|
||||||
-e PENPOT_SECRET_KEY=archipelago-penpot-secret-key-change-in-production \
|
-e PENPOT_SECRET_KEY=archipelago-penpot-secret-key-change-in-production \
|
||||||
-e PENPOT_DATABASE_URI=postgresql://penpot-postgres/penpot \
|
-e PENPOT_DATABASE_URI=postgresql://penpot-postgres/penpot \
|
||||||
-e PENPOT_DATABASE_USERNAME=penpot -e PENPOT_DATABASE_PASSWORD=$PENPOT_DB_PASS \
|
-e PENPOT_DATABASE_USERNAME=penpot -e "PENPOT_DATABASE_PASSWORD=$PENPOT_DB_PASS" \
|
||||||
-e PENPOT_REDIS_URI=redis://penpot-valkey/0 \
|
-e PENPOT_REDIS_URI=redis://penpot-valkey/0 \
|
||||||
-e PENPOT_OBJECTS_STORAGE_BACKEND=fs \
|
-e PENPOT_OBJECTS_STORAGE_BACKEND=fs \
|
||||||
-e PENPOT_OBJECTS_STORAGE_FS_DIRECTORY=/opt/data/assets \
|
-e PENPOT_OBJECTS_STORAGE_FS_DIRECTORY=/opt/data/assets \
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user