mirror of
https://github.com/andrey271192/amnezia_web.git
synced 2026-09-20 14:41:59 +00:00
fix(community): PRO install from UI uses docker-cli helper to free :8080
Run install.sh in a disposable docker:26-cli container after docker rm FREE/landing/stale PRO; keeps working after API response. Legacy in-panel bash retained when COMMUNITY_DISABLE_DOCKER_CLI_HELPER=1. Persist related env vars in install.sh from previous container. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -48,6 +48,10 @@ cd /opt/amnezia-admin && chmod +x scripts/install.sh && sudo SKIP_DOWNLOAD=1 bas
|
|||||||
| `COMMUNITY_PRIVATE_INSTALL_SCRIPT_URL` | `https://raw.githubusercontent.com/andrey271192/amnezia_web-pro/main/scripts/install.sh` | Сырой URL `scripts/install.sh` в **вашем** приватном репозитории PRO |
|
| `COMMUNITY_PRIVATE_INSTALL_SCRIPT_URL` | `https://raw.githubusercontent.com/andrey271192/amnezia_web-pro/main/scripts/install.sh` | Сырой URL `scripts/install.sh` в **вашем** приватном репозитории PRO |
|
||||||
| `COMMUNITY_INSTALL_FETCH_MS` | `60000` | Таймаут HTTP при скачивании установщика из GitHub |
|
| `COMMUNITY_INSTALL_FETCH_MS` | `60000` | Таймаут HTTP при скачивании установщика из GitHub |
|
||||||
| `PRIVATE_INSTALL_SCRIPT_MAX_BYTES` | `2097152` | Максимальный размер скачанного скрипта (байты) |
|
| `PRIVATE_INSTALL_SCRIPT_MAX_BYTES` | `2097152` | Максимальный размер скачанного скрипта (байты) |
|
||||||
|
| `COMMUNITY_DISABLE_DOCKER_CLI_HELPER` | _(не задано)_ | Если `1`, установка из UI снова только «bash внутри панели» (часто конфликт **:8080** с PRO). По умолчанию используется одноразовый контейнер **docker:cli**, который **снимает FREE** и зависший PRO перед `install.sh`. |
|
||||||
|
| `COMMUNITY_SKIP_REMOVE_FREE_BEFORE_PRIVATE_PRO` | `0` | `1` — не выполнять `docker rm` FREE/лендинга перед install (если знаете, что делаете). |
|
||||||
|
| `COMMUNITY_PRO_INSTALL_HELPER_IMAGE` | `docker:26-cli` | Образ с бинарём `docker` для фонового контейнера (нужен доступ к демону по сокету). |
|
||||||
|
| `FREE_PANEL_CONTAINER_FOR_PRO_INSTALL` и др. | см. `server.js` | Имена контейнеров для `docker rm` перед install (по умолчанию `amnezia-admin`, `amnezia-web-landing`, `amnezia-admin-pro`). |
|
||||||
|
|
||||||
Чтобы включить форму установки из панели, задайте в контейнере **`AMNEZIA_EDITION=community`** и **`ALLOW_COMMUNITY_GITHUB_ACTIVATION=1`** (`install.sh`: добавьте `-e ALLOW_COMMUNITY_GITHUB_ACTIVATION=1` или пересоздайте контейнер после правки переменных). URL приватного `install.sh` при необходимости переопределите через **`COMMUNITY_PRIVATE_INSTALL_SCRIPT_URL`**.
|
Чтобы включить форму установки из панели, задайте в контейнере **`AMNEZIA_EDITION=community`** и **`ALLOW_COMMUNITY_GITHUB_ACTIVATION=1`** (`install.sh`: добавьте `-e ALLOW_COMMUNITY_GITHUB_ACTIVATION=1` или пересоздайте контейнер после правки переменных). URL приватного `install.sh` при необходимости переопределите через **`COMMUNITY_PRIVATE_INSTALL_SCRIPT_URL`**.
|
||||||
|
|
||||||
@@ -90,7 +94,7 @@ curl -fsSL https://raw.githubusercontent.com/andrey271192/amnezia_web/main/scrip
|
|||||||
|
|
||||||
### `Bind for 0.0.0.0:8080 failed: port is already allocated` у `amnezia-admin-pro`
|
### `Bind for 0.0.0.0:8080 failed: port is already allocated` у `amnezia-admin-pro`
|
||||||
|
|
||||||
На **8080** уже слушает **FREE**‑панель (**`amnezia-admin`**). Перед запуском образа PRO (**`docker compose`**, **`amnezia-web-pro-deploy`**, свой скрипт): **`docker rm -f amnezia-admin`**, затем снова **`up`**. Два контейнера панели на один и тот же порт не поднять.
|
На **8080** уже слушает **FREE**‑панель (**`amnezia-admin`**). Пока она работает, второй контейнер панели (PRO) на тот же порт не поднимется. **Кнопка «Установить PRO» из UI по умолчанию** использует одноразовый контейнер **`docker:cli`** (пауза → **`docker rm`** FREE / лендинг / возможный **`amnezia-admin-pro` в Created** → затем ваш **`install.sh`**). Иначе вручную: **`docker rm -f amnezia-admin`**, затем установщик PRO. Отключить авто-снос: **`COMMUNITY_DISABLE_DOCKER_CLI_HELPER=1`**.
|
||||||
|
|
||||||
### Лендинг: порт 80 занят
|
### Лендинг: порт 80 занят
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "amnezia-admin",
|
"name": "amnezia-admin",
|
||||||
"version": "1.1.9",
|
"version": "1.2.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "amnezia_web — базовая панель AmneziaWG (FREE: просмотр и удаление клиентов; полная версия — PRO).",
|
"description": "amnezia_web — базовая панель AmneziaWG (FREE: просмотр и удаление клиентов; полная версия — PRO).",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -125,6 +125,8 @@ fi
|
|||||||
for __reuse_var in UI_HIDE_SECTIONS UI_HIDE_USERS UI_HIDE_WARP UI_HIDE_CASCADE WARP_SSH_INSTALL_DIR \
|
for __reuse_var in UI_HIDE_SECTIONS UI_HIDE_USERS UI_HIDE_WARP UI_HIDE_CASCADE WARP_SSH_INSTALL_DIR \
|
||||||
ALLOW_COMMUNITY_GITHUB_ACTIVATION COMMUNITY_PRIVATE_INSTALL_SCRIPT_URL COMMUNITY_INSTALL_FETCH_MS PRIVATE_INSTALL_SCRIPT_MAX_BYTES \
|
ALLOW_COMMUNITY_GITHUB_ACTIVATION COMMUNITY_PRIVATE_INSTALL_SCRIPT_URL COMMUNITY_INSTALL_FETCH_MS PRIVATE_INSTALL_SCRIPT_MAX_BYTES \
|
||||||
COMMUNITY_UPGRADE_URL COMMUNITY_UPGRADE_PITCH AMNEZIA_EDITION \
|
COMMUNITY_UPGRADE_URL COMMUNITY_UPGRADE_PITCH AMNEZIA_EDITION \
|
||||||
|
COMMUNITY_DISABLE_DOCKER_CLI_HELPER COMMUNITY_SKIP_REMOVE_FREE_BEFORE_PRIVATE_PRO COMMUNITY_PRO_INSTALL_HELPER_IMAGE \
|
||||||
|
FREE_PANEL_CONTAINER_FOR_PRO_INSTALL FREE_LANDING_CONTAINER_FOR_PRO_INSTALL STALE_PRO_PANEL_CONTAINER_FOR_PRO_INSTALL \
|
||||||
EXPORT_CONFIG_SECRET CLIENT_CONFIG_ENDPOINT CLIENT_EXPORT_DNS1 CLIENT_EXPORT_DNS2; do
|
EXPORT_CONFIG_SECRET CLIENT_CONFIG_ENDPOINT CLIENT_EXPORT_DNS1 CLIENT_EXPORT_DNS2; do
|
||||||
if [[ -z "${!__reuse_var:-}" ]] && [[ -n "${PREV_CONTAINER_ENV}" ]]; then
|
if [[ -z "${!__reuse_var:-}" ]] && [[ -n "${PREV_CONTAINER_ENV}" ]]; then
|
||||||
PREV_VAL=""
|
PREV_VAL=""
|
||||||
@@ -200,7 +202,7 @@ elif [[ -f "${INSTALL_DIR}/.amnezia-panel-edition" ]]; then
|
|||||||
echo "→ AMNEZIA_EDITION из ${INSTALL_DIR}/.amnezia-panel-edition: ${__PE}"
|
echo "→ AMNEZIA_EDITION из ${INSTALL_DIR}/.amnezia-panel-edition: ${__PE}"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
for __ce_var in COMMUNITY_UPGRADE_URL COMMUNITY_UPGRADE_PITCH ALLOW_COMMUNITY_GITHUB_ACTIVATION COMMUNITY_PRIVATE_INSTALL_SCRIPT_URL COMMUNITY_INSTALL_FETCH_MS PRIVATE_INSTALL_SCRIPT_MAX_BYTES; do
|
for __ce_var in COMMUNITY_UPGRADE_URL COMMUNITY_UPGRADE_PITCH ALLOW_COMMUNITY_GITHUB_ACTIVATION COMMUNITY_PRIVATE_INSTALL_SCRIPT_URL COMMUNITY_INSTALL_FETCH_MS PRIVATE_INSTALL_SCRIPT_MAX_BYTES COMMUNITY_DISABLE_DOCKER_CLI_HELPER COMMUNITY_SKIP_REMOVE_FREE_BEFORE_PRIVATE_PRO COMMUNITY_PRO_INSTALL_HELPER_IMAGE FREE_PANEL_CONTAINER_FOR_PRO_INSTALL FREE_LANDING_CONTAINER_FOR_PRO_INSTALL STALE_PRO_PANEL_CONTAINER_FOR_PRO_INSTALL; do
|
||||||
if [[ -n "${!__ce_var:-}" ]]; then
|
if [[ -n "${!__ce_var:-}" ]]; then
|
||||||
RUN_ENV+=( -e "${__ce_var}=${!__ce_var}" )
|
RUN_ENV+=( -e "${__ce_var}=${!__ce_var}" )
|
||||||
fi
|
fi
|
||||||
|
|||||||
131
server.js
131
server.js
@@ -47,6 +47,10 @@ const COMMUNITY_PRIVATE_INSTALL_SCRIPT_URL =
|
|||||||
const PRIVATE_INSTALL_SCRIPT_MAX_BYTES = Number(process.env.PRIVATE_INSTALL_SCRIPT_MAX_BYTES || 2_097_152) || 2_097_152;
|
const PRIVATE_INSTALL_SCRIPT_MAX_BYTES = Number(process.env.PRIVATE_INSTALL_SCRIPT_MAX_BYTES || 2_097_152) || 2_097_152;
|
||||||
/** Флаг выполнения одноразового bash install из UI (держим второй параллельный запрос). */
|
/** Флаг выполнения одноразового bash install из UI (держим второй параллельный запрос). */
|
||||||
let communityPrivateInstallBusy = false;
|
let communityPrivateInstallBusy = false;
|
||||||
|
const COMMUNITY_SKIP_REMOVE_FREE_BEFORE_PRIVATE_PRO = envTruthy(
|
||||||
|
process.env.COMMUNITY_SKIP_REMOVE_FREE_BEFORE_PRIVATE_PRO,
|
||||||
|
);
|
||||||
|
const COMMUNITY_DISABLE_DOCKER_CLI_HELPER = envTruthy(process.env.COMMUNITY_DISABLE_DOCKER_CLI_HELPER);
|
||||||
const COMMUNITY_UPGRADE_URL =
|
const COMMUNITY_UPGRADE_URL =
|
||||||
process.env.COMMUNITY_UPGRADE_URL?.trim() ||
|
process.env.COMMUNITY_UPGRADE_URL?.trim() ||
|
||||||
"https://boosty.to/andrey27/purchase/3906453?ssource=DIRECT&share=subscription_link";
|
"https://boosty.to/andrey27/purchase/3906453?ssource=DIRECT&share=subscription_link";
|
||||||
@@ -367,6 +371,13 @@ function validateGithubBearerToken(tok) {
|
|||||||
return /^[!-~]+$/.test(s);
|
return /^[!-~]+$/.test(s);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Имя контейнера для встраивания в `sh -lc` во внешнем helper (docker rm -f …). */
|
||||||
|
function shellSafeDockerContainerName(raw, fallback) {
|
||||||
|
const s = typeof raw === "string" ? raw.trim() : "";
|
||||||
|
if (/^[a-zA-Z][a-zA-Z0-9_.-]{0,127}$/.test(s)) return s;
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
function privateInstallScriptUrlLogged() {
|
function privateInstallScriptUrlLogged() {
|
||||||
try {
|
try {
|
||||||
const u = new URL(COMMUNITY_PRIVATE_INSTALL_SCRIPT_URL);
|
const u = new URL(COMMUNITY_PRIVATE_INSTALL_SCRIPT_URL);
|
||||||
@@ -1474,21 +1485,35 @@ app.post("/api/community/run-private-install", requireAuth, async (req, res) =>
|
|||||||
const header = `\n${"=".repeat(60)}\n${new Date().toISOString()} — старт install из UI (COMMUNITY)\nURL: ${COMMUNITY_PRIVATE_INSTALL_SCRIPT_URL}\n${"=".repeat(60)}\n`;
|
const header = `\n${"=".repeat(60)}\n${new Date().toISOString()} — старт install из UI (COMMUNITY)\nURL: ${COMMUNITY_PRIVATE_INSTALL_SCRIPT_URL}\n${"=".repeat(60)}\n`;
|
||||||
fs.appendFileSync(logAbs, header);
|
fs.appendFileSync(logAbs, header);
|
||||||
|
|
||||||
const logStream = fs.createWriteStream(logAbs, { flags: "a" });
|
const useDockerCliHelper = !COMMUNITY_DISABLE_DOCKER_CLI_HELPER;
|
||||||
|
const helperImage =
|
||||||
|
process.env.COMMUNITY_PRO_INSTALL_HELPER_IMAGE?.trim() || "docker:26-cli";
|
||||||
|
const freePanelName = shellSafeDockerContainerName(
|
||||||
|
process.env.FREE_PANEL_CONTAINER_FOR_PRO_INSTALL,
|
||||||
|
"amnezia-admin",
|
||||||
|
);
|
||||||
|
const freeLandingName = shellSafeDockerContainerName(
|
||||||
|
process.env.FREE_LANDING_CONTAINER_FOR_PRO_INSTALL,
|
||||||
|
"amnezia-web-landing",
|
||||||
|
);
|
||||||
|
const staleProName = shellSafeDockerContainerName(
|
||||||
|
process.env.STALE_PRO_PANEL_CONTAINER_FOR_PRO_INSTALL,
|
||||||
|
"amnezia-admin-pro",
|
||||||
|
);
|
||||||
|
|
||||||
const child = spawn("bash", [scriptPath], {
|
if (useDockerCliHelper) {
|
||||||
cwd: tmpDir,
|
fs.appendFileSync(
|
||||||
env: {
|
logAbs,
|
||||||
...process.env,
|
`\n→ Одноразовый установщик: ${helperImage}; FREE=${freePanelName}, лендинг=${freeLandingName}, удаление зависшего PRO=${staleProName}. Отключить слой: COMMUNITY_DISABLE_DOCKER_CLI_HELPER=1\n`,
|
||||||
GITHUB_TOKEN: token,
|
);
|
||||||
GH_TOKEN: token,
|
} else {
|
||||||
GIT_TERMINAL_PROMPT: "0",
|
fs.appendFileSync(
|
||||||
},
|
logAbs,
|
||||||
stdio: ["ignore", "pipe", "pipe"],
|
"\n→ COMMUNITY_DISABLE_DOCKER_CLI_HELPER=1 — install идёт в том же контейнере; FREE не удаляется до скрипта (частый конфликт :8080 с PRO).\n",
|
||||||
});
|
);
|
||||||
|
}
|
||||||
|
|
||||||
child.stdout.pipe(logStream, { end: false });
|
const logStream = useDockerCliHelper ? null : fs.createWriteStream(logAbs, { flags: "a" });
|
||||||
child.stderr.pipe(logStream, { end: false });
|
|
||||||
|
|
||||||
let finalized = false;
|
let finalized = false;
|
||||||
const finalize = (code = null, signal = null, errText = "") => {
|
const finalize = (code = null, signal = null, errText = "") => {
|
||||||
@@ -1503,26 +1528,88 @@ app.post("/api/community/run-private-install", requireAuth, async (req, res) =>
|
|||||||
} catch {
|
} catch {
|
||||||
//
|
//
|
||||||
}
|
}
|
||||||
logStream.end(() => {
|
const rmTmp = () => {
|
||||||
try {
|
try {
|
||||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||||
} catch {
|
} catch {
|
||||||
//
|
//
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
if (logStream) {
|
||||||
|
logStream.end(() => rmTmp());
|
||||||
|
} else {
|
||||||
|
rmTmp();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
child.once("exit", (code, signal) => finalize(code, signal));
|
if (useDockerCliHelper) {
|
||||||
child.once("error", (err) => {
|
const dataAbs = path.resolve(DATA_DIR);
|
||||||
console.warn("community-private-install bash:", err?.message || err);
|
const prelude = COMMUNITY_SKIP_REMOVE_FREE_BEFORE_PRIVATE_PRO
|
||||||
finalize(null, null, String(err.message || err));
|
? "sleep 2"
|
||||||
});
|
: `sleep 3; docker rm -f ${freePanelName} ${freeLandingName} 2>/dev/null || true; docker rm -f ${staleProName} 2>/dev/null || true`;
|
||||||
|
const sh = `
|
||||||
|
set -eu
|
||||||
|
${prelude}
|
||||||
|
cd /mnt/stage && exec bash ./install.sh >> /mnt/data/community-install-last.log 2>&1
|
||||||
|
`.trim();
|
||||||
|
|
||||||
|
const dr = spawn(
|
||||||
|
"docker",
|
||||||
|
[
|
||||||
|
"run",
|
||||||
|
"--rm",
|
||||||
|
"--pull=missing",
|
||||||
|
"-v",
|
||||||
|
"/var/run/docker.sock:/var/run/docker.sock",
|
||||||
|
`-v`,
|
||||||
|
`${tmpDir}:/mnt/stage:ro`,
|
||||||
|
`-v`,
|
||||||
|
`${dataAbs}:/mnt/data`,
|
||||||
|
"-e",
|
||||||
|
`GITHUB_TOKEN=${token}`,
|
||||||
|
"-e",
|
||||||
|
`GH_TOKEN=${token}`,
|
||||||
|
"-e",
|
||||||
|
"GIT_TERMINAL_PROMPT=0",
|
||||||
|
helperImage,
|
||||||
|
"sh",
|
||||||
|
"-lc",
|
||||||
|
sh,
|
||||||
|
],
|
||||||
|
{ detached: true, stdio: "ignore" },
|
||||||
|
);
|
||||||
|
dr.unref();
|
||||||
|
dr.once("exit", (code, signal) => finalize(code, signal));
|
||||||
|
dr.once("error", (err) => {
|
||||||
|
console.warn("community-private-install docker-cli helper:", err?.message || err);
|
||||||
|
finalize(null, null, String(err.message || err));
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const child = spawn("bash", [scriptPath], {
|
||||||
|
cwd: tmpDir,
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
GITHUB_TOKEN: token,
|
||||||
|
GH_TOKEN: token,
|
||||||
|
GIT_TERMINAL_PROMPT: "0",
|
||||||
|
},
|
||||||
|
stdio: ["ignore", "pipe", "pipe"],
|
||||||
|
});
|
||||||
|
child.stdout.pipe(logStream, { end: false });
|
||||||
|
child.stderr.pipe(logStream, { end: false });
|
||||||
|
child.once("exit", (code, signal) => finalize(code, signal));
|
||||||
|
child.once("error", (err) => {
|
||||||
|
console.warn("community-private-install bash:", err?.message || err);
|
||||||
|
finalize(null, null, String(err.message || err));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
res.status(202).json({
|
res.status(202).json({
|
||||||
ok: true,
|
ok: true,
|
||||||
queued: true,
|
queued: true,
|
||||||
message:
|
message: useDockerCliHelper
|
||||||
"Установка PRO запущена. Панель может прерваться — через 2–5 минут откройте её снова по тому же адресу. Журнал пишется в /data/community-install-last.log в том контейнере, где была FREE-панель (до возможной замены образа).",
|
? "Установка PRO: отдельный контейнер сначала снимает FREE (освобождается порт панели) и зависший PRO при наличии, затем выполняется install.sh. Подождите 3–10 мин и откройте адрес снова. Журнал: на VPS каталог данных панели (часто /opt/amnezia-admin-data/community-install-last.log)."
|
||||||
|
: "Режим без docker-helper: при «port already allocated» снимите FREE вручную. Журнал: /data/community-install-last.log.",
|
||||||
logInsideContainer: "community-install-last.log",
|
logInsideContainer: "community-install-last.log",
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
Reference in New Issue
Block a user