Fix Dokploy Bad Gateway: panel-first compose and stable boot
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -40,6 +40,19 @@ POSTGRES_PASSWORD=надежный-пароль-бд
|
|||||||
4. Deploy. Панель: порт `8000` (или домен через Traefik в Dokploy).
|
4. Deploy. Панель: порт `8000` (или домен через Traefik в Dokploy).
|
||||||
5. Вход: `/admin/login`
|
5. Вход: `/admin/login`
|
||||||
|
|
||||||
|
### Если Bad Gateway (502)
|
||||||
|
|
||||||
|
1. В Dokploy Domain укажите сервис **`panel`**, порт **`8000`** (не `postgres`).
|
||||||
|
2. Откройте логи сервиса `panel` — должны быть строки `Database is ready` и `Starting uvicorn`.
|
||||||
|
3. Задайте `DATABASE_URL` явно, если пароль БД содержит спецсимволы:
|
||||||
|
|
||||||
|
```env
|
||||||
|
DATABASE_URL=postgresql+asyncpg://vpn:ПАРОЛЬ@postgres:5432/vpn_panel
|
||||||
|
POSTGRES_PASSWORD=ПАРОЛЬ
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Redeploy после правок.
|
||||||
|
|
||||||
Админ при каждом старте **синхронизируется из ENV**.
|
Админ при каждом старте **синхронизируется из ENV**.
|
||||||
|
|
||||||
Файл `.env` на сервере **не обязателен** — все переменные читаются из Compose / Dokploy UI.
|
Файл `.env` на сервере **не обязателен** — все переменные читаются из Compose / Dokploy UI.
|
||||||
|
|||||||
+9
-5
@@ -1,11 +1,15 @@
|
|||||||
from passlib.context import CryptContext
|
import bcrypt
|
||||||
|
|
||||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
|
||||||
|
|
||||||
|
|
||||||
def hash_password(password: str) -> str:
|
def hash_password(password: str) -> str:
|
||||||
return pwd_context.hash(password)
|
raw = password.encode("utf-8")
|
||||||
|
# bcrypt limit is 72 bytes
|
||||||
|
digest = bcrypt.hashpw(raw[:72], bcrypt.gensalt())
|
||||||
|
return digest.decode("utf-8")
|
||||||
|
|
||||||
|
|
||||||
def verify_password(password: str, password_hash: str) -> bool:
|
def verify_password(password: str, password_hash: str) -> bool:
|
||||||
return pwd_context.verify(password, password_hash)
|
try:
|
||||||
|
return bcrypt.checkpw(password.encode("utf-8")[:72], password_hash.encode("utf-8"))
|
||||||
|
except ValueError:
|
||||||
|
return False
|
||||||
|
|||||||
+23
-20
@@ -1,21 +1,5 @@
|
|||||||
services:
|
services:
|
||||||
postgres:
|
# IMPORTANT for Dokploy: attach domain to this service, port 8000
|
||||||
image: postgres:16-alpine
|
|
||||||
restart: unless-stopped
|
|
||||||
environment:
|
|
||||||
POSTGRES_USER: ${POSTGRES_USER:-vpn}
|
|
||||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-vpn}
|
|
||||||
POSTGRES_DB: ${POSTGRES_DB:-vpn_panel}
|
|
||||||
volumes:
|
|
||||||
- postgres_data:/var/lib/postgresql/data
|
|
||||||
networks:
|
|
||||||
- vpn_net
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"]
|
|
||||||
interval: 5s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 10
|
|
||||||
|
|
||||||
panel:
|
panel:
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
@@ -24,6 +8,8 @@ services:
|
|||||||
depends_on:
|
depends_on:
|
||||||
postgres:
|
postgres:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
expose:
|
||||||
|
- "8000"
|
||||||
ports:
|
ports:
|
||||||
- "${PANEL_PORT:-8000}:8000"
|
- "${PANEL_PORT:-8000}:8000"
|
||||||
environment:
|
environment:
|
||||||
@@ -39,7 +25,7 @@ services:
|
|||||||
POSTGRES_USER: ${POSTGRES_USER:-vpn}
|
POSTGRES_USER: ${POSTGRES_USER:-vpn}
|
||||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-vpn}
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-vpn}
|
||||||
POSTGRES_DB: ${POSTGRES_DB:-vpn_panel}
|
POSTGRES_DB: ${POSTGRES_DB:-vpn_panel}
|
||||||
DATABASE_URL: postgresql+asyncpg://${POSTGRES_USER:-vpn}:${POSTGRES_PASSWORD:-vpn}@postgres:5432/${POSTGRES_DB:-vpn_panel}
|
DATABASE_URL: ${DATABASE_URL:-postgresql+asyncpg://vpn:vpn@postgres:5432/vpn_panel}
|
||||||
|
|
||||||
PUBLIC_HOST: ${PUBLIC_HOST:-127.0.0.1}
|
PUBLIC_HOST: ${PUBLIC_HOST:-127.0.0.1}
|
||||||
PUBLIC_PORT: ${PUBLIC_PORT:-51820}
|
PUBLIC_PORT: ${PUBLIC_PORT:-51820}
|
||||||
@@ -62,8 +48,25 @@ services:
|
|||||||
test: ["CMD", "curl", "-f", "http://127.0.0.1:8000/health"]
|
test: ["CMD", "curl", "-f", "http://127.0.0.1:8000/health"]
|
||||||
interval: 15s
|
interval: 15s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 5
|
retries: 10
|
||||||
start_period: 20s
|
start_period: 90s
|
||||||
|
|
||||||
|
postgres:
|
||||||
|
image: postgres:16-alpine
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: ${POSTGRES_USER:-vpn}
|
||||||
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-vpn}
|
||||||
|
POSTGRES_DB: ${POSTGRES_DB:-vpn_panel}
|
||||||
|
volumes:
|
||||||
|
- postgres_data:/var/lib/postgresql/data
|
||||||
|
networks:
|
||||||
|
- vpn_net
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 10
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
vpn_net:
|
vpn_net:
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ asyncpg==0.30.0
|
|||||||
alembic==1.14.0
|
alembic==1.14.0
|
||||||
pydantic==2.10.4
|
pydantic==2.10.4
|
||||||
pydantic-settings==2.7.0
|
pydantic-settings==2.7.0
|
||||||
passlib[bcrypt]==1.7.4
|
|
||||||
bcrypt==4.2.1
|
bcrypt==4.2.1
|
||||||
python-multipart==0.0.20
|
python-multipart==0.0.20
|
||||||
jinja2==3.1.5
|
jinja2==3.1.5
|
||||||
|
|||||||
+18
-2
@@ -1,6 +1,21 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
|
echo "==> VpnPanel starting"
|
||||||
|
|
||||||
|
# Always build DATABASE_URL from POSTGRES_* (Dokploy-friendly)
|
||||||
|
export DATABASE_URL="$(
|
||||||
|
python - <<'PY'
|
||||||
|
import os
|
||||||
|
from urllib.parse import quote_plus
|
||||||
|
|
||||||
|
user = os.environ.get("POSTGRES_USER", "vpn")
|
||||||
|
password = os.environ.get("POSTGRES_PASSWORD", "vpn")
|
||||||
|
db = os.environ.get("POSTGRES_DB", "vpn_panel")
|
||||||
|
print(f"postgresql+asyncpg://{quote_plus(user)}:{quote_plus(password)}@postgres:5432/{db}")
|
||||||
|
PY
|
||||||
|
)"
|
||||||
|
|
||||||
echo "Waiting for database..."
|
echo "Waiting for database..."
|
||||||
python - <<'PY'
|
python - <<'PY'
|
||||||
import asyncio
|
import asyncio
|
||||||
@@ -11,7 +26,7 @@ from sqlalchemy import text
|
|||||||
from sqlalchemy.ext.asyncio import create_async_engine
|
from sqlalchemy.ext.asyncio import create_async_engine
|
||||||
|
|
||||||
|
|
||||||
async def wait_db(retries: int = 40) -> None:
|
async def wait_db(retries: int = 60) -> None:
|
||||||
url = os.environ["DATABASE_URL"]
|
url = os.environ["DATABASE_URL"]
|
||||||
engine = create_async_engine(url)
|
engine = create_async_engine(url)
|
||||||
for i in range(retries):
|
for i in range(retries):
|
||||||
@@ -25,6 +40,7 @@ async def wait_db(retries: int = 40) -> None:
|
|||||||
print(f"DB not ready ({i + 1}/{retries}): {exc}")
|
print(f"DB not ready ({i + 1}/{retries}): {exc}")
|
||||||
await asyncio.sleep(1)
|
await asyncio.sleep(1)
|
||||||
await engine.dispose()
|
await engine.dispose()
|
||||||
|
print("ERROR: database did not become ready", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
@@ -37,5 +53,5 @@ alembic upgrade head
|
|||||||
echo "Bootstrapping admin..."
|
echo "Bootstrapping admin..."
|
||||||
python -m app.bootstrap
|
python -m app.bootstrap
|
||||||
|
|
||||||
echo "Starting application..."
|
echo "Starting uvicorn..."
|
||||||
exec "$@"
|
exec "$@"
|
||||||
|
|||||||
Reference in New Issue
Block a user