import json import os import re import shutil import subprocess import time from datetime import datetime, timezone import requests from flask import Flask, jsonify, redirect, render_template_string, request app = Flask(__name__) CF_API_TOKEN = os.environ["CF_API_TOKEN"] CF_ZONE_ID = os.environ["CF_ZONE_ID"] SELF_NAME = os.environ.get("SELF_CONTAINER_NAME", "") API_TOKEN = os.environ["PANEL_API_TOKEN"] SERVER_IP = os.environ.get("SERVER_IP", "148.135.181.126") ROUTER_RULE_RE = re.compile(r"^traefik\.http\.routers\.[^.]+\.rule$") def check_auth(): auth = request.headers.get("Authorization", "") return auth == f"Bearer {API_TOKEN}" def extract_hostname(labels): """Scan ALL traefik router rule labels, not just one matching the container's own name — compose-based containers get auto-generated docker names that no longer match the Traefik router name.""" for key, value in labels.items(): if ROUTER_RULE_RE.match(key) and "`" in value: return value.split("`")[1] return None def human_relative(iso_ts): if not iso_ts or iso_ts.startswith("0001-01-01"): return None try: dt = datetime.fromisoformat(iso_ts.replace("Z", "+00:00")) except ValueError: return None secs = int((datetime.now(timezone.utc) - dt).total_seconds()) if secs < 60: return f"{secs} sn önce" mins = secs // 60 if mins < 60: return f"{mins} dk önce" hours = mins // 60 if hours < 24: return f"{hours} sa önce" days = hours // 24 return f"{days} gün önce" def _cpu_times(): with open("/proc/stat") as f: parts = f.readline().split()[1:] nums = [int(x) for x in parts] idle = nums[3] + nums[4] total = sum(nums) return idle, total def server_stats(): stats = {} idle1, total1 = _cpu_times() time.sleep(0.3) idle2, total2 = _cpu_times() d_idle = idle2 - idle1 d_total = total2 - total1 stats["cpu_percent"] = round((1 - d_idle / d_total) * 100, 1) if d_total > 0 else 0.0 stats["cpu_cores"] = os.cpu_count() with open("/proc/loadavg") as f: stats["load"] = " / ".join(f.read().split()[:3]) meminfo = {} with open("/proc/meminfo") as f: for line in f: key, _, rest = line.partition(":") meminfo[key] = int(rest.strip().split()[0]) mem_total = meminfo.get("MemTotal", 0) mem_available = meminfo.get("MemAvailable", 0) mem_used = mem_total - mem_available stats["mem_used_gb"] = round(mem_used / 1024 / 1024, 1) stats["mem_total_gb"] = round(mem_total / 1024 / 1024, 1) stats["mem_percent"] = round(mem_used / mem_total * 100, 1) if mem_total else 0.0 disk = shutil.disk_usage("/") stats["disk_used_gb"] = round(disk.used / 1024**3, 1) stats["disk_total_gb"] = round(disk.total / 1024**3, 1) stats["disk_percent"] = round(disk.used / disk.total * 100, 1) if disk.total else 0.0 with open("/proc/uptime") as f: up_secs = int(float(f.read().split()[0])) days, rem = divmod(up_secs, 86400) hours, _ = divmod(rem, 3600) stats["uptime"] = f"{days} gün {hours} saat" if days else f"{hours} saat" all_states = subprocess.run( ["docker", "ps", "-a", "--format", "{{.State}}"], capture_output=True, text=True, ).stdout.split() stats["containers_running"] = sum(1 for s in all_states if s == "running") stats["containers_total"] = len(all_states) return stats def inspect_all(cid): return json.loads(subprocess.run( ["docker", "inspect", cid], capture_output=True, text=True, check=True ).stdout)[0] def port_summary(info): ports = info.get("NetworkSettings", {}).get("Ports") or {} out = [] for container_port, bindings in ports.items(): if not bindings: continue for b in bindings: out.append(f"{b.get('HostPort')}→{container_port}") return ", ".join(out) if out else None def list_projects(): out = subprocess.run( ["docker", "ps", "-a", "--filter", "label=traefik.enable=true", "--format", "{{.ID}}"], capture_output=True, text=True, check=True, ).stdout.split() projects = [] for cid in out: info = inspect_all(cid) name = info["Name"].lstrip("/") if name == SELF_NAME: continue labels = info["Config"]["Labels"] or {} state = info["State"] status = state["Status"] when = human_relative(state.get("StartedAt") if status == "running" else state.get("FinishedAt")) projects.append({ "name": name, "hostname": extract_hostname(labels), "status": status, "since": when, "image": info["Config"]["Image"], }) projects.sort(key=lambda p: p["name"]) return projects def list_all_containers(): out = subprocess.run( ["docker", "ps", "-a", "--format", "{{.ID}}"], capture_output=True, text=True, check=True, ).stdout.split() containers = [] for cid in out: info = inspect_all(cid) name = info["Name"].lstrip("/") if name == SELF_NAME: continue labels = info["Config"]["Labels"] or {} state = info["State"] status = state["Status"] when = human_relative(state.get("StartedAt") if status == "running" else state.get("FinishedAt")) containers.append({ "name": name, "hostname": extract_hostname(labels), "status": status, "since": when, "image": info["Config"]["Image"], "ports": port_summary(info), }) containers.sort(key=lambda c: c["name"]) return containers def list_images(): out = subprocess.run( ["docker", "images", "--format", "{{json .}}"], capture_output=True, text=True, check=True, ).stdout.strip().splitlines() images = [] for line in out: if not line: continue d = json.loads(line) repo, tag = d.get("Repository", ""), d.get("Tag", "") if repo == "" and tag == "": label = d.get("ID", "")[:19] else: label = f"{repo}:{tag}" images.append({ "id": d.get("ID"), "label": label, "size": d.get("Size"), "created": d.get("CreatedSince"), }) images.sort(key=lambda i: i["label"]) return images def list_volumes(): out = subprocess.run( ["docker", "volume", "ls", "--format", "{{json .}}"], capture_output=True, text=True, check=True, ).stdout.strip().splitlines() volumes = [] for line in out: if not line: continue d = json.loads(line) volumes.append({"name": d.get("Name"), "driver": d.get("Driver")}) volumes.sort(key=lambda v: v["name"]) return volumes def docker_system_df(): out = subprocess.run(["docker", "system", "df"], capture_output=True, text=True).stdout lines = out.strip().splitlines() rows = [] for line in lines[1:]: parts = re.split(r"\s{2,}", line.strip()) if len(parts) >= 4: rows.append({ "type": parts[0], "total": parts[1], "active": parts[2], "size": parts[3], "reclaimable": parts[4] if len(parts) > 4 else "", }) return rows def find_container_hostname(name): result = subprocess.run(["docker", "inspect", name], capture_output=True, text=True) if result.returncode != 0: return None info = json.loads(result.stdout)[0] labels = info["Config"]["Labels"] or {} return extract_hostname(labels) def ensure_dns(hostname): existing = requests.get( f"https://api.cloudflare.com/client/v4/zones/{CF_ZONE_ID}/dns_records", params={"type": "A", "name": hostname}, headers={"Authorization": f"Bearer {CF_API_TOKEN}"}, timeout=10, ).json() if existing.get("result"): return {"created": False} requests.post( f"https://api.cloudflare.com/client/v4/zones/{CF_ZONE_ID}/dns_records", headers={"Authorization": f"Bearer {CF_API_TOKEN}", "Content-Type": "application/json"}, json={"type": "A", "name": hostname, "content": SERVER_IP, "ttl": 1, "proxied": True}, timeout=10, ) return {"created": True} def delete_dns_record(hostname): if not hostname: return r = requests.get( f"https://api.cloudflare.com/client/v4/zones/{CF_ZONE_ID}/dns_records", params={"name": hostname}, headers={"Authorization": f"Bearer {CF_API_TOKEN}"}, timeout=10, ).json() for rec in r.get("result", []): requests.delete( f"https://api.cloudflare.com/client/v4/zones/{CF_ZONE_ID}/dns_records/{rec['id']}", headers={"Authorization": f"Bearer {CF_API_TOKEN}"}, timeout=10, ) BASE_STYLE = """ body { font-family: system-ui, sans-serif; background: #0f172a; color: #e2e8f0; padding: 2.5rem; } h1 { color: #38bdf8; margin-bottom: 0.2rem; } h2 { color: #38bdf8; font-size: 1.1rem; margin: 2rem 0 0.8rem; } .sub { opacity: 0.55; font-size: 0.85rem; margin-bottom: 1rem; } table { width: 100%; border-collapse: collapse; } td, th { padding: 0.7rem; border-bottom: 1px solid #334155; text-align: left; vertical-align: middle; } th { opacity: 0.7; font-weight: 600; font-size: 0.85rem; text-transform: uppercase; letter-spacing: .03em; } a { color: #38bdf8; text-decoration: none; } a:hover { text-decoration: underline; } .badge { padding: 0.25rem 0.65rem; border-radius: 999px; font-size: 0.78rem; font-weight: 600; } .running { background: #065f46; color: #d1fae5; } .exited { background: #7f1d1d; color: #fecaca; } .since { opacity: 0.55; font-size: 0.8rem; display: block; } .actions { display: flex; gap: 0.4rem; flex-wrap: wrap; } button, .btn { border: none; padding: 0.4rem 0.8rem; border-radius: 6px; cursor: pointer; font-size: 0.85rem; color: white; } .btn-restart, .btn-start { background: #1e3a8a; } .btn-restart:hover, .btn-start:hover { background: #1e40af; } .btn-stop { background: #78350f; } .btn-stop:hover { background: #92400e; } .btn-logs { background: #334155; } .btn-logs:hover { background: #475569; } .btn-delete { background: #7f1d1d; } .btn-delete:hover { background: #991b1b; } .btn-prune { background: #4c1d95; } .btn-prune:hover { background: #5b21b6; } .empty { opacity: 0.6; margin-top: 1.5rem; } form { display: inline; } code { background: #1e293b; padding: 0.1rem 0.4rem; border-radius: 4px; font-size: 0.85rem; } .stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 1rem; margin-bottom: 2rem; } .stat-card { background: #1e293b; border-radius: 10px; padding: 1rem 1.2rem; } .stat-label { font-size: 0.75rem; opacity: 0.6; text-transform: uppercase; letter-spacing: .03em; } .stat-value { font-size: 1.5rem; font-weight: 700; margin: 0.15rem 0 0.5rem; } .stat-sub { font-size: 0.78rem; opacity: 0.55; } .bar { height: 6px; border-radius: 999px; background: #334155; overflow: hidden; margin-top: 0.5rem; } .bar-fill { height: 100%; background: #38bdf8; } .bar-fill.warn { background: #f59e0b; } .bar-fill.crit { background: #ef4444; } nav { margin-bottom: 1.5rem; display: flex; gap: 1.2rem; border-bottom: 1px solid #334155; padding-bottom: 0.8rem; } nav a { font-size: 0.9rem; opacity: 0.65; } nav a.active { opacity: 1; color: #38bdf8; font-weight: 600; } """ NAV = """ """ PROJECTS_TEMPLATE = """ Deploy Panel

