Fix 502 after update: safe startup, single DB init, healthcheck
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+1
-1
@@ -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"]
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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()
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
@@ -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 "$@"
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
from app import create_app
|
||||||
|
|
||||||
|
create_app(setup_database=True)
|
||||||
Reference in New Issue
Block a user