Initial import: bare-metal stacks, Server UI, GPU fan, tutorials.
Infrastructure configs for GMKtec K11 (Docker, vLLM, LocalAI, ComfyUI, control-plane, gpu-fan agent, Server UI with CLI/file explorer/GPU fan curve). Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
# Data disk mount point
|
||||
DATA_ROOT=/data
|
||||
|
||||
# NPMPlus image
|
||||
NPMPLUS_IMAGE=docker.io/zoeyvid/npmplus:latest
|
||||
|
||||
# Required for Let's Encrypt certificates
|
||||
ACME_EMAIL=admin@example.com
|
||||
|
||||
# Timezone (TZ database name)
|
||||
TZ=Europe/Warsaw
|
||||
|
||||
# Optional: set on first start instead of random password in logs
|
||||
# INITIAL_ADMIN_EMAIL=admin@example.com
|
||||
# INITIAL_ADMIN_PASSWORD=change-me-strong-password
|
||||
|
||||
# Bind admin UI (port 81) to localhost only — use with SSH tunnel
|
||||
# NPM_LISTEN_LOCALHOST=true
|
||||
|
||||
# Disable IPv6 listeners if your network has no IPv6
|
||||
# DISABLE_IPV6=true
|
||||
@@ -0,0 +1 @@
|
||||
.env
|
||||
@@ -0,0 +1,89 @@
|
||||
# NPMPlus — reverse proxy + Let's Encrypt
|
||||
|
||||
[NPMPlus](https://github.com/ZoeyVid/NPMPlus) (fork nginx-proxy-manager) exposes backend services on **HTTPS** with automatic certificates.
|
||||
|
||||
On this host it proxies **LocalAI** at `https://llm.rtx1.mobile.agency-ai.dev` → `http://127.0.0.1:8070`.
|
||||
|
||||
## Ports (host network)
|
||||
|
||||
| Port | Protocol | Service |
|
||||
|------|----------|---------|
|
||||
| 80 | tcp | HTTP (ACME challenge + redirects) |
|
||||
| 443 | tcp, udp | HTTPS / HTTP3 |
|
||||
| 81 | tcp | NPMPlus admin UI (HTTPS) |
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. `/data` mounted (tutorial 04)
|
||||
2. DNS `A` record: `llm.rtx1.mobile.agency-ai.dev` → public IP
|
||||
3. Router port forward: `80`, `443/tcp`, `443/udp` → `192.168.100.5`
|
||||
4. `ACME_EMAIL` in `.env` (Let's Encrypt notifications)
|
||||
|
||||
## Quick start
|
||||
|
||||
```bash
|
||||
cd stacks/npmplus
|
||||
cp .env.example .env
|
||||
# Edit ACME_EMAIL (and optionally INITIAL_ADMIN_*)
|
||||
./scripts/start.sh
|
||||
```
|
||||
|
||||
Admin UI: `https://<LAN-IP>:81` (default login `admin@example.org` unless `INITIAL_ADMIN_EMAIL` set).
|
||||
|
||||
## Admin access (LAN)
|
||||
|
||||
Use **`https://192.168.100.90:81`** (or your LAN IP), **not** `http://`.
|
||||
|
||||
| Symptom | Cause |
|
||||
|---------|-------|
|
||||
| `http://IP:81` does not load UI | Port 81 serves HTTPS only; HTTP returns 308 redirect |
|
||||
| Browser shows certificate error | Default cert has no SAN for IP; run cert regeneration (below) |
|
||||
| `docker ps` shows empty PORTS | Normal with `network_mode: host` — check with `ss -tlnp \| grep ':81'` |
|
||||
|
||||
Regenerate admin cert with SAN for your LAN IP:
|
||||
|
||||
```bash
|
||||
sudo ./scripts/regenerate-admin-cert.sh
|
||||
```
|
||||
|
||||
After regeneration, open `https://<LAN-IP>:81`, click through the self-signed cert warning once, then log in.
|
||||
|
||||
## Proxy host — LocalAI
|
||||
|
||||
In NPMPlus UI → **Hosts** → **Proxy Hosts** → Add:
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Domain | `llm.rtx1.mobile.agency-ai.dev` |
|
||||
| Scheme | `http` |
|
||||
| Forward hostname | `127.0.0.1` |
|
||||
| Forward port | `8070` |
|
||||
| SSL | Request new certificate, Force SSL, HTTP/2 |
|
||||
| Websockets | Enabled |
|
||||
|
||||
Clients send `Authorization: Bearer <LOCALAI_API_KEY>` (same key as in `stacks/localai/.env`).
|
||||
|
||||
Test:
|
||||
|
||||
```bash
|
||||
curl -s https://llm.rtx1.mobile.agency-ai.dev/v1/models \
|
||||
-H "Authorization: Bearer $LOCALAI_API_KEY"
|
||||
```
|
||||
|
||||
## Firewall
|
||||
|
||||
```bash
|
||||
sudo ./scripts/configure-firewall.sh
|
||||
```
|
||||
|
||||
Restricts direct access to LocalAI (8070) and gpu-fan (8090); allows 80/443 for NPMPlus.
|
||||
|
||||
## Data
|
||||
|
||||
Persistent files: `/data/apps/npmplus/`
|
||||
|
||||
## Related
|
||||
|
||||
- LocalAI stack: [`../localai/README.md`](../localai/README.md)
|
||||
- Client handoff: [`../localai/coding-agent/APP-CLIENT-HANDOFF.md`](../localai/coding-agent/APP-CLIENT-HANDOFF.md)
|
||||
- Tutorial: [`../../manual-tutorial/07-npmplus-reverse-proxy.md`](../../manual-tutorial/07-npmplus-reverse-proxy.md)
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
docker-compose.yml
|
||||
@@ -0,0 +1,27 @@
|
||||
name: npmplus
|
||||
|
||||
services:
|
||||
npmplus:
|
||||
image: ${NPMPLUS_IMAGE:-docker.io/zoeyvid/npmplus:latest}
|
||||
container_name: npmplus
|
||||
profiles:
|
||||
- npmplus
|
||||
restart: unless-stopped
|
||||
network_mode: host
|
||||
cap_drop:
|
||||
- ALL
|
||||
cap_add:
|
||||
- NET_BIND_SERVICE
|
||||
- SETGID
|
||||
- DAC_OVERRIDE
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
volumes:
|
||||
- ${DATA_ROOT:-/data}/apps/npmplus:/data
|
||||
environment:
|
||||
- TZ=${TZ:-Europe/Warsaw}
|
||||
- ACME_EMAIL=${ACME_EMAIL}
|
||||
- INITIAL_ADMIN_EMAIL=${INITIAL_ADMIN_EMAIL:-}
|
||||
- INITIAL_ADMIN_PASSWORD=${INITIAL_ADMIN_PASSWORD:-}
|
||||
- NPM_LISTEN_LOCALHOST=${NPM_LISTEN_LOCALHOST:-false}
|
||||
- DISABLE_IPV6=${DISABLE_IPV6:-false}
|
||||
Executable
+45
@@ -0,0 +1,45 @@
|
||||
#!/usr/bin/env bash
|
||||
# UFW rules for NPMPlus + LocalAI hardening. Run: sudo ./scripts/configure-firewall.sh
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
if [[ "${EUID}" -ne 0 ]]; then
|
||||
echo "Run as root: sudo ${SCRIPT_DIR}/configure-firewall.sh"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
LAN_CIDR="${LAN_CIDR:-192.168.100.0/24}"
|
||||
|
||||
echo "=== NPMPlus firewall (ufw) ==="
|
||||
echo "LAN CIDR for admin port 81: ${LAN_CIDR}"
|
||||
echo ""
|
||||
|
||||
if ! command -v ufw &>/dev/null; then
|
||||
echo "Installing ufw..."
|
||||
apt-get update -qq && apt-get install -y ufw
|
||||
fi
|
||||
|
||||
ufw default deny incoming
|
||||
ufw default allow outgoing
|
||||
|
||||
ufw allow 22/tcp comment 'SSH'
|
||||
ufw allow 80/tcp comment 'NPMPlus HTTP / ACME'
|
||||
ufw allow 443/tcp comment 'NPMPlus HTTPS'
|
||||
ufw allow 443/udp comment 'NPMPlus HTTP/3'
|
||||
ufw allow from "${LAN_CIDR}" to any port 81 proto tcp comment 'NPMPlus admin LAN only'
|
||||
|
||||
ufw deny 8070/tcp comment 'LocalAI localhost only'
|
||||
ufw deny 8090/tcp comment 'gpu-fan not public'
|
||||
|
||||
echo ""
|
||||
echo "Rules to apply:"
|
||||
ufw show added || true
|
||||
echo ""
|
||||
|
||||
ufw --force enable
|
||||
ufw status verbose
|
||||
|
||||
echo ""
|
||||
echo "Done. Admin UI: https://<LAN-IP>:81 (from ${LAN_CIDR} only)"
|
||||
echo "Public HTTPS: ports 80/443"
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
#!/usr/bin/env bash
|
||||
# Create NPMPlus proxy host for LocalAI + request Let's Encrypt cert.
|
||||
# Usage: ./scripts/configure-localai-proxy.sh
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
STACK_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||
|
||||
cd "${STACK_DIR}"
|
||||
|
||||
if [[ ! -f .env ]]; then
|
||||
echo "ERROR: .env not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
set -a
|
||||
# shellcheck disable=SC1091
|
||||
source .env
|
||||
set +a
|
||||
|
||||
export INITIAL_ADMIN_EMAIL INITIAL_ADMIN_PASSWORD ACME_EMAIL \
|
||||
LOCALAI_PROXY_DOMAIN LOCALAI_FORWARD_PORT
|
||||
|
||||
python3 <<'PY'
|
||||
import json
|
||||
import os
|
||||
import ssl
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
import http.cookiejar
|
||||
|
||||
domain = os.environ.get("LOCALAI_PROXY_DOMAIN", "llm.rtx1.mobile.agency-ai.dev")
|
||||
forward_port = int(os.environ.get("LOCALAI_FORWARD_PORT", "8070"))
|
||||
admin_email = os.environ.get("INITIAL_ADMIN_EMAIL", "")
|
||||
admin_pass = os.environ.get("INITIAL_ADMIN_PASSWORD", "")
|
||||
acme_email = os.environ.get("ACME_EMAIL", admin_email)
|
||||
|
||||
if not admin_email or not admin_pass:
|
||||
print("ERROR: Set INITIAL_ADMIN_EMAIL and INITIAL_ADMIN_PASSWORD in .env", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
ctx = ssl.create_default_context()
|
||||
ctx.check_hostname = False
|
||||
ctx.verify_mode = ssl.CERT_NONE
|
||||
|
||||
cj = http.cookiejar.CookieJar()
|
||||
opener = urllib.request.build_opener(
|
||||
urllib.request.HTTPCookieProcessor(cj),
|
||||
urllib.request.HTTPSHandler(context=ctx),
|
||||
)
|
||||
|
||||
|
||||
def api(method: str, path: str, payload=None):
|
||||
data = None
|
||||
headers = {}
|
||||
if payload is not None:
|
||||
data = json.dumps(payload).encode()
|
||||
headers["Content-Type"] = "application/json"
|
||||
req = urllib.request.Request(f"https://127.0.0.1:81{path}", data=data, headers=headers, method=method)
|
||||
with opener.open(req) as resp:
|
||||
body = resp.read()
|
||||
return json.loads(body) if body else {}
|
||||
|
||||
|
||||
try:
|
||||
api("POST", "/api/tokens", {"identity": admin_email, "secret": admin_pass})
|
||||
except urllib.error.HTTPError as e:
|
||||
print(f"ERROR: NPMPlus login failed ({e.code})", file=sys.stderr)
|
||||
print(e.read().decode()[:300], file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
if not any(c.name == "__Host-Http-token" for c in cj):
|
||||
print("ERROR: NPMPlus login did not return session cookie", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
hosts = api("GET", "/api/nginx/proxy-hosts")
|
||||
for host in hosts:
|
||||
if domain in host.get("domain_names", []):
|
||||
print(f"Proxy host already exists (id={host['id']}) for {domain}")
|
||||
sys.exit(0)
|
||||
|
||||
payload = {
|
||||
"domain_names": [domain],
|
||||
"forward_scheme": "http",
|
||||
"forward_host": "127.0.0.1",
|
||||
"forward_port": forward_port,
|
||||
"access_list_id": 0,
|
||||
"certificate_id": "new",
|
||||
"ssl_forced": True,
|
||||
"http2_support": True,
|
||||
"block_exploits": True,
|
||||
"allow_websocket_upgrade": True,
|
||||
"meta": {
|
||||
"letsencrypt_email": acme_email,
|
||||
"letsencrypt_agree": True,
|
||||
"dns_challenge": False,
|
||||
},
|
||||
}
|
||||
|
||||
try:
|
||||
result = api("POST", "/api/nginx/proxy-hosts", payload)
|
||||
except urllib.error.HTTPError as e:
|
||||
print(f"ERROR: create proxy host failed ({e.code})", file=sys.stderr)
|
||||
print(e.read().decode()[:500], file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
print(json.dumps(result, indent=2))
|
||||
print(f"\nProxy host created for https://{domain} -> 127.0.0.1:{forward_port}")
|
||||
print("Certificate issuance may take 1-2 minutes (check NPMPlus logs).")
|
||||
PY
|
||||
Executable
+81
@@ -0,0 +1,81 @@
|
||||
#!/usr/bin/env bash
|
||||
# Enable HTTP proxy for LocalAI (no SSL) — use when LE cert is pending port-forward.
|
||||
# After cert works, enable SSL in NPMPlus UI or re-run configure-localai-proxy.sh.
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
STACK_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||
cd "${STACK_DIR}"
|
||||
|
||||
set -a
|
||||
# shellcheck disable=SC1091
|
||||
source .env
|
||||
set +a
|
||||
|
||||
export INITIAL_ADMIN_EMAIL INITIAL_ADMIN_PASSWORD LOCALAI_PROXY_DOMAIN LOCALAI_FORWARD_PORT
|
||||
|
||||
python3 <<'PY'
|
||||
import json
|
||||
import os
|
||||
import ssl
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
import http.cookiejar
|
||||
|
||||
domain = os.environ.get("LOCALAI_PROXY_DOMAIN", "llm.rtx1.mobile.agency-ai.dev")
|
||||
forward_port = int(os.environ.get("LOCALAI_FORWARD_PORT", "8070"))
|
||||
|
||||
ctx = ssl.create_default_context()
|
||||
ctx.check_hostname = False
|
||||
ctx.verify_mode = ssl.CERT_NONE
|
||||
cj = http.cookiejar.CookieJar()
|
||||
opener = urllib.request.build_opener(
|
||||
urllib.request.HTTPCookieProcessor(cj),
|
||||
urllib.request.HTTPSHandler(context=ctx),
|
||||
)
|
||||
|
||||
|
||||
def api(method, path, payload=None):
|
||||
data = json.dumps(payload).encode() if payload is not None else None
|
||||
headers = {"Content-Type": "application/json"} if payload is not None else {}
|
||||
req = urllib.request.Request(
|
||||
f"https://127.0.0.1:81{path}", data=data, headers=headers, method=method
|
||||
)
|
||||
with opener.open(req) as resp:
|
||||
body = resp.read()
|
||||
return json.loads(body) if body else {}
|
||||
|
||||
|
||||
api("POST", "/api/tokens", {
|
||||
"identity": os.environ["INITIAL_ADMIN_EMAIL"],
|
||||
"secret": os.environ["INITIAL_ADMIN_PASSWORD"],
|
||||
})
|
||||
|
||||
hosts = api("GET", "/api/nginx/proxy-hosts")
|
||||
host = next((h for h in hosts if domain in h.get("domain_names", [])), None)
|
||||
if not host:
|
||||
print(f"No proxy host for {domain}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
host_id = host["id"]
|
||||
payload = {
|
||||
**{k: host[k] for k in host if k not in ("id", "created_on", "modified_on", "meta")},
|
||||
"ssl_forced": False,
|
||||
"certificate_id": 0,
|
||||
"http2_support": False,
|
||||
"allow_websocket_upgrade": True,
|
||||
"forward_scheme": "http",
|
||||
"forward_host": "127.0.0.1",
|
||||
"forward_port": forward_port,
|
||||
}
|
||||
|
||||
try:
|
||||
result = api("PUT", f"/api/nginx/proxy-hosts/{host_id}", payload)
|
||||
except urllib.error.HTTPError as e:
|
||||
print(f"ERROR ({e.code}):", e.read().decode()[:500], file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
print(f"Updated proxy host {host_id}: HTTP only until LE cert is ready")
|
||||
print(json.dumps({"id": result.get("id"), "domain_names": result.get("domain_names")}, indent=2))
|
||||
PY
|
||||
Executable
+11
@@ -0,0 +1,11 @@
|
||||
#!/usr/bin/env bash
|
||||
# Create NPMPlus data directory on the data disk.
|
||||
|
||||
ensure_npmplus_dirs() {
|
||||
local data_root="${1:-/data}"
|
||||
mkdir -p "${data_root}/apps/npmplus"
|
||||
}
|
||||
|
||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||
ensure_npmplus_dirs "${1:-/data}"
|
||||
fi
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
#!/usr/bin/env bash
|
||||
# Regenerate NPMPlus admin UI TLS cert (port 81) with SAN for LAN IP access.
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
STACK_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||
|
||||
cd "${STACK_DIR}"
|
||||
|
||||
if [[ -f .env ]]; then
|
||||
set -a
|
||||
# shellcheck disable=SC1091
|
||||
source .env
|
||||
set +a
|
||||
fi
|
||||
|
||||
DATA_ROOT="${DATA_ROOT:-/data}"
|
||||
TLS_DIR="${DATA_ROOT}/apps/npmplus/tls"
|
||||
LAN_IP="${LAN_IP:-$(hostname -I 2>/dev/null | awk '{print $1}')}"
|
||||
HOST_SHORT="${HOST_SHORT:-$(hostname -s 2>/dev/null || hostname)}"
|
||||
|
||||
if [[ -z "${LAN_IP}" ]]; then
|
||||
echo "ERROR: Could not detect LAN IP. Set LAN_IP in environment."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -d "${TLS_DIR}" ]]; then
|
||||
echo "ERROR: ${TLS_DIR} not found. Start NPMPlus once: ./scripts/start.sh"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SAN="IP:${LAN_IP},DNS:${HOST_SHORT},DNS:localhost"
|
||||
TS="$(date +%Y%m%d%H%M%S)"
|
||||
|
||||
echo "=== NPMPlus admin cert regeneration ==="
|
||||
echo "TLS dir: ${TLS_DIR}"
|
||||
echo "SAN: ${SAN}"
|
||||
echo ""
|
||||
|
||||
if [[ "${EUID}" -ne 0 ]]; then
|
||||
echo "Re-run as root to write certs owned by container:"
|
||||
echo " sudo ${SCRIPT_DIR}/regenerate-admin-cert.sh"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
for f in dummycert.pem dummykey.pem; do
|
||||
if [[ -f "${TLS_DIR}/${f}" ]]; then
|
||||
cp -a "${TLS_DIR}/${f}" "${TLS_DIR}/${f}.bak.${TS}"
|
||||
echo "Backed up ${f} -> ${f}.bak.${TS}"
|
||||
fi
|
||||
done
|
||||
|
||||
TMP="$(mktemp -d)"
|
||||
trap 'rm -rf "${TMP}"' EXIT
|
||||
|
||||
openssl req -x509 -nodes -days 3650 -newkey rsa:4096 \
|
||||
-keyout "${TMP}/dummykey.pem" \
|
||||
-out "${TMP}/dummycert.pem" \
|
||||
-subj "/CN=${HOST_SHORT}" \
|
||||
-addext "subjectAltName=${SAN}"
|
||||
|
||||
install -m 600 -o root -g root "${TMP}/dummykey.pem" "${TLS_DIR}/dummykey.pem"
|
||||
install -m 644 -o root -g root "${TMP}/dummycert.pem" "${TLS_DIR}/dummycert.pem"
|
||||
|
||||
echo ""
|
||||
echo "Installed new cert. Verifying SAN:"
|
||||
openssl x509 -in "${TLS_DIR}/dummycert.pem" -noout -text | grep -A1 'Subject Alternative Name' || true
|
||||
|
||||
if docker ps --format '{{.Names}}' | grep -qx npmplus; then
|
||||
echo ""
|
||||
echo "Restarting npmplus..."
|
||||
docker compose --profile npmplus restart npmplus
|
||||
else
|
||||
echo ""
|
||||
echo "Container npmplus not running — start with: ./scripts/start.sh"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Admin UI: https://${LAN_IP}:81"
|
||||
echo "Use HTTPS (not http). Accept the self-signed cert warning once in the browser."
|
||||
Executable
+68
@@ -0,0 +1,68 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
STACK_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||
|
||||
# shellcheck disable=SC1091
|
||||
source "${SCRIPT_DIR}/ensure-dirs.sh"
|
||||
|
||||
cd "${STACK_DIR}"
|
||||
|
||||
if [[ ! -f .env ]]; then
|
||||
echo "ERROR: .env not found. Run: cp .env.example .env"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
set -a
|
||||
# shellcheck disable=SC1091
|
||||
source .env
|
||||
set +a
|
||||
|
||||
DATA_ROOT="${DATA_ROOT:-/data}"
|
||||
|
||||
if [[ -z "${ACME_EMAIL:-}" || "${ACME_EMAIL}" == "admin@example.com" ]]; then
|
||||
echo "ERROR: Set ACME_EMAIL in .env (required for Let's Encrypt)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! mountpoint -q "${DATA_ROOT}" 2>/dev/null; then
|
||||
echo "ERROR: ${DATA_ROOT} is not mounted"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ensure_npmplus_dirs "${DATA_ROOT}"
|
||||
|
||||
if ! docker info &>/dev/null; then
|
||||
echo "ERROR: Docker is not running"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ss -tlnp 2>/dev/null | grep -qE ':80 |:443 '; then
|
||||
echo "WARNING: Port 80 or 443 already in use — NPMPlus needs both for Let's Encrypt"
|
||||
ss -tlnp 2>/dev/null | grep -E ':80 |:443 ' || true
|
||||
fi
|
||||
|
||||
echo "=== NPMPlus stack ==="
|
||||
echo "Image: ${NPMPLUS_IMAGE:-docker.io/zoeyvid/npmplus:latest}"
|
||||
echo "Data: ${DATA_ROOT}/apps/npmplus"
|
||||
echo "Ports: 80, 443 (tcp+udp), 81 (admin HTTPS)"
|
||||
echo "ACME: ${ACME_EMAIL}"
|
||||
echo ""
|
||||
|
||||
docker compose --profile npmplus pull
|
||||
docker compose --profile npmplus up -d
|
||||
|
||||
echo ""
|
||||
echo "Started. Admin UI (HTTPS only — http:// redirects and may fail in browser):"
|
||||
echo " https://$(hostname -I 2>/dev/null | awk '{print $1}'):81"
|
||||
echo ""
|
||||
echo "If the browser blocks the cert when opening by IP, run:"
|
||||
echo " sudo ./scripts/regenerate-admin-cert.sh"
|
||||
echo ""
|
||||
echo "First login — check logs for random password if INITIAL_ADMIN_PASSWORD unset:"
|
||||
echo " docker compose --profile npmplus logs npmplus | grep -i password"
|
||||
echo ""
|
||||
echo "Prerequisites before proxy host + SSL:"
|
||||
echo " DNS A record → this host's public IP"
|
||||
echo " Router port forward: 80/tcp, 443/tcp, 443/udp → this host"
|
||||
Reference in New Issue
Block a user