fix: container install flow, filebrowser auth, AppCard enrichment
- Fix .198-style fresh installs: systemd service ExecStartPre creates /run/user/1000, enable podman.socket, chmod 644 /etc/hosts - Filebrowser: add /data volume for database (fixes read-only crash), secure auth with random password via backend RPC (no more admin/admin) - AppCard: enrich installing state with marketplace metadata (icon, title, description, tier badge, author, version) - Registry: btcpayserver 1.13.5 → 1.13.7, images mirrored - ReadWritePaths: add home container paths for rootless podman Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -38,6 +38,7 @@ impl RpcHandler {
|
||||
"package.stop" => self.handle_package_stop(params).await,
|
||||
"package.restart" => self.handle_package_restart(params).await,
|
||||
"package.uninstall" => self.handle_package_uninstall(params).await,
|
||||
"app.filebrowser-token" => self.handle_filebrowser_token().await,
|
||||
|
||||
// Bundled app management (for pre-loaded container images)
|
||||
"bundled-app-start" => self.handle_bundled_app_start(params).await,
|
||||
|
||||
@@ -562,10 +562,18 @@ pub(super) async fn get_app_config(
|
||||
.unwrap_or(8083);
|
||||
(
|
||||
vec![format!("{}:80", host_port)],
|
||||
vec!["/var/lib/archipelago/filebrowser:/srv".to_string()],
|
||||
vec![
|
||||
"/var/lib/archipelago/filebrowser:/srv".to_string(),
|
||||
"/var/lib/archipelago/filebrowser-data:/data".to_string(),
|
||||
],
|
||||
vec![],
|
||||
None,
|
||||
None,
|
||||
Some(vec![
|
||||
"--database=/data/database.db".to_string(),
|
||||
"--root=/srv".to_string(),
|
||||
"--address=0.0.0.0".to_string(),
|
||||
"--port=80".to_string(),
|
||||
]),
|
||||
)
|
||||
}
|
||||
"nginx-proxy-manager" => (
|
||||
|
||||
@@ -404,6 +404,67 @@ printtoconsole=1\n",
|
||||
|
||||
/// Run post-install hooks (Nextcloud trusted domains, Bitcoin UI container).
|
||||
async fn run_post_install_hooks(&self, package_id: &str) {
|
||||
if package_id == "filebrowser" {
|
||||
tokio::spawn(async move {
|
||||
// Wait for filebrowser to start and initialize its database
|
||||
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
|
||||
|
||||
// Generate a random password (32 bytes, hex-encoded)
|
||||
let mut buf = [0u8; 32];
|
||||
rand::RngCore::fill_bytes(&mut rand::rngs::OsRng, &mut buf);
|
||||
let password = hex::encode(buf);
|
||||
|
||||
// Get a JWT token with default credentials
|
||||
let login_res = reqwest::Client::builder()
|
||||
.timeout(std::time::Duration::from_secs(10))
|
||||
.build()
|
||||
.unwrap_or_default()
|
||||
.post("http://127.0.0.1:8083/api/login")
|
||||
.json(&serde_json::json!({"username": "admin", "password": "admin"}))
|
||||
.send()
|
||||
.await;
|
||||
|
||||
let token = match login_res {
|
||||
Ok(resp) if resp.status().is_success() => {
|
||||
resp.text().await.unwrap_or_default().trim_matches('"').to_string()
|
||||
}
|
||||
_ => {
|
||||
tracing::warn!("FileBrowser not ready for password change — keeping default");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// Change admin password via filebrowser API
|
||||
let change_res = reqwest::Client::builder()
|
||||
.timeout(std::time::Duration::from_secs(10))
|
||||
.build()
|
||||
.unwrap_or_default()
|
||||
.put("http://127.0.0.1:8083/api/users/1")
|
||||
.header("X-Auth", &token)
|
||||
.json(&serde_json::json!({"password": password}))
|
||||
.send()
|
||||
.await;
|
||||
|
||||
match change_res {
|
||||
Ok(resp) if resp.status().is_success() => {
|
||||
let secret_dir = "/var/lib/archipelago/secrets/filebrowser";
|
||||
let _ = tokio::fs::create_dir_all(secret_dir).await;
|
||||
let _ = tokio::fs::write(
|
||||
format!("{}/password", secret_dir),
|
||||
&password,
|
||||
).await;
|
||||
info!("FileBrowser admin password secured (default credentials replaced)");
|
||||
}
|
||||
Ok(resp) => {
|
||||
tracing::warn!("FileBrowser password change failed: {}", resp.status());
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!("FileBrowser password change error: {}", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if package_id == "nextcloud" {
|
||||
let host_ip = self.config.host_ip.clone();
|
||||
tokio::spawn(async move {
|
||||
@@ -464,4 +525,36 @@ printtoconsole=1\n",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a fresh FileBrowser JWT token for the frontend.
|
||||
/// Reads the stored random password and authenticates to filebrowser's API.
|
||||
pub(in crate::api::rpc) async fn handle_filebrowser_token(
|
||||
&self,
|
||||
) -> Result<serde_json::Value> {
|
||||
let secret_path = "/var/lib/archipelago/secrets/filebrowser/password";
|
||||
let password = tokio::fs::read_to_string(secret_path)
|
||||
.await
|
||||
.unwrap_or_else(|_| "admin".to_string());
|
||||
|
||||
let client = reqwest::Client::builder()
|
||||
.timeout(std::time::Duration::from_secs(10))
|
||||
.build()
|
||||
.unwrap_or_default();
|
||||
|
||||
let resp = client
|
||||
.post("http://127.0.0.1:8083/api/login")
|
||||
.json(&serde_json::json!({"username": "admin", "password": password}))
|
||||
.send()
|
||||
.await
|
||||
.context("Failed to connect to FileBrowser")?;
|
||||
|
||||
if !resp.status().is_success() {
|
||||
return Err(anyhow::anyhow!("FileBrowser login failed ({})", resp.status()));
|
||||
}
|
||||
|
||||
let token = resp.text().await.unwrap_or_default();
|
||||
let token = token.trim_matches('"');
|
||||
|
||||
Ok(serde_json::json!({ "token": token }))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,6 +142,7 @@ cat > /mnt/archipelago/etc/hosts <<EOF
|
||||
|
||||
::1 localhost ip6-localhost ip6-loopback
|
||||
EOF
|
||||
chmod 644 /mnt/archipelago/etc/hosts
|
||||
|
||||
# Install bootloader and essential packages in chroot
|
||||
echo "📦 Configuring package sources..."
|
||||
|
||||
@@ -1324,6 +1324,7 @@ cat > /mnt/target/etc/hosts <<EOF
|
||||
127.0.1.1 archipelago
|
||||
::1 localhost ip6-localhost ip6-loopback
|
||||
EOF
|
||||
chmod 644 /mnt/target/etc/hosts
|
||||
|
||||
# Configure Archipelago app registry (HTTP, insecure)
|
||||
mkdir -p /mnt/target/home/archipelago/.config/containers
|
||||
|
||||
@@ -9,6 +9,7 @@ User=archipelago
|
||||
Environment="ARCHIPELAGO_BIND=127.0.0.1:5678"
|
||||
# DEV_MODE disabled in production — enabled via override.conf on dev servers
|
||||
Environment="XDG_RUNTIME_DIR=/run/user/1000"
|
||||
ExecStartPre=/bin/bash -c 'mkdir -p /run/user/1000 && chown archipelago:archipelago /run/user/1000 && chmod 700 /run/user/1000'
|
||||
ExecStartPre=/bin/bash -c 'mkdir -p /var/lib/archipelago && echo "ARCHIPELAGO_HOST_IP=$(hostname -I 2>/dev/null | awk "{print $$1}")" > /var/lib/archipelago/host-ip.env'
|
||||
ExecStart=/usr/local/bin/archipelago
|
||||
Restart=on-failure
|
||||
@@ -22,7 +23,7 @@ ProtectSystem=strict
|
||||
ProtectHome=no
|
||||
# PrivateTmp disabled: rootless podman runtime lives in /tmp/podman-run-UID/
|
||||
# and must be shared between the service and SSH-created containers
|
||||
ReadWritePaths=/var/lib/archipelago /etc/containers /var/lib/containers /run/containers /run/user /tmp
|
||||
ReadWritePaths=/var/lib/archipelago /etc/containers /var/lib/containers /run/containers /run/user /tmp /home/archipelago/.local/share/containers /home/archipelago/.config/containers /etc
|
||||
|
||||
# Privilege restriction — restored with rootless podman (no sudo needed)
|
||||
NoNewPrivileges=yes
|
||||
|
||||
@@ -52,6 +52,15 @@ mkdir -p /home/archipelago/.config/systemd/user
|
||||
# Enable lingering for archipelago user (allows user services to run without login)
|
||||
loginctl enable-linger archipelago || true
|
||||
|
||||
# Ensure /run/user/1000 exists for podman socket
|
||||
mkdir -p /run/user/1000
|
||||
chown archipelago:archipelago /run/user/1000
|
||||
chmod 700 /run/user/1000
|
||||
|
||||
# Enable podman API socket for archipelago user (backend connects via this)
|
||||
su - archipelago -c "XDG_RUNTIME_DIR=/run/user/1000 systemctl --user enable podman.socket" || true
|
||||
su - archipelago -c "XDG_RUNTIME_DIR=/run/user/1000 systemctl --user start podman.socket" || true
|
||||
|
||||
# Set proper permissions
|
||||
chown -R archipelago:archipelago /home/archipelago/.config
|
||||
chown -R archipelago:archipelago /home/archipelago/.local
|
||||
|
||||
@@ -40,19 +40,18 @@ describe('FileBrowserClient', () => {
|
||||
})
|
||||
|
||||
describe('login', () => {
|
||||
it('authenticates and stores token', async () => {
|
||||
mockFetch.mockResolvedValueOnce(jsonResponse('"jwt-token-123"'))
|
||||
it('authenticates via backend RPC and stores token', async () => {
|
||||
mockFetch.mockResolvedValueOnce(jsonResponse({ result: { token: 'jwt-token-123' } }))
|
||||
|
||||
// We need a fresh instance to test login — use the exported singleton
|
||||
const result = await fileBrowserClient.login('admin', 'admin')
|
||||
const result = await fileBrowserClient.login()
|
||||
|
||||
expect(result).toBe(true)
|
||||
expect(fileBrowserClient.isAuthenticated).toBe(true)
|
||||
expect(mockFetch).toHaveBeenCalledWith(
|
||||
expect.stringContaining('/app/filebrowser/api/login'),
|
||||
'/rpc/v1',
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ username: 'admin', password: 'admin' }),
|
||||
body: JSON.stringify({ method: 'app.filebrowser-token' }),
|
||||
}),
|
||||
)
|
||||
})
|
||||
@@ -60,7 +59,7 @@ describe('FileBrowserClient', () => {
|
||||
it('returns false on failed login', async () => {
|
||||
mockFetch.mockResolvedValueOnce(jsonResponse(null, 403))
|
||||
|
||||
const result = await fileBrowserClient.login('admin', 'wrong')
|
||||
const result = await fileBrowserClient.login()
|
||||
|
||||
expect(result).toBe(false)
|
||||
})
|
||||
|
||||
@@ -52,20 +52,21 @@ class FileBrowserClient {
|
||||
return match ? match[1]! : null
|
||||
}
|
||||
|
||||
async login(username = 'admin', password = 'admin'): Promise<boolean> {
|
||||
async login(): Promise<boolean> {
|
||||
try {
|
||||
const res = await fetch(`${this.baseUrl}/api/login`, {
|
||||
// Get a filebrowser JWT via the authenticated backend (no credentials exposed to browser)
|
||||
const rpcRes = await fetch('/rpc/v1', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username, password }),
|
||||
body: JSON.stringify({ method: 'app.filebrowser-token' }),
|
||||
credentials: 'same-origin',
|
||||
})
|
||||
if (!res.ok) return false
|
||||
const text = await res.text()
|
||||
// FileBrowser returns the JWT as a plain string (possibly quoted)
|
||||
const token = text.replace(/^"|"$/g, '')
|
||||
// Store token as cookie — the only auth mechanism we use
|
||||
if (!rpcRes.ok) return false
|
||||
const rpcData = await rpcRes.json()
|
||||
const token = rpcData?.result?.token
|
||||
if (!token) return false
|
||||
|
||||
const expires = new Date(Date.now() + 24 * 60 * 60 * 1000).toUTCString()
|
||||
// Only set Secure flag on HTTPS — on HTTP it silently prevents the cookie from being stored
|
||||
const secure = window.location.protocol === 'https:' ? '; Secure' : ''
|
||||
document.cookie = `auth=${token}; path=/app/filebrowser; SameSite=Lax${secure}; expires=${expires}`
|
||||
this._authenticated = true
|
||||
|
||||
@@ -107,6 +107,7 @@ export interface Manifest {
|
||||
'donation-url': string | null
|
||||
author?: string
|
||||
website?: string
|
||||
tier?: string
|
||||
interfaces?: {
|
||||
main?: {
|
||||
ui?: string
|
||||
|
||||
@@ -39,43 +39,51 @@
|
||||
|
||||
<div class="flex items-start gap-4">
|
||||
<img
|
||||
:src="pkg['static-files']?.icon || `/assets/img/app-icons/${id}.png`"
|
||||
:alt="pkg.manifest?.title || String(id)"
|
||||
class="w-16 h-16 rounded-lg object-cover bg-white/10"
|
||||
:src="icon"
|
||||
:alt="title"
|
||||
class="w-14 h-14 rounded-lg object-cover bg-white/10"
|
||||
@error="handleImageError"
|
||||
/>
|
||||
<div class="flex-1 min-w-0 overflow-hidden">
|
||||
<h3 class="text-lg font-semibold text-white mb-1 truncate" :title="pkg.manifest.title">
|
||||
{{ pkg.manifest.title }}
|
||||
</h3>
|
||||
<p class="text-sm text-white/70 mb-2 truncate">
|
||||
{{ pkg.manifest?.description?.short || '' }}
|
||||
</p>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="flex items-center gap-2 mb-0.5">
|
||||
<h3 class="text-lg font-semibold text-white truncate" :title="title">
|
||||
{{ title }}
|
||||
</h3>
|
||||
<span
|
||||
class="inline-flex items-center gap-1.5 px-2 py-1 rounded text-xs font-medium"
|
||||
:class="getStatusClass(pkg.state, pkg.health)"
|
||||
>
|
||||
<svg
|
||||
v-if="isTransitioning"
|
||||
class="animate-spin h-3 w-3"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
<span v-if="pkg.state === 'running' && pkg.health === 'unhealthy'" class="w-1.5 h-1.5 rounded-full bg-orange-400 animate-pulse"></span>
|
||||
{{ getStatusLabel(pkg.state, pkg.health) }}
|
||||
</span>
|
||||
<span class="text-xs text-white/50">
|
||||
v{{ pkg.manifest.version }}
|
||||
</span>
|
||||
v-if="tier && tier !== 'optional'"
|
||||
class="tier-badge"
|
||||
:class="tier === 'core' ? 'tier-badge-core' : 'tier-badge-recommended'"
|
||||
>{{ tier }}</span>
|
||||
</div>
|
||||
<p class="text-sm text-white/50">{{ version ? `v${version}` : '' }}</p>
|
||||
<p v-if="author" class="text-xs text-white/40 mt-0.5">{{ author }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="text-white/70 text-sm mt-3 mb-3 line-clamp-2">
|
||||
{{ description }}
|
||||
</p>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<span
|
||||
class="inline-flex items-center gap-1.5 px-2 py-1 rounded text-xs font-medium"
|
||||
:class="getStatusClass(pkg.state, pkg.health)"
|
||||
>
|
||||
<svg
|
||||
v-if="isTransitioning"
|
||||
class="animate-spin h-3 w-3"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
<span v-if="pkg.state === 'running' && pkg.health === 'unhealthy'" class="w-1.5 h-1.5 rounded-full bg-orange-400 animate-pulse"></span>
|
||||
{{ getStatusLabel(pkg.state, pkg.health) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Quick Actions -->
|
||||
<div v-if="!isUninstalling" class="mt-4 flex gap-2">
|
||||
<button
|
||||
@@ -145,9 +153,13 @@ import {
|
||||
isWebOnlyApp, opensInTab, canLaunch,
|
||||
getStatusClass, getStatusLabel, handleImageError,
|
||||
} from './appsConfig'
|
||||
import { getCuratedAppList } from '../discover/curatedApps'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
// Build a lookup map for enriching sparse backend data during install
|
||||
const curatedMap = new Map(getCuratedAppList().map(a => [a.id, a]))
|
||||
|
||||
const props = defineProps<{
|
||||
id: string
|
||||
pkg: PackageDataEntry
|
||||
@@ -168,6 +180,35 @@ defineEmits<{
|
||||
|
||||
const isWebOnly = computed(() => isWebOnlyApp(props.id))
|
||||
|
||||
// Enrich from marketplace when backend data is sparse (e.g. during install)
|
||||
const curated = computed(() => curatedMap.get(props.id))
|
||||
const title = computed(() => {
|
||||
const t = props.pkg.manifest?.title
|
||||
return (t && t !== props.id) ? t : (curated.value?.title || t || props.id)
|
||||
})
|
||||
const description = computed(() => {
|
||||
const d = props.pkg.manifest?.description?.short
|
||||
return (d && d !== 'Installing...') ? d : (curated.value?.description || d || '')
|
||||
})
|
||||
const icon = computed(() => {
|
||||
const i = props.pkg['static-files']?.icon
|
||||
return i || curated.value?.icon || `/assets/img/app-icons/${props.id}.png`
|
||||
})
|
||||
const version = computed(() => {
|
||||
const v = props.pkg.manifest?.version
|
||||
return v || curated.value?.version || ''
|
||||
})
|
||||
const author = computed(() => props.pkg.manifest?.author || curated.value?.author || '')
|
||||
const tier = computed(() => {
|
||||
const t = props.pkg.manifest?.tier
|
||||
if (t && t !== '') return t
|
||||
const core = ['bitcoin-knots', 'bitcoin', 'lnd', 'mempool', 'btcpay-server', 'dwn', 'filebrowser']
|
||||
const recommended = ['fedimint', 'thunderhub', 'vaultwarden', 'uptime-kuma', 'grafana', 'searxng', 'tailscale', 'portainer']
|
||||
if (core.includes(props.id)) return 'core'
|
||||
if (recommended.includes(props.id)) return 'recommended'
|
||||
return 'optional'
|
||||
})
|
||||
|
||||
const isTransitioning = computed(() => {
|
||||
const s = props.pkg.state
|
||||
const h = props.pkg.health
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { MarketplaceApp } from './types'
|
||||
export function getCuratedAppList(): MarketplaceApp[] {
|
||||
return [
|
||||
{ id: 'bitcoin-knots', title: 'Bitcoin Knots', version: '28.1.0', description: 'Run a full Bitcoin node. Validate and relay blocks and transactions on the Bitcoin network.', icon: '/assets/img/app-icons/bitcoin-knots.webp', author: 'Bitcoin Knots', dockerImage: 'docker.io/bitcoinknots/bitcoin:v28.1', repoUrl: 'https://github.com/bitcoinknots/bitcoin' },
|
||||
{ id: 'btcpay-server', title: 'BTCPay Server', version: '1.13.5', description: 'Self-hosted Bitcoin payment processor. Accept Bitcoin payments without intermediaries or fees.', icon: '/assets/img/app-icons/btcpay-server.png', author: 'BTCPay Server Foundation', dockerImage: 'docker.io/btcpayserver/btcpayserver:1.13.5', repoUrl: 'https://github.com/btcpayserver/btcpayserver' },
|
||||
{ id: 'btcpay-server', title: 'BTCPay Server', version: '1.13.7', description: 'Self-hosted Bitcoin payment processor. Accept Bitcoin payments without intermediaries or fees.', icon: '/assets/img/app-icons/btcpay-server.png', author: 'BTCPay Server Foundation', dockerImage: 'docker.io/btcpayserver/btcpayserver:1.13.7', repoUrl: 'https://github.com/btcpayserver/btcpayserver' },
|
||||
{ id: 'lnd', title: 'LND', version: '0.17.4', description: 'Lightning Network Daemon. Fast and cheap Bitcoin payments through the Lightning Network.', icon: '/assets/img/app-icons/lnd.svg', author: 'Lightning Labs', dockerImage: 'docker.io/lightninglabs/lnd:v0.17.4-beta', repoUrl: 'https://github.com/lightningnetwork/lnd' },
|
||||
{ id: 'thunderhub', title: 'ThunderHub', version: '0.13.31', description: 'Lightning node management UI. Manage channels, payments, routing fees, and monitor your Lightning node.', icon: '/assets/img/app-icons/thunderhub.svg', author: 'Anthony Potdevin', dockerImage: 'docker.io/apotdevin/thunderhub:v0.13.31', repoUrl: 'https://github.com/apotdevin/thunderhub' },
|
||||
{ id: 'mempool', title: 'Mempool Explorer', version: '2.5.0', description: 'Self-hosted Bitcoin blockchain and mempool visualizer. Monitor transactions without revealing your addresses to third parties.', icon: '/assets/img/app-icons/mempool.webp', author: 'Mempool', dockerImage: 'docker.io/mempool/frontend:v2.5.0', repoUrl: 'https://github.com/mempool/mempool' },
|
||||
|
||||
@@ -141,11 +141,11 @@ export function getCuratedAppList(): MarketplaceApp[] {
|
||||
{
|
||||
id: 'btcpay-server',
|
||||
title: 'BTCPay Server',
|
||||
version: '1.13.5',
|
||||
version: '1.13.7',
|
||||
description: 'Self-hosted Bitcoin payment processor. Accept Bitcoin payments without intermediaries or fees.',
|
||||
icon: '/assets/img/app-icons/btcpay-server.png',
|
||||
author: 'BTCPay Server Foundation',
|
||||
dockerImage: `${REGISTRY}/btcpayserver:1.13.5`,
|
||||
dockerImage: `${REGISTRY}/btcpayserver:1.13.7`,
|
||||
manifestUrl: undefined,
|
||||
repoUrl: 'https://github.com/btcpayserver/btcpayserver'
|
||||
},
|
||||
|
||||
@@ -700,12 +700,17 @@ fi
|
||||
track_container "onlyoffice"
|
||||
if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q filebrowser; then
|
||||
log "Creating File Browser..."
|
||||
mkdir -p /var/lib/archipelago/filebrowser /var/lib/archipelago/filebrowser-db
|
||||
mkdir -p /var/lib/archipelago/filebrowser /var/lib/archipelago/filebrowser-data
|
||||
$DOCKER run -d --name filebrowser --restart unless-stopped \
|
||||
--health-cmd="curl -sf http://localhost:80/ || exit 1" --health-interval=30s --health-timeout=5s --health-retries=3 \
|
||||
--memory=$(mem_limit filebrowser) \
|
||||
-p 8083:80 -v /var/lib/archipelago/filebrowser:/srv \
|
||||
"$FILEBROWSER_IMAGE" 2>>"$LOG" || true
|
||||
--cap-drop ALL --security-opt no-new-privileges:true \
|
||||
--read-only --tmpfs=/tmp:rw,noexec,nosuid,size=256m --tmpfs=/run:rw,noexec,nosuid,size=64m \
|
||||
-p 8083:80 \
|
||||
-v /var/lib/archipelago/filebrowser:/srv \
|
||||
-v /var/lib/archipelago/filebrowser-data:/data \
|
||||
"$FILEBROWSER_IMAGE" \
|
||||
--database=/data/database.db --root=/srv --address=0.0.0.0 --port=80 2>>"$LOG" || true
|
||||
fi
|
||||
track_container "filebrowser"
|
||||
if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q nginx-proxy-manager; then
|
||||
|
||||
Reference in New Issue
Block a user