fix: add MEALIE_INTERNAL_URL env var for Docker-to-Docker API calls

When deployed behind Cloudflare Tunnel, requests from the container to
the external Mealie URL fail with 530 (hairpin). MEALIE_INTERNAL_URL
lets the container use the Docker-internal address (e.g. http://mealie:9000)
for API calls while keeping the external URL for browser links.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-24 07:58:59 +01:00
parent 92912c5890
commit 9a59b38fd6
3 changed files with 13 additions and 7 deletions
+4
View File
@@ -7,6 +7,10 @@ from pathlib import Path
DATA_DIR = Path(os.environ.get("DATA_DIR", "/data")) DATA_DIR = Path(os.environ.get("DATA_DIR", "/data"))
CONFIG_FILE = DATA_DIR / "config.json" CONFIG_FILE = DATA_DIR / "config.json"
# Optional internal URL for Docker-to-Docker API calls (avoids Cloudflare hairpin).
# The user-facing mealie_url is still used for browser links.
MEALIE_INTERNAL_URL = os.environ.get("MEALIE_INTERNAL_URL", "").strip().rstrip("/")
_DEFAULTS = { _DEFAULTS = {
"mealie_url": "", "mealie_url": "",
"mealie_api_key": "", "mealie_api_key": "",
+3 -2
View File
@@ -56,7 +56,7 @@ def settings_test():
if not url or not key: if not url or not key:
return jsonify({"ok": False, "error": "Nincs megadva Mealie URL vagy API kulcs."}) return jsonify({"ok": False, "error": "Nincs megadva Mealie URL vagy API kulcs."})
try: try:
client = MealieClient(url, key) client = MealieClient(url, key, api_url=config.MEALIE_INTERNAL_URL)
info = client.test_connection() info = client.test_connection()
return jsonify({"ok": True, "data": info}) return jsonify({"ok": True, "data": info})
except Exception as exc: except Exception as exc:
@@ -98,7 +98,8 @@ def send_to_mealie():
return jsonify({"ok": False, "error": "Érvénytelen kérés."}) return jsonify({"ok": False, "error": "Érvénytelen kérés."})
try: try:
client = MealieClient(cfg["mealie_url"], cfg["mealie_api_key"]) client = MealieClient(cfg["mealie_url"], cfg["mealie_api_key"],
api_url=config.MEALIE_INTERNAL_URL)
slug = client.create_recipe(payload) slug = client.create_recipe(payload)
recipe_url = f"{cfg['mealie_url']}/g/home/r/{slug}" recipe_url = f"{cfg['mealie_url']}/g/home/r/{slug}"
return jsonify({"ok": True, "slug": slug, "url": recipe_url}) return jsonify({"ok": True, "slug": slug, "url": recipe_url})
+6 -5
View File
@@ -8,8 +8,9 @@ import requests
class MealieClient: class MealieClient:
"""Thin wrapper around the Mealie REST API.""" """Thin wrapper around the Mealie REST API."""
def __init__(self, base_url: str, api_key: str): def __init__(self, base_url: str, api_key: str, api_url: str = ""):
self.base_url = base_url.rstrip("/") self.base_url = base_url.rstrip("/")
self.api_url = api_url.rstrip("/") if api_url else self.base_url
self.session = requests.Session() self.session = requests.Session()
self.session.headers.update({ self.session.headers.update({
"Authorization": f"Bearer {api_key}", "Authorization": f"Bearer {api_key}",
@@ -22,7 +23,7 @@ class MealieClient:
def test_connection(self) -> dict: def test_connection(self) -> dict:
"""Return Mealie app info or raise on failure.""" """Return Mealie app info or raise on failure."""
r = self.session.get(f"{self.base_url}/api/app/about", timeout=10) r = self.session.get(f"{self.api_url}/api/app/about", timeout=10)
r.raise_for_status() r.raise_for_status()
return r.json() return r.json()
@@ -34,7 +35,7 @@ class MealieClient:
""" """
# Step 1: create stub # Step 1: create stub
r = self.session.post( r = self.session.post(
f"{self.base_url}/api/recipes", f"{self.api_url}/api/recipes",
json={"name": recipe["title"]}, json={"name": recipe["title"]},
timeout=15, timeout=15,
) )
@@ -44,7 +45,7 @@ class MealieClient:
# Step 2: build full payload and PATCH # Step 2: build full payload and PATCH
payload = self._build_payload(recipe) payload = self._build_payload(recipe)
r = self.session.patch( r = self.session.patch(
f"{self.base_url}/api/recipes/{slug}", f"{self.api_url}/api/recipes/{slug}",
json=payload, json=payload,
timeout=15, timeout=15,
) )
@@ -107,7 +108,7 @@ class MealieClient:
"image": (f"recipe.{ext}", io.BytesIO(img_resp.content), content_type), "image": (f"recipe.{ext}", io.BytesIO(img_resp.content), content_type),
} }
r = self.session.put( r = self.session.put(
f"{self.base_url}/api/recipes/{slug}/image", f"{self.api_url}/api/recipes/{slug}/image",
files=files, files=files,
timeout=30, timeout=30,
) )