docker yoneticisi: tum containerlar, imajlar, volumeler, disk kullanimi
Deploy / deploy (push) Successful in 18s

This commit is contained in:
2026-09-23 16:38:07 +03:00
parent f6256445ff
commit e948f10bd5
+320 -27
View File
@@ -67,14 +67,12 @@ def _cpu_times():
def server_stats():
stats = {}
# CPU: kisa bir ornekleme araligiyla kullanim yuzdesi
idle1, total1 = _cpu_times()
time.sleep(0.3)
idle2, total2 = _cpu_times()
d_idle = idle2 - idle1
d_total = total2 - total1
cpu_percent = round((1 - d_idle / d_total) * 100, 1) if d_total > 0 else 0.0
stats["cpu_percent"] = cpu_percent
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:
@@ -84,7 +82,7 @@ def server_stats():
with open("/proc/meminfo") as f:
for line in f:
key, _, rest = line.partition(":")
meminfo[key] = int(rest.strip().split()[0]) # kB
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
@@ -103,12 +101,12 @@ def server_stats():
hours, _ = divmod(rem, 3600)
stats["uptime"] = f"{days} gün {hours} saat" if days else f"{hours} saat"
all_containers = subprocess.run(
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_containers if s == "running")
stats["containers_total"] = len(all_containers)
stats["containers_running"] = sum(1 for s in all_states if s == "running")
stats["containers_total"] = len(all_states)
return stats
@@ -119,6 +117,17 @@ def inspect_all(cid):
).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}}"],
@@ -146,6 +155,88 @@ def list_projects():
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 == "<none>" and tag == "<none>":
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:
@@ -190,17 +281,11 @@ def delete_dns_record(hostname):
)
TEMPLATE = """
<!doctype html>
<html>
<head>
<title>Deploy Panel</title>
<meta charset="utf-8">
<meta http-equiv="refresh" content="20">
<style>
BASE_STYLE = """
body { font-family: system-ui, sans-serif; background: #0f172a; color: #e2e8f0; padding: 2.5rem; }
h1 { color: #38bdf8; margin-bottom: 0.2rem; }
.sub { opacity: 0.55; font-size: 0.85rem; margin-bottom: 1.5rem; }
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; }
@@ -210,17 +295,22 @@ TEMPLATE = """
.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; }
.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 { background: #1e3a8a; }
.btn-restart:hover { background: #1e40af; }
.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; }
@@ -231,10 +321,32 @@ TEMPLATE = """
.bar-fill { height: 100%; background: #38bdf8; }
.bar-fill.warn { background: #f59e0b; }
.bar-fill.crit { background: #ef4444; }
</style>
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 = """
<nav>
<a href="/" class="{{ 'active' if page == 'projects' else '' }}">Projeler</a>
<a href="/containers" class="{{ 'active' if page == 'containers' else '' }}">Tüm Container'lar</a>
<a href="/system" class="{{ 'active' if page == 'system' else '' }}">Sistem &amp; Disk</a>
</nav>
"""
PROJECTS_TEMPLATE = """
<!doctype html>
<html>
<head>
<title>Deploy Panel</title>
<meta charset="utf-8">
<meta http-equiv="refresh" content="20">
<style>""" + BASE_STYLE + """</style>
</head>
<body>
<h1>Deploy Panel</h1>
""" + NAV + """
<div class="sub">{{ projects|length }} proje · 20 saniyede bir otomatik yenilenir · <a href="/">şimdi yenile</a></div>
<div class="stats">
@@ -302,6 +414,141 @@ TEMPLATE = """
</html>
"""
CONTAINERS_TEMPLATE = """
<!doctype html>
<html>
<head>
<title>Container'lar - Deploy Panel</title>
<meta charset="utf-8">
<meta http-equiv="refresh" content="20">
<style>""" + BASE_STYLE + """</style>
</head>
<body>
<h1>Deploy Panel</h1>
""" + NAV + """
<div class="sub">Sunucudaki TÜM container'lar (sadece Traefik projeleri değil) · {{ containers|length }} adet</div>
{% if containers %}
<table>
<tr><th>İsim</th><th>Adres</th><th>Portlar</th><th>Durum</th><th>İmaj</th><th></th></tr>
{% for c in containers %}
<tr>
<td>{{ c.name }}</td>
<td>{% if c.hostname %}<a href="https://{{ c.hostname }}" target="_blank">{{ c.hostname }}</a>{% else %}—{% endif %}</td>
<td>{{ c.ports or '' }}</td>
<td>
<span class="badge {{ c.status }}">{{ c.status }}</span>
{% if c.since %}<span class="since">{{ c.since }}</span>{% endif %}
</td>
<td>{{ c.image }}</td>
<td>
<div class="actions">
<a class="btn btn-logs" href="/logs/{{ c.name }}">Loglar</a>
{% if c.status == 'running' %}
<form method="post" action="/stop/{{ c.name }}">
<button class="btn-stop" type="submit">Durdur</button>
</form>
<form method="post" action="/restart/{{ c.name }}">
<button class="btn-restart" type="submit">Yeniden Başlat</button>
</form>
{% else %}
<form method="post" action="/start/{{ c.name }}">
<button class="btn-start" type="submit">Başlat</button>
</form>
{% endif %}
<form method="post" action="/delete/{{ c.name }}"
onsubmit="return confirm('{{ c.name }} silinsin mi?{{ ' DNS kaydı da kaldırılacak.' if c.hostname else '' }}');">
<button class="btn-delete" type="submit">Sil</button>
</form>
</div>
</td>
</tr>
{% endfor %}
</table>
{% else %}
<p class="empty">Hiç container yok.</p>
{% endif %}
</body>
</html>
"""
SYSTEM_TEMPLATE = """
<!doctype html>
<html>
<head>
<title>Sistem &amp; Disk - Deploy Panel</title>
<meta charset="utf-8">
<style>""" + BASE_STYLE + """</style>
</head>
<body>
<h1>Deploy Panel</h1>
""" + NAV + """
<div class="sub">Docker'ın diskte kapladığı alan ve imaj/volume yönetimi</div>
<table>
<tr><th>Tür</th><th>Toplam</th><th>Kullanımda</th><th>Boyut</th><th>Geri kazanılabilir</th></tr>
{% for row in df %}
<tr>
<td>{{ row.type }}</td>
<td>{{ row.total }}</td>
<td>{{ row.active }}</td>
<td>{{ row.size }}</td>
<td>{{ row.reclaimable }}</td>
</tr>
{% endfor %}
</table>
<h2>İmajlar ({{ images|length }})</h2>
<form method="post" action="/images/prune" onsubmit="return confirm('Hiçbir container tarafından kullanılmayan TÜM imajlar silinecek. Emin misin?');">
<button class="btn-prune" type="submit">Kullanılmayan imajları temizle</button>
</form>
{% if images %}
<table>
<tr><th>İmaj</th><th>Boyut</th><th>Oluşturulma</th><th></th></tr>
{% for img in images %}
<tr>
<td><code>{{ img.label }}</code></td>
<td>{{ img.size }}</td>
<td>{{ img.created }}</td>
<td>
<form method="post" action="/images/delete/{{ img.id }}"
onsubmit="return confirm('{{ img.label }} silinsin mi? Kullanan bir container varsa silme başarısız olur.');">
<button class="btn-delete" type="submit">Sil</button>
</form>
</td>
</tr>
{% endfor %}
</table>
{% else %}
<p class="empty">Hiç imaj yok.</p>
{% endif %}
<h2>Volume'ler ({{ volumes|length }})</h2>
<form method="post" action="/volumes/prune" onsubmit="return confirm('Hiçbir container tarafından kullanılmayan TÜM volume\\'ler silinecek. Emin misin?');">
<button class="btn-prune" type="submit">Kullanılmayan volume'leri temizle</button>
</form>
{% if volumes %}
<table>
<tr><th>İsim</th><th>Driver</th><th></th></tr>
{% for v in volumes %}
<tr>
<td><code>{{ v.name }}</code></td>
<td>{{ v.driver }}</td>
<td>
<form method="post" action="/volumes/delete/{{ v.name }}"
onsubmit="return confirm('{{ v.name }} silinsin mi? Kullanan bir container varsa silme başarısız olur.');">
<button class="btn-delete" type="submit">Sil</button>
</form>
</td>
</tr>
{% endfor %}
</table>
{% else %}
<p class="empty">Hiç volume yok.</p>
{% endif %}
</body>
</html>
"""
LOGS_TEMPLATE = """
<!doctype html>
<html>
@@ -327,7 +574,19 @@ LOGS_TEMPLATE = """
@app.route("/")
def index():
return render_template_string(TEMPLATE, projects=list_projects(), s=server_stats())
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/<name>")
@@ -340,20 +599,54 @@ def logs_ui(name):
return render_template_string(LOGS_TEMPLATE, name=name, lines=150, logs=output or "(log yok)")
@app.route("/start/<name>", methods=["POST"])
def start_ui(name):
subprocess.run(["docker", "start", name])
return redirect(request.referrer or "/")
@app.route("/stop/<name>", methods=["POST"])
def stop_ui(name):
subprocess.run(["docker", "stop", name])
return redirect(request.referrer or "/")
@app.route("/restart/<name>", methods=["POST"])
def restart_ui(name):
subprocess.run(["docker", "restart", name])
return redirect("/")
return redirect(request.referrer or "/")
@app.route("/delete/<name>", methods=["POST"])
def delete_ui(name):
projects = {p["name"]: p for p in list_projects()}
target = projects.get(name)
if target:
hostname = find_container_hostname(name)
subprocess.run(["docker", "rm", "-f", name])
delete_dns_record(target["hostname"])
return redirect("/")
delete_dns_record(hostname)
return redirect(request.referrer or "/")
@app.route("/images/delete/<image_id>", 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/<name>", 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"])