Fix 502 after update: safe startup, single DB init, healthcheck

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-06 22:58:09 +03:00
parent 82fdb60f5e
commit 5353c82066
10 changed files with 107 additions and 52 deletions
+1 -1
View File
@@ -28,4 +28,4 @@ ENV GIT_CONFIG_VALUE_0=/repo
EXPOSE 8000 EXPOSE 8000
ENTRYPOINT ["/app/entrypoint.sh"] ENTRYPOINT ["/app/entrypoint.sh"]
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "--workers", "2", "--timeout", "120", "wsgi:app"] CMD ["gunicorn", "--bind", "0.0.0.0:8000", "--workers", "2", "--timeout", "120", "--preload", "wsgi:app"]
+22
View File
@@ -545,6 +545,28 @@ docker compose restart web
docker compose down && docker compose up -d docker compose down && docker compose up -d
``` ```
**502 Bad Gateway (Nginx)**
Nginx не может достучаться до контейнера `web`. Проверьте:
```bash
cd ~/fotohost
docker compose ps
docker compose logs --tail=100 web
curl -I http://127.0.0.1:8080/health
```
Частые причины после обновления:
1. Контейнер `photohost-web` не запущен или перезапускается — смотрите логи `docker compose logs web`
2. В Nginx указан неверный порт — должен совпадать с `APP_PORT` из `.env` (по умолчанию `8080`):
```nginx
proxy_pass http://127.0.0.1:8080;
```
3. База данных ещё не готова — подождите 30–60 секунд и выполните `docker compose restart web`
--- ---
## Технологии ## Технологии
+8 -17
View File
@@ -21,7 +21,7 @@ def load_user(user_id):
return db.session.get(User, int(user_id)) return db.session.get(User, int(user_id))
def create_app(): def create_app(setup_database=True):
app = Flask(__name__) app = Flask(__name__)
app.config["SECRET_KEY"] = os.getenv("SECRET_KEY", "dev-secret-change-me") app.config["SECRET_KEY"] = os.getenv("SECRET_KEY", "dev-secret-change-me")
@@ -63,7 +63,10 @@ def create_app():
def inject_banners(): def inject_banners():
from app.banner_service import get_banners_by_position from app.banner_service import get_banners_by_position
try:
return {"site_banners": get_banners_by_position()} return {"site_banners": get_banners_by_position()}
except Exception:
return {"site_banners": {}}
with app.app_context(): with app.app_context():
from app.models import ( # noqa: F401 from app.models import ( # noqa: F401
@@ -79,23 +82,11 @@ def create_app():
) )
db.create_all() db.create_all()
from app.bootstrap import (
create_first_admin,
ensure_default_group,
ensure_group_limit_columns,
ensure_photo_storage_column,
ensure_schema,
ensure_site_settings,
)
from app.folders import ensure_folder_schema
ensure_schema() if setup_database and os.getenv("SKIP_DB_INIT") != "1":
ensure_default_group(app) from app.bootstrap import run_database_setup
ensure_group_limit_columns()
ensure_folder_schema() run_database_setup(app)
ensure_site_settings(app)
ensure_photo_storage_column()
create_first_admin(app)
return app return app
+34 -15
View File
@@ -12,18 +12,20 @@ def ensure_schema():
tables = inspector.get_table_names() tables = inspector.get_table_names()
if "photos" in tables: if "photos" in tables:
columns = {col["name"] for col in inspector.get_columns("photos")}
if "user_id" not in columns:
db.session.execute( db.session.execute(
text("ALTER TABLE photos ADD COLUMN user_id INTEGER REFERENCES users(id)") text(
"ALTER TABLE photos ADD COLUMN IF NOT EXISTS "
"user_id INTEGER REFERENCES users(id)"
)
) )
db.session.commit() db.session.commit()
if "users" in tables and "user_groups" in tables: if "users" in tables and "user_groups" in tables:
columns = {col["name"] for col in inspector.get_columns("users")}
if "group_id" not in columns:
db.session.execute( db.session.execute(
text("ALTER TABLE users ADD COLUMN group_id INTEGER REFERENCES user_groups(id)") text(
"ALTER TABLE users ADD COLUMN IF NOT EXISTS "
"group_id INTEGER REFERENCES user_groups(id)"
)
) )
db.session.commit() db.session.commit()
@@ -65,15 +67,17 @@ def ensure_group_limit_columns():
if "user_groups" not in inspector.get_table_names(): if "user_groups" not in inspector.get_table_names():
return return
columns = {col["name"] for col in inspector.get_columns("user_groups")}
if "max_folders" not in columns:
db.session.execute( db.session.execute(
text("ALTER TABLE user_groups ADD COLUMN max_folders INTEGER NOT NULL DEFAULT 10") text(
"ALTER TABLE user_groups ADD COLUMN IF NOT EXISTS "
"max_folders INTEGER NOT NULL DEFAULT 10"
)
) )
db.session.commit()
if "max_photos" not in columns:
db.session.execute( db.session.execute(
text("ALTER TABLE user_groups ADD COLUMN max_photos INTEGER NOT NULL DEFAULT 500") text(
"ALTER TABLE user_groups ADD COLUMN IF NOT EXISTS "
"max_photos INTEGER NOT NULL DEFAULT 500"
)
) )
db.session.commit() db.session.commit()
@@ -91,12 +95,27 @@ def ensure_photo_storage_column():
inspector = inspect(db.engine) inspector = inspect(db.engine)
if "photos" not in inspector.get_table_names(): if "photos" not in inspector.get_table_names():
return return
columns = {col["name"] for col in inspector.get_columns("photos")} db.session.execute(
if "storage_backend" not in columns: text(
db.session.execute(text("ALTER TABLE photos ADD COLUMN storage_backend VARCHAR(20) DEFAULT 'local'")) "ALTER TABLE photos ADD COLUMN IF NOT EXISTS "
"storage_backend VARCHAR(20) DEFAULT 'local'"
)
)
db.session.commit() db.session.commit()
def run_database_setup(app):
ensure_schema()
ensure_default_group(app)
ensure_group_limit_columns()
from app.folders import ensure_folder_schema
ensure_folder_schema()
ensure_site_settings(app)
ensure_photo_storage_column()
create_first_admin(app)
def slugify(name): def slugify(name):
slug = re.sub(r"[^a-z0-9]+", "-", name.lower().strip()) slug = re.sub(r"[^a-z0-9]+", "-", name.lower().strip())
slug = slug.strip("-") or "group" slug = slug.strip("-") or "group"
+4 -3
View File
@@ -319,9 +319,10 @@ def ensure_folder_schema():
tables = inspector.get_table_names() tables = inspector.get_table_names()
if "photos" in tables: if "photos" in tables:
columns = {col["name"] for col in inspector.get_columns("photos")}
if "folder_id" not in columns:
db.session.execute( db.session.execute(
text("ALTER TABLE photos ADD COLUMN folder_id INTEGER REFERENCES folders(id)") text(
"ALTER TABLE photos ADD COLUMN IF NOT EXISTS "
"folder_id INTEGER REFERENCES folders(id)"
)
) )
db.session.commit() db.session.commit()
+9
View File
@@ -25,6 +25,15 @@ from app.upload_service import process_uploads
bp = Blueprint("main", __name__) bp = Blueprint("main", __name__)
@bp.route("/health")
def health():
try:
db.session.execute(db.text("SELECT 1"))
return {"status": "ok"}, 200
except Exception as exc:
return {"status": "error", "detail": str(exc)}, 503
@bp.route("/") @bp.route("/")
def index(): def index():
photos = Photo.query.order_by(Photo.created_at.desc()).limit(24).all() photos = Photo.query.order_by(Photo.created_at.desc()).limit(24).all()
+6
View File
@@ -44,6 +44,12 @@ services:
depends_on: depends_on:
db: db:
condition: service_healthy condition: service_healthy
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/health', timeout=5)"]
interval: 15s
timeout: 10s
retries: 8
start_period: 40s
volumes: volumes:
postgres_data: postgres_data:
+8 -4
View File
@@ -1,14 +1,18 @@
#!/bin/sh #!/bin/sh
set -e set -e
# Mounted /repo belongs to host user; appuser needs write access for git deploy. # Git deploy needs write access to mounted /repo; never fail container start on chown errors.
if [ "$ALLOW_GIT_DEPLOY" = "true" ] || [ "$ALLOW_GIT_DEPLOY" = "1" ] || [ "$ALLOW_GIT_DEPLOY" = "yes" ]; then if [ "$ALLOW_GIT_DEPLOY" = "true" ] || [ "$ALLOW_GIT_DEPLOY" = "1" ] || [ "$ALLOW_GIT_DEPLOY" = "yes" ]; then
if [ -d /repo/.git ]; then if [ -d /repo/.git ]; then
chown -R appuser:appuser /repo chown -R appuser:appuser /repo 2>/dev/null || true
chmod -R u+rwX /repo/.git chmod -R u+rwX /repo/.git 2>/dev/null || true
elif [ -d /repo ]; then elif [ -d /repo ]; then
chown -R appuser:appuser /repo chown -R appuser:appuser /repo 2>/dev/null || true
fi fi
fi fi
# Run DB migrations once before gunicorn workers start.
gosu appuser python /app/init_db.py
export SKIP_DB_INIT=1
exec gosu appuser "$@" exec gosu appuser "$@"
+3
View File
@@ -0,0 +1,3 @@
from app import create_app
create_app(setup_database=True)
+1 -1
View File
@@ -1,3 +1,3 @@
from app import create_app from app import create_app
app = create_app() app = create_app(setup_database=False)