Deploy Panel

""" + NAV + """
{{ projects|length }} proje · 20 saniyede bir otomatik yenilenir · şimdi yenile
CPU ({{ s.cpu_cores }} çekirdek)
%{{ s.cpu_percent }}
yük: {{ s.load }}
Bellek
%{{ s.mem_percent }}
{{ s.mem_used_gb }} / {{ s.mem_total_gb }} GB
Disk
%{{ s.disk_percent }}
{{ s.disk_used_gb }} / {{ s.disk_total_gb }} GB
Sunucu
{{ s.uptime }}
çalışma süresi
Container
{{ s.containers_running }} / {{ s.containers_total }}
çalışan / toplam
{% if projects %} {% for p in projects %} {% endfor %}
ProjeAdresDurumİmaj
{{ p.name }} {% if p.hostname %}{{ p.hostname }}{% else %}—{% endif %} {{ p.status }} {% if p.since %}{{ p.since }}{% endif %} {{ p.image }}
Loglar
{% else %}

Henüz deploy edilmiş bir proje yok.

{% endif %} """ CONTAINERS_TEMPLATE = """ Container'lar - Deploy Panel

Deploy Panel

""" + NAV + """
Sunucudaki TÜM container'lar (sadece Traefik projeleri değil) · {{ containers|length }} adet
{% if containers %} {% for c in containers %} {% endfor %}
İsimAdresPortlarDurumİmaj
{{ c.name }} {% if c.hostname %}{{ c.hostname }}{% else %}—{% endif %} {{ c.ports or '—' }} {{ c.status }} {% if c.since %}{{ c.since }}{% endif %} {{ c.image }}
Loglar {% if c.status == 'running' %}
{% else %}
{% endif %}
{% else %}

