├── requirements.txt ├── .gitattributes ├── data └── links.json ├── docker-compose.yml ├── app.py └── README.md /requirements.txt: -------------------------------------------------------------------------------- 1 | streamlit 2 | watchdog 3 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /data/links.json: -------------------------------------------------------------------------------- 1 | { 2 | "Grafana": "http://your-hostname.ts.net:3000", 3 | "Portainer": "http://your-hostname.ts.net:9000", 4 | "Ollama API": "100.x.x.x:11434" 5 | } 6 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.9' 2 | 3 | services: 4 | dashboard: 5 | image: python:3.11 6 | command: bash -c "pip install -r /app/requirements.txt && streamlit run /app/app.py --server.port=8501 --server.enableCORS=false --server.headless=true" 7 | volumes: 8 | - ./app.py:/app/app.py 9 | - ./requirements.txt:/app/requirements.txt 10 | - ./data:/app/data 11 | ports: 12 | - "8501:8501" 13 | working_dir: /app 14 | environment: 15 | - PYTHONUNBUFFERED=1 16 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | import streamlit as st 2 | import json 3 | from pathlib import Path 4 | 5 | # Constants 6 | LINKS_FILE = Path("data/links.json") 7 | USERNAME = "admin" 8 | PASSWORD = "password" 9 | 10 | # Set wide layout for flexibility 11 | st.set_page_config(page_title="Dashboard", layout="wide") 12 | 13 | # Authentication (Simple) 14 | if "logged_in" not in st.session_state: 15 | st.session_state.logged_in = False 16 | 17 | if not st.session_state.logged_in: 18 | center_col = st.columns([1, 2, 1])[1] 19 | with center_col: 20 | st.title("Login") 21 | username = st.text_input("Username") 22 | password = st.text_input("Password", type="password") 23 | if st.button("Login"): 24 | if username == USERNAME and password == PASSWORD: 25 | st.session_state.logged_in = True 26 | else: 27 | st.error("Incorrect username or password.") 28 | st.stop() 29 | 30 | # Load links from JSON 31 | def load_links(): 32 | if LINKS_FILE.exists(): 33 | with open(LINKS_FILE) as f: 34 | return json.load(f) 35 | return {} 36 | 37 | # Responsive column layout: adjusts based on screen size 38 | def display_links_responsive(links_dict, max_cols=4): 39 | items = list(links_dict.items()) 40 | n = len(items) 41 | if n == 0: 42 | st.info("No links found. Add entries to data/links.json.") 43 | return 44 | 45 | for i in range(0, n, max_cols): 46 | cols = st.columns(min(max_cols, n - i)) 47 | for col, (name, url) in zip(cols, items[i:i + max_cols]): 48 | with col: 49 | st.link_button(name, url) 50 | 51 | # Main centered layout 52 | center_col = st.columns([1, 5, 1])[1] 53 | with center_col: 54 | st.title("📟 Container Dashboard") 55 | st.caption("Click a button to open the respective container service.") 56 | if st.button("🔄 Refresh Links"): 57 | st.rerun() 58 | 59 | display_links_responsive(load_links(), max_cols=4) 60 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🧭 Container Dashboard 2 | 3 | A minimal, dark-themed **Streamlit dashboard** for easily accessing services running on your local Docker containers — no more remembering random ports. Designed to be accessed securely using **Tailscale**, with auto-refreshing links and a simple login screen. 4 | 5 | > 💡 **Note:** You don't need to use Tailscale for this to work. The dashboard simply uses the URLs from your `links.json` file. You can use `localhost`, your LAN IP, a domain name, or a reverse proxy like NGINX — anything that lets your device reach the services. 6 | 7 | --- 8 | 9 | ## 📦 Features 10 | 11 | - ✅ Clean Streamlit interface in **dark mode** 12 | - ✅ Display container service links as clickable buttons 13 | - ✅ JSON-driven, hot-reloadable link list 14 | - ✅ Login screen with hardcoded credentials 15 | - ✅ Responsive layout (mobile friendly) 16 | - ✅ Access from anywhere via **Tailscale** 17 | - ✅ Fully containerized with `docker-compose` 18 | 19 | --- 20 | 21 | ## 📁 Project Structure 22 | 23 | ``` 24 | dashboard/ 25 | ├── data/ 26 | │ └── links.json # Store your link buttons here (editable JSON) 27 | ├── app.py # Streamlit app 28 | ├── requirements.txt # Python dependencies 29 | └── docker-compose.yml # Docker config (no Dockerfile needed) 30 | ``` 31 | 32 | --- 33 | 34 | ## 🚀 Quick Start 35 | 36 | ### 1. Clone the repo 37 | 38 | ```bash 39 | git clone https://github.com/yourusername/container-dashboard.git 40 | cd container-dashboard 41 | ``` 42 | 43 | ### 2. Edit `data/links.json` 44 | 45 | Example: 46 | 47 | ```json 48 | { 49 | "Grafana": "http://localhost:3000", 50 | "Portainer": "http://localhost:9000", 51 | "Ollama API": "http://localhost:11434" 52 | } 53 | ``` 54 | 55 | ### 3. Start the dashboard 56 | 57 | ```bash 58 | docker-compose up -d 59 | ``` 60 | 61 | This runs the dashboard at: 62 | 63 | ``` 64 | http://localhost:8501 65 | ``` 66 | 67 | > ✅ Make sure port `8501` is open if accessing remotely or via Tailscale. 68 | 69 | --- 70 | 71 | ## 🌐 Access Remotely with Tailscale 72 | 73 | This dashboard is designed to be accessed from other devices via [Tailscale](https://tailscale.com/). 74 | 75 | ### Option 1: Basic Access (Tailscale IP) 76 | 77 | On your mobile or another device in the tailnet: 78 | 79 | ``` 80 | http://100.x.x.x:8501 81 | ``` 82 | 83 | ### Option 2: Friendly `.ts.net` Hostname (with MagicDNS) 84 | 85 | Ensure: 86 | - MagicDNS is enabled in your Tailscale admin panel 87 | - DNS override is enabled in your mobile's Tailscale app 88 | 89 | Then access: 90 | 91 | ``` 92 | http://your-hostname.ts.net:8501 93 | ``` 94 | 95 | ### Option 3: Clean HTTPS with `tailscale serve` 96 | 97 | Run this on the host: 98 | 99 | ```bash 100 | tailscale serve --https /dashboard=http://localhost:8501 101 | ``` 102 | 103 | Now access via: 104 | 105 | ``` 106 | https://your-hostname.ts.net/dashboard 107 | ``` 108 | 109 | No ports, no DNS hassle, and fully HTTPS. 110 | 111 | --- 112 | 113 | ## 🔐 Login Credentials 114 | 115 | Hardcoded in `app.py` (you can change these): 116 | 117 | ```python 118 | USERNAME = "admin" 119 | PASSWORD = "password" 120 | ``` 121 | 122 | --- 123 | 124 | ## 🛠 Requirements (if running locally) 125 | 126 | If you prefer not to use Docker: 127 | 128 | ```bash 129 | pip install -r requirements.txt 130 | streamlit run app.py 131 | ``` 132 | 133 | --- 134 | 135 | ## 📄 License 136 | 137 | MIT — free to use and modify. 138 | 139 | --- 140 | 141 | ## ✨ Credits 142 | 143 | Made with ❤️ using [Streamlit](https://streamlit.io), [Docker](https://docker.com), and [Tailscale](https://tailscale.com). 144 | --------------------------------------------------------------------------------