Fix Dokploy Bad Gateway: panel-first compose and stable boot

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
test2
2026-07-25 20:44:47 +03:00
co-authored by Cursor
parent 8320a7651e
commit d78797db82
5 changed files with 63 additions and 28 deletions
+13
View File
@@ -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
View File
@@ -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
View File
@@ -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:
-1
View File
@@ -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
View File
@@ -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 "$@"