Hiç container yok.

{% endif %} """ SYSTEM_TEMPLATE = """ Sistem & Disk - Deploy Panel

Deploy Panel

""" + NAV + """
Docker'ın diskte kapladığı alan ve imaj/volume yönetimi
{% for row in df %} {% endfor %}
TürToplamKullanımdaBoyutGeri kazanılabilir
{{ row.type }} {{ row.total }} {{ row.active }} {{ row.size }} {{ row.reclaimable }}

İmajlar ({{ images|length }})

{% if images %} {% for img in images %} {% endfor %}
İmajBoyutOluşturulma
{{ img.label }} {{ img.size }} {{ img.created }}
{% else %}

Hiç imaj yok.

{% endif %}

Volume'ler ({{ volumes|length }})

{% if volumes %} {% for v in volumes %} {% endfor %}
İsimDriver
{{ v.name }} {{ v.driver }}
{% else %}

Hiç volume yok.

{% endif %} """ LOGS_TEMPLATE = """ {{ name }} - Loglar ← panele dön

{{ name }} — son {{ lines }} satır

{{ logs }}
""" @app.route("/") def index(): return render_template_string(PROJECTS_TEMPLATE, projects=list_projects(), s=server_stats(), page="projects") @app.route("/containers") def containers_ui(): return render_template_string(CONTAINERS_TEMPLATE, containers=list_all_containers(), page="containers") @app.route("/system") def system_ui(): return render_template_string( SYSTEM_TEMPLATE, df=docker_system_df(), images=list_images(), volumes=list_volumes(), page="system" ) @app.route("/logs/") def logs_ui(name): result = subprocess.run( ["docker", "logs", "--tail", "150", name], capture_output=True, text=True, ) output = (result.stdout or "") + (result.stderr or "") return render_template_string(LOGS_TEMPLATE, name=name, lines=150, logs=output or "(log yok)") @app.route("/start/", methods=["POST"]) def start_ui(name): subprocess.run(["docker", "start", name]) return redirect(request.referrer or "/") @app.route("/stop/", methods=["POST"]) def stop_ui(name): subprocess.run(["docker", "stop", name]) return redirect(request.referrer or "/") @app.route("/restart/", methods=["POST"]) def restart_ui(name): subprocess.run(["docker", "restart", name]) return redirect(request.referrer or "/") @app.route("/delete/", methods=["POST"]) def delete_ui(name): hostname = find_container_hostname(name) subprocess.run(["docker", "rm", "-f", name]) delete_dns_record(hostname) return redirect(request.referrer or "/") @app.route("/images/delete/", methods=["POST"]) def delete_image(image_id): subprocess.run(["docker", "rmi", image_id]) return redirect("/system") @app.route("/images/prune", methods=["POST"]) def prune_images(): subprocess.run(["docker", "image", "prune", "-af"]) return redirect("/system") @app.route("/volumes/delete/", methods=["POST"]) def delete_volume(name): subprocess.run(["docker", "volume", "rm", name]) return redirect("/system") @app.route("/volumes/prune", methods=["POST"]) def prune_volumes(): subprocess.run(["docker", "volume", "prune", "-f"]) return redirect("/system") @app.route("/api/ensure-dns", methods=["POST"]) def api_ensure_dns(): if not check_auth(): return jsonify({"error": "unauthorized"}), 401 body = request.get_json(force=True) hostname = body.get("hostname") if not hostname: return jsonify({"error": "hostname required"}), 400 result = ensure_dns(hostname) return jsonify(result), 200 @app.route("/api/deploy", methods=["POST"]) def api_deploy(): if not check_auth(): return jsonify({"error": "unauthorized"}), 401 body = request.get_json(force=True) name = body.get("name") hostname = body.get("hostname") port = str(body.get("port", "80")) if not name or not hostname: return jsonify({"error": "name and hostname required"}), 400 current_hostname = find_container_hostname(name) if current_hostname is not None and current_hostname != hostname: return jsonify({ "error": "name_conflict", "message": f"'{name}' is already deployed with a different hostname ({current_hostname}).", }), 409 ensure_dns(hostname) subprocess.run(["docker", "rm", "-f", name]) rule = f"Host(`{hostname}`)" subprocess.run([ "docker", "run", "-d", "--name", name, "--network", "proxy", "--restart", "always", "-l", "traefik.enable=true", "-l", f"traefik.http.routers.{name}.rule={rule}", "-l", f"traefik.http.routers.{name}.entrypoints=websecure", "-l", f"traefik.http.routers.{name}.tls.certresolver=letsencrypt", "-l", f"traefik.http.services.{name}.loadbalancer.server.port={port}", f"{name}:latest", ], check=True) return jsonify({"status": "deployed", "url": f"https://{hostname}"}), 200 @app.route("/api/deploy/", methods=["DELETE"]) def api_delete(name): if not check_auth(): return jsonify({"error": "unauthorized"}), 401 hostname = find_container_hostname(name) subprocess.run(["docker", "rm", "-f", name]) delete_dns_record(hostname) return jsonify({"status": "deleted"}), 200 if __name__ == "__main__": app.run(host="0.0.0.0", port=5000)