import os import json import subprocess from datetime import datetime from flask import Flask, jsonify, render_template app = Flask(__name__) DOKKU = "/usr/bin/dokku" DOCKER = "/usr/bin/docker" POLL_SECONDS = int(os.getenv("POLL_SECONDS", "10")) SHOW_CONTAINERS = os.getenv("SHOW_CONTAINERS", "0") == "1" def sh(cmd: list[str]) -> str: return subprocess.check_output(cmd, stderr=subprocess.STDOUT, text=True).strip() def dokku_apps() -> list[str]: out = sh([DOKKU, "apps:list"]) lines = [l.strip() for l in out.splitlines()] # Format: # =====> My Apps # app1 # app2 return [l for l in lines if l and not l.startswith("====")] def dokku_urls(app_name: str) -> list[str]: try: out = sh([DOKKU, "urls:report", app_name, "--urls"]) return [u.strip() for u in out.split() if u.strip()] except Exception: return [] def dokku_ps_report(app_name: str) -> dict: try: out = sh([DOKKU, "ps:report", app_name]) # crude parse: "Key: value" data = {} for line in out.splitlines(): if ":" in line: k, v = line.split(":", 1) data[k.strip()] = v.strip() return data except Exception as e: return {"error": str(e)} def docker_stats() -> dict: """ Returns a mapping of container_name -> {cpu, mem_used, mem_limit, mem_pct} """ fmt = "{{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.MemPerc}}" out = sh([DOCKER, "stats", "--no-stream", "--format", fmt]) stats = {} for line in out.splitlines(): name, cpu, mem_usage, mem_pct = line.split("\t") # mem_usage like: "58.84MiB / 384MiB" mem_used, mem_limit = [s.strip() for s in mem_usage.split("/", 1)] stats[name] = { "cpu": cpu, "mem_used": mem_used, "mem_limit": mem_limit, "mem_pct": mem_pct, } return stats def docker_ps() -> list[dict]: fmt = "{{.Names}}\t{{.Image}}\t{{.Status}}\t{{.Ports}}" out = sh([DOCKER, "ps", "--format", fmt]) rows = [] for line in out.splitlines(): name, image, status, ports = line.split("\t") rows.append({"name": name, "image": image, "status": status, "ports": ports}) return rows def collect(): apps = dokku_apps() stats = docker_stats() rows = [] for a in apps: urls = dokku_urls(a) ps = dokku_ps_report(a) # Your containers are typically named like ".web.1" container_name = f"{a}.web.1" cstats = stats.get(container_name, None) rows.append({ "app": a, "urls": urls, "running": ps.get("Running", ""), "processes": ps.get("Processes", ""), "status_web_1": ps.get("Status web 1", ""), "cpu": cstats["cpu"] if cstats else "", "mem_used": cstats["mem_used"] if cstats else "", "mem_limit": cstats["mem_limit"] if cstats else "", "mem_pct": cstats["mem_pct"] if cstats else "", }) extra_containers = docker_ps() if SHOW_CONTAINERS else [] return { "generated_at": datetime.utcnow().isoformat() + "Z", "poll_seconds": POLL_SECONDS, "apps": sorted(rows, key=lambda r: r["app"]), "containers": extra_containers, } @app.get("/api/status") def api_status(): return jsonify(collect()) @app.get("/") def index(): return render_template("index.html", poll_seconds=POLL_SECONDS) @app.get("/partial/apps") def partial_apps(): data = collect() return render_template("apps_table.html", data=data)