From 2bd778117f776881521bd3545541cde2834ea250 Mon Sep 17 00:00:00 2001 From: Fusion Date: Wed, 20 May 2026 16:23:16 +0200 Subject: [PATCH] feat(tunnels): switch to temporary Cloudflare tunnels Replace persistent Cloudflare tunnels (API-based) with temporary tunnels using 'cloudflared tunnel --url'. This removes the need for Cloudflare API tokens, DNS records, and persistent tunnel management. Changes: - Install cloudflared binary in API Dockerfile - Add start_cloudflared_tunnel() and stop_cloudflared_tunnel() to docker.py - Update instance start/stop/restart/delete to use temporary tunnels - Store tunnel PID in tunnel_id field, temporary URL in url/public_url - Remove Cloudflare API service (cloudflare_tunnel.py) - Remove cloudflared container from docker-compose - Remove Cloudflare env vars (CLOUDFLARE_API_TOKEN, ZONE_ID, etc.) - Remove Cloudflare configuration from config.py - Remove Cloudflare startup check from main.py - Remove /health/cloudflare endpoint --- apps/api/Dockerfile | 2 + apps/api/src/api/health.py | 18 - apps/api/src/api/tool_instances.py | 78 ++--- apps/api/src/config.py | 7 +- apps/api/src/main.py | 12 - apps/api/src/services/cloudflare_tunnel.py | 376 --------------------- apps/api/src/services/docker.py | 76 +++++ docker-compose.traefik.yml | 20 -- 8 files changed, 113 insertions(+), 476 deletions(-) delete mode 100644 apps/api/src/services/cloudflare_tunnel.py diff --git a/apps/api/Dockerfile b/apps/api/Dockerfile index ed836c3..f6ceaf8 100644 --- a/apps/api/Dockerfile +++ b/apps/api/Dockerfile @@ -38,6 +38,8 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ "$(. /etc/os-release && echo "$VERSION_CODENAME")" stable" > /etc/apt/sources.list.d/docker.list \ && apt-get update \ && apt-get install -y --no-install-recommends docker-ce-cli docker-compose-plugin \ + && curl -L --output /usr/local/bin/cloudflared https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64 \ + && chmod +x /usr/local/bin/cloudflared \ && rm -rf /var/lib/apt/lists/* # Copy dependencies from builder diff --git a/apps/api/src/api/health.py b/apps/api/src/api/health.py index e2bc382..380112e 100644 --- a/apps/api/src/api/health.py +++ b/apps/api/src/api/health.py @@ -147,21 +147,3 @@ async def health_check_db() -> dict[str, Any]: status="unhealthy", response_time_ms=0.0, ).model_dump() - - -@router.get( - "/health/cloudflare", - summary="Cloudflare tunnel health check", - description="Returns Cloudflare tunnel configuration status and diagnostics.", - tags=["Health"], -) -async def health_check_cloudflare() -> dict[str, Any]: - """Check Cloudflare tunnel configuration. - - Returns: - Dict with Cloudflare configuration status. - """ - from src.services.cloudflare_tunnel import check_cloudflare_config - - result = await check_cloudflare_config() - return result diff --git a/apps/api/src/api/tool_instances.py b/apps/api/src/api/tool_instances.py index ce57425..94f0bd9 100644 --- a/apps/api/src/api/tool_instances.py +++ b/apps/api/src/api/tool_instances.py @@ -22,12 +22,13 @@ from src.models.tool_config import ToolConfig from src.models.tool_instance import ToolInstance from src.models.tool_type import ToolType from src.models.user import User -from src.services.cloudflare_tunnel import create_tunnel, delete_tunnel from src.services.docker import ( ensure_instance_directory, execute_compose_command, find_free_port, get_container_id, + start_cloudflared_tunnel, + stop_cloudflared_tunnel, get_container_name, get_container_logs, get_container_status, @@ -420,24 +421,23 @@ async def start_instance( logger.info("Tool type for instance %s: name=%s, default_port=%s", instance.id, tool_type.name if tool_type else "unknown", instance_port) - # Create Cloudflare tunnel for public access + # Create temporary Cloudflare tunnel for public access try: - logger.info("Creating Cloudflare tunnel for instance %s (name=%s, port=%d)", - instance.id, instance.name, instance_port) - tunnel_info = await create_tunnel( - instance_name=instance.name, - instance_id=str(instance.id), - instance_port=instance_port, + logger.info("Creating temporary tunnel for instance %s (container=%s, port=%d)", + instance.id, instance.container_name, instance_port) + tunnel_info = start_cloudflared_tunnel( + container_name=instance.container_name or instance.name, + port=instance_port, ) - instance.tunnel_id = tunnel_info["tunnel_id"] - instance.public_url = tunnel_info["public_url"] - instance.url = tunnel_info["public_url"] + instance.tunnel_id = tunnel_info["pid"] + instance.public_url = tunnel_info["url"] + instance.url = tunnel_info["url"] await session.commit() logger.info( - "Created tunnel for instance %s: tunnel_id=%s, url=%s", + "Created temporary tunnel for instance %s: pid=%s, url=%s", instance.id, - tunnel_info["tunnel_id"], - tunnel_info["public_url"], + tunnel_info["pid"], + tunnel_info["url"], ) except Exception as exc: import traceback @@ -488,16 +488,13 @@ async def stop_instance( status_code=status.HTTP_404_NOT_FOUND, detail="instance not found" ) - # Delete Cloudflare tunnel if exists + # Stop Cloudflare tunnel if exists if instance.tunnel_id: try: - await delete_tunnel( - tunnel_id=instance.tunnel_id, - subdomain=f"instance-{str(instance.id)[:8]}", - ) - logger.info("Deleted tunnel for instance %s", instance.id) + stop_cloudflared_tunnel(instance.tunnel_id) + logger.info("Stopped tunnel for instance %s (pid=%s)", instance.id, instance.tunnel_id) except Exception as exc: - logger.warning("Failed to delete tunnel for instance %s: %s", instance.id, exc) + logger.warning("Failed to stop tunnel for instance %s: %s", instance.id, exc) if instance.compose_path and os.path.exists(instance.compose_path): execute_compose_command(instance.compose_path, "stop") @@ -545,16 +542,13 @@ async def restart_instance( status_code=status.HTTP_404_NOT_FOUND, detail="instance not found" ) - # Delete old tunnel if exists + # Stop old tunnel if exists if instance.tunnel_id: try: - await delete_tunnel( - tunnel_id=instance.tunnel_id, - subdomain=f"instance-{str(instance.id)[:8]}", - ) - logger.info("Deleted old tunnel for instance %s", instance.id) + stop_cloudflared_tunnel(instance.tunnel_id) + logger.info("Stopped old tunnel for instance %s (pid=%s)", instance.id, instance.tunnel_id) except Exception as exc: - logger.warning("Failed to delete old tunnel for instance %s: %s", instance.id, exc) + logger.warning("Failed to stop old tunnel for instance %s: %s", instance.id, exc) if instance.compose_path and os.path.exists(instance.compose_path): returncode, stdout, stderr = execute_compose_command( @@ -569,20 +563,19 @@ async def restart_instance( tool_type = await session.get(ToolType, instance.tool_type_id) instance_port = tool_type.default_port if tool_type and tool_type.default_port else 8080 - # Create new tunnel + # Create new temporary tunnel try: - tunnel_info = await create_tunnel( - instance_name=instance.name, - instance_id=str(instance.id), - instance_port=instance_port, + tunnel_info = start_cloudflared_tunnel( + container_name=instance.container_name or instance.name, + port=instance_port, ) - instance.tunnel_id = tunnel_info["tunnel_id"] - instance.public_url = tunnel_info["public_url"] - instance.url = tunnel_info["public_url"] + instance.tunnel_id = tunnel_info["pid"] + instance.public_url = tunnel_info["url"] + instance.url = tunnel_info["url"] logger.info( "Created new tunnel for instance %s: %s", instance.id, - tunnel_info["public_url"], + tunnel_info["url"], ) except Exception as exc: logger.warning( @@ -633,16 +626,13 @@ async def delete_instance( status_code=status.HTTP_404_NOT_FOUND, detail="instance not found" ) - # Delete Cloudflare tunnel if exists + # Stop Cloudflare tunnel if exists if instance.tunnel_id: try: - await delete_tunnel( - tunnel_id=instance.tunnel_id, - subdomain=f"instance-{str(instance.id)[:8]}", - ) - logger.info("Deleted tunnel for instance %s", instance.id) + stop_cloudflared_tunnel(instance.tunnel_id) + logger.info("Stopped tunnel for instance %s (pid=%s)", instance.id, instance.tunnel_id) except Exception as exc: - logger.warning("Failed to delete tunnel for instance %s: %s", instance.id, exc) + logger.warning("Failed to stop tunnel for instance %s: %s", instance.id, exc) # Stop and remove container if instance.compose_path and os.path.exists(instance.compose_path): diff --git a/apps/api/src/config.py b/apps/api/src/config.py index e731c39..60b5db6 100644 --- a/apps/api/src/config.py +++ b/apps/api/src/config.py @@ -53,12 +53,7 @@ class Settings(BaseSettings): # Tool instance storage instance_base_path: str = "/data/instances" - # Cloudflare Tunnel configuration - cloudflare_api_token: str | None = None - cloudflare_zone_id: str | None = None - cloudflare_account_id: str | None = None - cloudflare_base_domain: str | None = None - cloudflared_config_dir: str = "/etc/cloudflared" + model_config = SettingsConfigDict(env_file=".env", extra="ignore", populate_by_name=True) diff --git a/apps/api/src/main.py b/apps/api/src/main.py index d235b19..db98f1f 100644 --- a/apps/api/src/main.py +++ b/apps/api/src/main.py @@ -229,18 +229,6 @@ async def on_startup(): # Seed built-in data await seed_builtin_tool_types() - - # Check Cloudflare configuration - settings = Settings() - if settings.cloudflare_api_token and settings.cloudflare_account_id and settings.cloudflare_zone_id and settings.cloudflare_base_domain: - logger.info("Cloudflare tunnel configuration detected: base_domain=%s", settings.cloudflare_base_domain) - else: - logger.warning( - "Cloudflare tunnel configuration incomplete. " - "Tunnels will not be created. Set CLOUDFLARE_API_TOKEN, " - "CLOUDFLARE_ACCOUNT_ID, CLOUDFLARE_ZONE_ID, and CLOUDFLARE_BASE_DOMAIN." - ) - logger.info("Startup complete.") app.include_router(health_router) diff --git a/apps/api/src/services/cloudflare_tunnel.py b/apps/api/src/services/cloudflare_tunnel.py deleted file mode 100644 index 9d0a9a7..0000000 --- a/apps/api/src/services/cloudflare_tunnel.py +++ /dev/null @@ -1,376 +0,0 @@ -"""Cloudflare Tunnel management service.""" - -import json -import logging -import os -import uuid -from pathlib import Path -from typing import Any - -import httpx -from src.config import Settings - -logger = logging.getLogger(__name__) - -CLOUDFLARE_API_BASE = "https://api.cloudflare.com/client/v4" - - -def _get_headers(settings: Settings) -> dict[str, str]: - """Get Cloudflare API headers.""" - return { - "Authorization": f"Bearer {settings.cloudflare_api_token}", - "Content-Type": "application/json", - } - - -async def create_tunnel( - instance_name: str, - instance_id: str, - instance_port: int = 8080, - settings: Settings | None = None, -) -> dict[str, str]: - """Create a Cloudflare tunnel for an instance. - - Args: - instance_name: Name of the instance (used for tunnel name) - instance_id: UUID of the instance - instance_port: Internal port the container listens on - settings: Optional settings override - - Returns: - Dict with tunnel_id and public_url - """ - if settings is None: - settings = Settings() - - logger.info("Settings loaded: api_token=%s, account_id=%s, zone_id=%s, base_domain=%s", - "set" if settings.cloudflare_api_token else "NOT SET", - settings.cloudflare_account_id or "NOT SET", - settings.cloudflare_zone_id or "NOT SET", - settings.cloudflare_base_domain or "NOT SET") - - if not settings.cloudflare_api_token: - raise ValueError("CLOUDFLARE_API_TOKEN not configured") - - logger.info("Creating Cloudflare tunnel for instance_name=%s, instance_id=%s, port=%d", - instance_name, instance_id, instance_port) - - headers = _get_headers(settings) - account_id = settings.cloudflare_account_id - - try: - # Create tunnel - async with httpx.AsyncClient() as client: - logger.info("Step 1: Creating tunnel via Cloudflare API...") - response = await client.post( - f"{CLOUDFLARE_API_BASE}/accounts/{account_id}/cfd_tunnel", - headers=headers, - json={ - "name": f"headquarter-{instance_name}", - "config_src": "cloudflare", - }, - ) - logger.info("Tunnel creation response: status=%d, body=%s", response.status_code, response.text[:500]) - - if response.status_code == 403: - raise ValueError(f"Cloudflare API authentication failed. Check your API token permissions. Body: {response.text}") - if response.status_code == 400: - raise ValueError(f"Cloudflare API bad request: {response.text}") - - response.raise_for_status() - data = response.json() - - if not data.get("success"): - errors = data.get('errors', []) - raise ValueError(f"Failed to create tunnel: {errors}") - - tunnel = data["result"] - tunnel_id = tunnel["id"] - logger.info("Step 1 complete: tunnel_id=%s", tunnel_id) - - # Get tunnel token - logger.info("Step 2: Getting tunnel token...") - token_response = await client.get( - f"{CLOUDFLARE_API_BASE}/accounts/{account_id}/cfd_tunnel/{tunnel_id}/token", - headers=headers, - ) - logger.info("Token response: status=%d", token_response.status_code) - - if token_response.status_code != 200: - raise ValueError(f"Failed to get tunnel token: {token_response.status_code} - {token_response.text}") - - token_data = token_response.json() - tunnel_token = token_data["result"] - logger.info("Step 2 complete: got tunnel token") - - # Create DNS record for the tunnel - subdomain = f"instance-{instance_id[:8]}" - hostname = f"{subdomain}.{settings.cloudflare_base_domain}" - logger.info("Step 3: Creating DNS record for subdomain=%s, hostname=%s", subdomain, hostname) - - dns_response = await client.post( - f"{CLOUDFLARE_API_BASE}/zones/{settings.cloudflare_zone_id}/dns_records", - headers=headers, - json={ - "type": "CNAME", - "name": subdomain, - "content": f"{tunnel_id}.cfargotunnel.com", - "ttl": 1, - "proxied": True, - }, - ) - logger.info("DNS response: status=%d, body=%s", dns_response.status_code, dns_response.text[:500]) - - if dns_response.status_code == 403: - raise ValueError(f"Cloudflare DNS API authentication failed. Check token has Zone:Edit permission.") - if dns_response.status_code == 400: - raise ValueError(f"Cloudflare DNS bad request: {dns_response.text}") - - dns_response.raise_for_status() - logger.info("Step 3 complete: DNS record created") - - # Update cloudflared config - logger.info("Step 4: Updating cloudflared config...") - await update_cloudflared_config( - tunnel_id=tunnel_id, - tunnel_token=tunnel_token, - hostname=hostname, - instance_name=instance_name, - instance_port=instance_port, - settings=settings, - ) - logger.info("Step 4 complete: cloudflared config updated") - - logger.info("Tunnel creation complete: tunnel_id=%s, public_url=https://%s", tunnel_id, hostname) - return { - "tunnel_id": tunnel_id, - "public_url": f"https://{hostname}", - "subdomain": subdomain, - } - except Exception as e: - logger.error("Failed to create Cloudflare tunnel: %s", str(e), exc_info=True) - raise - - -async def delete_tunnel( - tunnel_id: str, - subdomain: str, - settings: Settings | None = None, -) -> None: - """Delete a Cloudflare tunnel and its DNS record. - - Args: - tunnel_id: Cloudflare tunnel ID - subdomain: Subdomain to remove DNS record for - settings: Optional settings override - """ - if settings is None: - settings = Settings() - - if not settings.cloudflare_api_token: - raise ValueError("CLOUDFLARE_API_TOKEN not configured") - - headers = _get_headers(settings) - account_id = settings.cloudflare_account_id - - async with httpx.AsyncClient() as client: - # Delete DNS record - # First find the DNS record ID - dns_list = await client.get( - f"{CLOUDFLARE_API_BASE}/zones/{settings.cloudflare_zone_id}/dns_records", - headers=headers, - params={"name": f"{subdomain}.{settings.cloudflare_base_domain}"}, - ) - dns_list.raise_for_status() - dns_data = dns_list.json() - - if dns_data.get("success") and dns_data.get("result"): - for record in dns_data["result"]: - await client.delete( - f"{CLOUDFLARE_API_BASE}/zones/{settings.cloudflare_zone_id}/dns_records/{record['id']}", - headers=headers, - ) - - # Delete tunnel - await client.delete( - f"{CLOUDFLARE_API_BASE}/accounts/{account_id}/cfd_tunnel/{tunnel_id}", - headers=headers, - ) - - # Remove from cloudflared config - await remove_tunnel_from_config(tunnel_id, settings) - - -async def update_cloudflared_config( - tunnel_id: str, - tunnel_token: str, - hostname: str, - instance_name: str, - instance_port: int = 8080, - settings: Settings | None = None, -) -> None: - """Update the cloudflared config.yml with a new tunnel. - - Args: - tunnel_id: Cloudflare tunnel ID - tunnel_token: Tunnel token for authentication - hostname: Public hostname for the tunnel - instance_name: Instance name for the service - settings: Optional settings override - """ - if settings is None: - settings = Settings() - - config_dir = Path(settings.cloudflared_config_dir) - config_file = config_dir / "config.yml" - credentials_file = config_dir / f"{tunnel_id}.json" - - # Ensure config directory exists - config_dir.mkdir(parents=True, exist_ok=True) - - # Write tunnel credentials - credentials = { - "AccountTag": settings.cloudflare_account_id, - "TunnelID": tunnel_id, - "TunnelName": f"headquarter-{instance_name}", - "TunnelSecret": tunnel_token, - } - with open(credentials_file, "w") as f: - json.dump(credentials, f, indent=2) - - # Read existing config or create new one - config: dict = {"tunnel": "", "credentials-file": "", "ingress": []} - if config_file.exists(): - import yaml - with open(config_file, "r") as f: - config = yaml.safe_load(f) or config - - # Update tunnel and credentials-file (should point to latest) - config["tunnel"] = tunnel_id - config["credentials-file"] = str(credentials_file) - - # Add ingress rule for this instance - ingress_rule = { - "hostname": hostname, - "service": f"http://{instance_name}:{instance_port}", - } - - # Remove existing rule for this hostname if present - config["ingress"] = [ - rule for rule in config.get("ingress", []) - if rule.get("hostname") != hostname - ] - - # Add new rule and catch-all - config["ingress"].append(ingress_rule) - config["ingress"].append({"service": "http_status:404"}) - - # Write updated config - import yaml - with open(config_file, "w") as f: - yaml.dump(config, f, default_flow_style=False) - - logger.info("Updated cloudflared config for tunnel %s", tunnel_id) - - -async def remove_tunnel_from_config( - tunnel_id: str, - settings: Settings | None = None, -) -> None: - """Remove a tunnel from the cloudflared config. - - Args: - tunnel_id: Cloudflare tunnel ID to remove - settings: Optional settings override - """ - if settings is None: - settings = Settings() - - config_dir = Path(settings.cloudflared_config_dir) - config_file = config_dir / "config.yml" - credentials_file = config_dir / f"{tunnel_id}.json" - - if not config_file.exists(): - return - - import yaml - with open(config_file, "r") as f: - config = yaml.safe_load(f) or {} - - # Remove ingress rules for this tunnel - config["ingress"] = [ - rule for rule in config.get("ingress", []) - if rule.get("hostname") != f"instance-{tunnel_id[:8]}.{settings.cloudflare_base_domain}" - ] - - # Write updated config - with open(config_file, "w") as f: - yaml.dump(config, f, default_flow_style=False) - - # Remove credentials file - if credentials_file.exists(): - credentials_file.unlink() - - logger.info("Removed tunnel %s from cloudflared config", tunnel_id) - - -async def check_cloudflare_config(settings: Settings | None = None) -> dict[str, Any]: - """Check if Cloudflare configuration is valid and working. - - Args: - settings: Optional settings override - - Returns: - Dict with status and diagnostic information - """ - if settings is None: - settings = Settings() - - result: dict[str, Any] = { - "configured": False, - "api_token_set": bool(settings.cloudflare_api_token), - "account_id_set": bool(settings.cloudflare_account_id), - "zone_id_set": bool(settings.cloudflare_zone_id), - "base_domain_set": bool(settings.cloudflare_base_domain), - "api_test": None, - "errors": [], - } - - if not all([ - settings.cloudflare_api_token, - settings.cloudflare_account_id, - settings.cloudflare_zone_id, - settings.cloudflare_base_domain, - ]): - result["errors"].append("Missing required Cloudflare configuration") - return result - - result["configured"] = True - - # Test API connectivity - try: - headers = _get_headers(settings) - async with httpx.AsyncClient() as client: - # Test account access - resp = await client.get( - f"{CLOUDFLARE_API_BASE}/accounts/{settings.cloudflare_account_id}", - headers=headers, - ) - if resp.status_code == 200: - result["api_test"] = "ok" - elif resp.status_code == 403: - result["api_test"] = "auth_failed" - result["errors"].append("API token authentication failed - check token permissions") - else: - result["api_test"] = f"error_{resp.status_code}" - result["errors"].append(f"API test failed: {resp.status_code}") - except Exception as e: - result["api_test"] = "exception" - result["errors"].append(f"API test exception: {str(e)}") - - # Check config directory - config_dir = Path(settings.cloudflared_config_dir) - result["config_dir_exists"] = config_dir.exists() - result["config_dir_writable"] = os.access(config_dir, os.W_OK) if config_dir.exists() else False - - return result diff --git a/apps/api/src/services/docker.py b/apps/api/src/services/docker.py index cccc210..0ce0293 100644 --- a/apps/api/src/services/docker.py +++ b/apps/api/src/services/docker.py @@ -232,3 +232,79 @@ def find_free_port(start: int = 10000, end: int = 20000) -> int: return port raise RuntimeError(f"No free port found in range {start}-{end}") + + +import subprocess +import time +import re + + +def start_cloudflared_tunnel( + container_name: str, port: int, timeout: int = 30 +) -> dict[str, str]: + """Start a temporary Cloudflare tunnel for a container. + + Uses 'cloudflared tunnel --url' to create a temporary tunnel + with a random trycloudflare.com URL. + + Args: + container_name: Name of the Docker container to tunnel to + port: Port number the container listens on + timeout: Maximum seconds to wait for tunnel URL + + Returns: + Dict with 'url' (the public tunnel URL) and 'pid' (process ID) + """ + import subprocess + import time + import re + + # Run cloudflared in background, capture output + proc = subprocess.Popen( + ["cloudflared", "tunnel", "--url", f"http://{container_name}:{port}"], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + ) + + # Wait for the URL to appear in output + url_pattern = re.compile(r"https://[a-z0-9-]+\.trycloudflare\.com") + start_time = time.time() + url = None + + while time.time() - start_time < timeout: + # Read available output + import select + readable, _, _ = select.select([proc.stdout], [], [], 1.0) + if readable: + line = proc.stdout.readline() + if line: + match = url_pattern.search(line) + if match: + url = match.group(0) + break + + if not url: + proc.terminate() + proc.wait(timeout=5) + raise RuntimeError( + f"Failed to get tunnel URL within {timeout}s. " + f"cloudflared output may contain errors." + ) + + return {"url": url, "pid": str(proc.pid)} + + +def stop_cloudflared_tunnel(pid: str) -> None: + """Stop a cloudflared tunnel process. + + Args: + pid: Process ID of the cloudflared tunnel + """ + import os + import signal + + try: + os.kill(int(pid), signal.SIGTERM) + except ProcessLookupError: + pass # Already stopped diff --git a/docker-compose.traefik.yml b/docker-compose.traefik.yml index 96f27d3..7bf6a14 100644 --- a/docker-compose.traefik.yml +++ b/docker-compose.traefik.yml @@ -91,17 +91,11 @@ services: AUTHENTIK_APPLICATION_SLUG: ${AUTHENTIK_APPLICATION_SLUG:-headquarter-web} AUTHENTIK_AUTHORIZE_URL: ${AUTHENTIK_AUTHORIZE_URL:-} AUTHENTIK_TOKEN_URL: ${AUTHENTIK_TOKEN_URL:-} - CLOUDFLARE_API_TOKEN: ${CLOUDFLARE_API_TOKEN} - CLOUDFLARE_ZONE_ID: ${CLOUDFLARE_ZONE_ID} - CLOUDFLARE_ACCOUNT_ID: ${CLOUDFLARE_ACCOUNT_ID} - CLOUDFLARE_BASE_DOMAIN: ${CLOUDFLARE_BASE_DOMAIN} - CLOUDFLARED_CONFIG_DIR: /etc/cloudflared volumes: - repo_data:/data/repos - instance_data:/data/instances - avatar_uploads:/app/uploads - /var/run/docker.sock:/var/run/docker.sock - - cloudflared_config:/etc/cloudflared depends_on: postgres: condition: service_healthy @@ -119,26 +113,12 @@ services: - "traefik.http.routers.headquarter-api.tls.certresolver=${TRAEFIK_CERT_RESOLVER:-letsencrypt}" - "traefik.http.services.headquarter-api.loadbalancer.server.port=8000" - # Cloudflare Tunnel - cloudflared: - image: cloudflare/cloudflared:latest - container_name: hq-cloudflared - command: tunnel --config /etc/cloudflared/config.yml run - volumes: - - cloudflared_config:/etc/cloudflared - networks: - - backend - restart: unless-stopped - depends_on: - - headquarter-api - volumes: postgres_data: redis_data: repo_data: instance_data: avatar_uploads: - cloudflared_config: networks: backend: