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).
|
||||
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` на сервере **не обязателен** — все переменные читаются из Compose / Dokploy UI.
|
||||
|
||||
+9
-5
@@ -1,11 +1,15 @@
|
||||
from passlib.context import CryptContext
|
||||
|
||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||
import bcrypt
|
||||
|
||||
|
||||
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:
|
||||
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:
|
||||
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
|
||||
|
||||
# IMPORTANT for Dokploy: attach domain to this service, port 8000
|
||||
panel:
|
||||
build:
|
||||
context: .
|
||||
@@ -24,6 +8,8 @@ services:
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
expose:
|
||||
- "8000"
|
||||
ports:
|
||||
- "${PANEL_PORT:-8000}:8000"
|
||||
environment:
|
||||
@@ -39,7 +25,7 @@ services:
|
||||
POSTGRES_USER: ${POSTGRES_USER:-vpn}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-vpn}
|
||||
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_PORT: ${PUBLIC_PORT:-51820}
|
||||
@@ -62,8 +48,25 @@ services:
|
||||
test: ["CMD", "curl", "-f", "http://127.0.0.1:8000/health"]
|
||||
interval: 15s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 20s
|
||||
retries: 10
|
||||
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:
|
||||
vpn_net:
|
||||
|
||||
@@ -5,7 +5,6 @@ asyncpg==0.30.0
|
||||
alembic==1.14.0
|
||||
pydantic==2.10.4
|
||||
pydantic-settings==2.7.0
|
||||
passlib[bcrypt]==1.7.4
|
||||
bcrypt==4.2.1
|
||||
python-multipart==0.0.20
|
||||
jinja2==3.1.5
|
||||
|
||||
+18
-2
@@ -1,6 +1,21 @@
|
||||
#!/bin/sh
|
||||
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..."
|
||||
python - <<'PY'
|
||||
import asyncio
|
||||
@@ -11,7 +26,7 @@ from sqlalchemy import text
|
||||
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"]
|
||||
engine = create_async_engine(url)
|
||||
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}")
|
||||
await asyncio.sleep(1)
|
||||
await engine.dispose()
|
||||
print("ERROR: database did not become ready", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
@@ -37,5 +53,5 @@ alembic upgrade head
|
||||
echo "Bootstrapping admin..."
|
||||
python -m app.bootstrap
|
||||
|
||||
echo "Starting application..."
|
||||
echo "Starting uvicorn..."
|
||||
exec "$@"
|
||||
|
||||
Reference in New Issue
Block a user