Harden database startup for Portainer
This commit is contained in:
@@ -154,6 +154,12 @@ not need to bind-mount individual files like `database/init.sql` or
|
|||||||
`mqtt/mosquitto.conf`. This avoids file-versus-directory mount errors in
|
`mqtt/mosquitto.conf`. This avoids file-versus-directory mount errors in
|
||||||
Portainer Git stacks.
|
Portainer Git stacks.
|
||||||
|
|
||||||
|
The backend receives PostgreSQL connection settings as separate variables
|
||||||
|
(`POSTGRES_HOST`, `POSTGRES_DB`, `POSTGRES_USER`, `POSTGRES_PASSWORD`) and
|
||||||
|
builds the SQLAlchemy URL internally. This avoids broken database URLs when the
|
||||||
|
password contains special characters. The backend also retries the database
|
||||||
|
connection during startup to tolerate Portainer service/DNS startup delays.
|
||||||
|
|
||||||
### 4. Verify
|
### 4. Verify
|
||||||
|
|
||||||
Open:
|
Open:
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
|
|
||||||
|
from sqlalchemy.engine import URL
|
||||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||||
|
|
||||||
|
|
||||||
@@ -7,13 +8,33 @@ class Settings(BaseSettings):
|
|||||||
model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8", extra="ignore")
|
model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8", extra="ignore")
|
||||||
|
|
||||||
app_name: str = "Plast Track MVP"
|
app_name: str = "Plast Track MVP"
|
||||||
database_url: str = "postgresql+psycopg://plasttrack:plasttrack@database:5432/plasttrack"
|
database_url: str | None = None
|
||||||
|
postgres_host: str = "database"
|
||||||
|
postgres_port: int = 5432
|
||||||
|
postgres_db: str = "plasttrack"
|
||||||
|
postgres_user: str = "plasttrack"
|
||||||
|
postgres_password: str = "plasttrack"
|
||||||
mqtt_host: str = "mqtt"
|
mqtt_host: str = "mqtt"
|
||||||
mqtt_port: int = 1883
|
mqtt_port: int = 1883
|
||||||
mqtt_topic: str = "plasttrack/machines/+/events"
|
mqtt_topic: str = "plasttrack/machines/+/events"
|
||||||
mqtt_client_id: str = "plast-track-backend"
|
mqtt_client_id: str = "plast-track-backend"
|
||||||
frontend_origin: str = "http://localhost:5173"
|
frontend_origin: str = "http://localhost:5173"
|
||||||
downtime_check_interval_sec: int = 5
|
downtime_check_interval_sec: int = 5
|
||||||
|
database_startup_retries: int = 30
|
||||||
|
database_startup_retry_delay_sec: float = 2.0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def sqlalchemy_database_url(self) -> str:
|
||||||
|
if self.database_url:
|
||||||
|
return self.database_url
|
||||||
|
return URL.create(
|
||||||
|
"postgresql+psycopg",
|
||||||
|
username=self.postgres_user,
|
||||||
|
password=self.postgres_password,
|
||||||
|
host=self.postgres_host,
|
||||||
|
port=self.postgres_port,
|
||||||
|
database=self.postgres_db,
|
||||||
|
).render_as_string(hide_password=False)
|
||||||
|
|
||||||
|
|
||||||
@lru_cache
|
@lru_cache
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ class Base(DeclarativeBase):
|
|||||||
|
|
||||||
|
|
||||||
settings = get_settings()
|
settings = get_settings()
|
||||||
engine = create_engine(settings.database_url, pool_pre_ping=True)
|
engine = create_engine(settings.sqlalchemy_database_url, pool_pre_ping=True)
|
||||||
SessionLocal = sessionmaker(bind=engine, autoflush=False, autocommit=False, expire_on_commit=False)
|
SessionLocal = sessionmaker(bind=engine, autoflush=False, autocommit=False, expire_on_commit=False)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ from datetime import date, datetime, timezone
|
|||||||
|
|
||||||
from fastapi import Depends, FastAPI, HTTPException, Query, WebSocket, WebSocketDisconnect
|
from fastapi import Depends, FastAPI, HTTPException, Query, WebSocket, WebSocketDisconnect
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from sqlalchemy import func, select
|
from sqlalchemy import func, select, text
|
||||||
|
from sqlalchemy.exc import OperationalError
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from .config import get_settings
|
from .config import get_settings
|
||||||
@@ -112,8 +113,23 @@ async def downtime_monitor() -> None:
|
|||||||
await asyncio.sleep(settings.downtime_check_interval_sec)
|
await asyncio.sleep(settings.downtime_check_interval_sec)
|
||||||
|
|
||||||
|
|
||||||
|
async def wait_for_database() -> None:
|
||||||
|
last_error: OperationalError | None = None
|
||||||
|
for _ in range(settings.database_startup_retries):
|
||||||
|
try:
|
||||||
|
with engine.connect() as connection:
|
||||||
|
connection.execute(text("SELECT 1"))
|
||||||
|
return
|
||||||
|
except OperationalError as error:
|
||||||
|
last_error = error
|
||||||
|
await asyncio.sleep(settings.database_startup_retry_delay_sec)
|
||||||
|
if last_error is not None:
|
||||||
|
raise last_error
|
||||||
|
|
||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def lifespan(app: FastAPI):
|
async def lifespan(app: FastAPI):
|
||||||
|
await wait_for_database()
|
||||||
Base.metadata.create_all(bind=engine)
|
Base.metadata.create_all(bind=engine)
|
||||||
with SessionLocal() as session:
|
with SessionLocal() as session:
|
||||||
seed_defaults(session)
|
seed_defaults(session)
|
||||||
|
|||||||
@@ -30,7 +30,11 @@ services:
|
|||||||
build:
|
build:
|
||||||
context: ./backend
|
context: ./backend
|
||||||
environment:
|
environment:
|
||||||
DATABASE_URL: postgresql+psycopg://${POSTGRES_USER:-plasttrack}:${POSTGRES_PASSWORD:-plasttrack}@database:5432/${POSTGRES_DB:-plasttrack}
|
POSTGRES_HOST: database
|
||||||
|
POSTGRES_PORT: 5432
|
||||||
|
POSTGRES_DB: ${POSTGRES_DB:-plasttrack}
|
||||||
|
POSTGRES_USER: ${POSTGRES_USER:-plasttrack}
|
||||||
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-plasttrack}
|
||||||
MQTT_HOST: mqtt
|
MQTT_HOST: mqtt
|
||||||
MQTT_PORT: 1883
|
MQTT_PORT: 1883
|
||||||
MQTT_TOPIC: plasttrack/machines/+/events
|
MQTT_TOPIC: plasttrack/machines/+/events
|
||||||
|
|||||||
Reference in New Issue
Block a user