README에 conda/systemd/Apache 설치 문서 추가

- Miniconda(ncue) 기반 설치/실행 및 systemd 예시 추가
- Apache ProxyPass 설정 및 503 트러블슈팅 절차 정리
- Flask DB pool lazy 생성으로 임포트 단계 장애(503) 완화

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dsyoon
2026-02-08 09:05:47 +09:00
parent 9fe71ad6a4
commit f2119ed8da
2 changed files with 196 additions and 37 deletions

View File

@@ -24,13 +24,6 @@ def env(name: str, default: str = "") -> str:
return str(os.getenv(name, default))
def must(name: str) -> str:
v = env(name).strip()
if not v:
raise RuntimeError(f"Missing env: {name}")
return v
_IDENT_RE = re.compile(r"^[a-zA-Z_][a-zA-Z0-9_]*$")
@@ -51,11 +44,11 @@ def parse_email_csv(s: str) -> list[str]:
PORT = int(env("PORT", "8023") or "8023")
DB_HOST = must("DB_HOST")
DB_HOST = env("DB_HOST", "").strip()
DB_PORT = int(env("DB_PORT", "5432") or "5432")
DB_NAME = must("DB_NAME")
DB_USER = must("DB_USER")
DB_PASSWORD = must("DB_PASSWORD")
DB_NAME = env("DB_NAME", "").strip()
DB_USER = env("DB_USER", "").strip()
DB_PASSWORD = env("DB_PASSWORD", "").strip()
TABLE = safe_ident(env("TABLE", "ncue_user") or "ncue_user")
CONFIG_TABLE = "ncue_app_config"
@@ -71,31 +64,51 @@ AUTH0_GOOGLE_CONNECTION = env("AUTH0_GOOGLE_CONNECTION", "").strip()
# Optional CORS (for static on different origin)
CORS_ORIGINS = env("CORS_ORIGINS", "*").strip() or "*"
_POOL: Optional[psycopg2.pool.SimpleConnectionPool] = None
POOL: psycopg2.pool.SimpleConnectionPool = psycopg2.pool.SimpleConnectionPool(
minconn=1,
maxconn=10,
host=DB_HOST,
port=DB_PORT,
dbname=DB_NAME,
user=DB_USER,
password=DB_PASSWORD,
sslmode="disable",
)
def db_configured() -> bool:
return bool(DB_HOST and DB_NAME and DB_USER and DB_PASSWORD)
def get_pool() -> psycopg2.pool.SimpleConnectionPool:
"""
Lazy DB pool creation.
- prevents app import failure (which causes Apache 503) when DB is temporarily unavailable
- /api/config/auth can still work purely from .env without DB
"""
global _POOL
if _POOL is not None:
return _POOL
if not db_configured():
raise RuntimeError("db_not_configured")
_POOL = psycopg2.pool.SimpleConnectionPool(
minconn=1,
maxconn=10,
host=DB_HOST,
port=DB_PORT,
dbname=DB_NAME,
user=DB_USER,
password=DB_PASSWORD,
sslmode="disable",
)
return _POOL
def db_exec(sql: str, params: Tuple[Any, ...] = ()) -> None:
conn = POOL.getconn()
pool = get_pool()
conn = pool.getconn()
try:
conn.autocommit = True
with conn.cursor() as cur:
cur.execute(sql, params)
finally:
POOL.putconn(conn)
pool.putconn(conn)
def db_one(sql: str, params: Tuple[Any, ...] = ()) -> Optional[tuple]:
conn = POOL.getconn()
pool = get_pool()
conn = pool.getconn()
try:
conn.autocommit = True
with conn.cursor() as cur:
@@ -103,7 +116,7 @@ def db_one(sql: str, params: Tuple[Any, ...] = ()) -> Optional[tuple]:
row = cur.fetchone()
return row
finally:
POOL.putconn(conn)
pool.putconn(conn)
def ensure_user_table() -> None:
@@ -248,6 +261,8 @@ def static_files(filename: str) -> Response:
@app.get("/healthz")
def healthz() -> Response:
try:
if not db_configured():
return jsonify({"ok": False, "error": "db_not_configured"}), 500
row = db_one("select 1 as ok")
if not row:
return jsonify({"ok": False}), 500
@@ -259,6 +274,8 @@ def healthz() -> Response:
@app.post("/api/auth/sync")
def api_auth_sync() -> Response:
try:
if not db_configured():
return jsonify({"ok": False, "error": "db_not_configured"}), 500
ensure_user_table()
id_token = bearer_token()
if not id_token:
@@ -318,6 +335,8 @@ def api_auth_sync() -> Response:
@app.post("/api/auth/logout")
def api_auth_logout() -> Response:
try:
if not db_configured():
return jsonify({"ok": False, "error": "db_not_configured"}), 500
ensure_user_table()
id_token = bearer_token()
if not id_token:
@@ -364,6 +383,8 @@ def api_config_auth_get() -> Response:
}
)
if not db_configured():
return jsonify({"ok": False, "error": "not_set"}), 404
ensure_config_table()
row = db_one(f"select value, updated_at from public.{CONFIG_TABLE} where key = %s", ("auth",))
if not row:
@@ -384,6 +405,8 @@ def api_config_auth_get() -> Response:
@app.post("/api/config/auth")
def api_config_auth_post() -> Response:
try:
if not db_configured():
return jsonify({"ok": False, "error": "db_not_configured"}), 500
ensure_config_table()
if not CONFIG_TOKEN:
return jsonify({"ok": False, "error": "config_token_not_set"}), 403