mirror of
https://github.com/dqzboy/Docker-Proxy.git
synced 2026-01-12 16:25:42 +08:00
feat: Add container management page operations to implement container restart, stop, and other operations.
This commit is contained in:
@@ -149,6 +149,7 @@ docker logs -f [容器ID或名称]
|
||||
- [x] 支持国内服务器一键部署,解决国内环境无法安装Docker\Compose服务难题
|
||||
- [x] 支持主流Linux发行版操作系统,例如Centos、Ubuntu、Rocky、Debian、Rhel等
|
||||
- [x] 支持主流ARCH架构下部署,包括linux/amd64、linux/arm64
|
||||
- [x] 针对本项目单独开发等Docker Registry管理面板,实现镜像搜索、广告展示、文档教程、容器管理等
|
||||
|
||||
## ✨ 教程
|
||||
#### 配置Nginx反向代理
|
||||
@@ -227,6 +228,14 @@ docker pull gcr.your_domain_name/google-containers/pause:3.1
|
||||
<td width="50%" align="center"><img src="https://github.com/dqzboy/Docker-Proxy/assets/42825450/0ddb041b-64f6-4d93-b5bf-85ad3b53d0e0?raw=true"></td>
|
||||
<td width="50%" align="center"><img src="https://github.com/user-attachments/assets/c7e368ca-7f1a-4311-9a10-a5f4f06d86d8?raw=true"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="50%" align="center"><b>Docker官方镜像搜索</b></td>
|
||||
<td width="50%" align="center"><b>Docker容器服务管理</b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="50%" align="center"><img src="https://github.com/user-attachments/assets/8569c5c4-4ce6-4cd4-8547-fa9816019049?raw=true"></td>
|
||||
<td width="50%" align="center"><img src="https://github.com/user-attachments/assets/c90976d2-ed81-4ed6-aff0-e8642bb6c033?raw=true"></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
---
|
||||
|
||||
@@ -127,6 +127,12 @@ docker logs -f [容器ID或名称]
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td width="50%" align="center"><img src="https://github.com/user-attachments/assets/c90976d2-ed81-4ed6-aff0-e8642bb6c033"?raw=true"></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
---
|
||||
|
||||
## 🫶 赞助
|
||||
|
||||
@@ -4,5 +4,7 @@ services:
|
||||
container_name: hubcmd-ui
|
||||
image: dqzboy/hubcmd-ui:latest
|
||||
restart: always
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
ports:
|
||||
- 30080:3000
|
||||
@@ -2,6 +2,8 @@
|
||||
"dependencies": {
|
||||
"axios": "^1.7.5",
|
||||
"bcrypt": "^5.1.1",
|
||||
"cors": "^2.8.5",
|
||||
"dockerode": "^4.0.2",
|
||||
"express": "^4.19.2",
|
||||
"express-session": "^1.18.0",
|
||||
"morgan": "^1.10.0"
|
||||
|
||||
@@ -7,8 +7,27 @@ const bcrypt = require('bcrypt');
|
||||
const crypto = require('crypto');
|
||||
const logger = require('morgan'); // 引入 morgan 作为日志工具
|
||||
const axios = require('axios'); // 用于发送 HTTP 请求
|
||||
|
||||
const Docker = require('dockerode');
|
||||
const app = express();
|
||||
const cors = require('cors');
|
||||
|
||||
let docker = null;
|
||||
|
||||
async function initDocker() {
|
||||
if (docker === null) {
|
||||
docker = new Docker();
|
||||
try {
|
||||
await docker.ping();
|
||||
console.log('成功连接到 Docker 守护进程');
|
||||
} catch (err) {
|
||||
console.error('无法连接到 Docker 守护进程:', err);
|
||||
docker = null;
|
||||
}
|
||||
}
|
||||
return docker;
|
||||
}
|
||||
|
||||
app.use(cors());
|
||||
app.use(express.json());
|
||||
app.use(express.static('web'));
|
||||
app.use(bodyParser.urlencoded({ extended: true }));
|
||||
@@ -124,7 +143,6 @@ async function readDocumentation() {
|
||||
try {
|
||||
await ensureDocumentationDir();
|
||||
const files = await fs.readdir(DOCUMENTATION_DIR);
|
||||
console.log('Files in documentation directory:', files); // 添加日志
|
||||
|
||||
const documents = await Promise.all(files.map(async file => {
|
||||
const filePath = path.join(DOCUMENTATION_DIR, file);
|
||||
@@ -139,7 +157,6 @@ async function readDocumentation() {
|
||||
}));
|
||||
|
||||
const publishedDocuments = documents.filter(doc => doc.published);
|
||||
console.log('Published documents:', publishedDocuments); // 添加日志
|
||||
return publishedDocuments;
|
||||
} catch (error) {
|
||||
console.error('Error reading documentation:', error);
|
||||
@@ -205,9 +222,11 @@ app.post('/api/change-password', async (req, res) => {
|
||||
|
||||
// 需要登录验证的中间件
|
||||
function requireLogin(req, res, next) {
|
||||
console.log('Session:', req.session); // 添加这行
|
||||
if (req.session.user) {
|
||||
next();
|
||||
} else {
|
||||
console.log('用户未登录'); // 添加这行
|
||||
res.status(401).json({ error: 'Not logged in' });
|
||||
}
|
||||
}
|
||||
@@ -310,7 +329,6 @@ app.post('/api/documentation/:id/toggle-publish', requireLogin, async (req, res)
|
||||
app.get('/api/documentation', async (req, res) => {
|
||||
try {
|
||||
const documents = await readDocumentation();
|
||||
console.log('Sending documents:', documents); // 添加日志
|
||||
res.json(documents);
|
||||
} catch (error) {
|
||||
console.error('Error in /api/documentation:', error);
|
||||
@@ -379,11 +397,9 @@ app.get('/api/documentation-list', async (req, res) => {
|
||||
app.get('/api/documentation/:id', async (req, res) => {
|
||||
try {
|
||||
const docId = req.params.id;
|
||||
console.log('Fetching document with id:', docId); // 添加日志
|
||||
const docPath = path.join(DOCUMENTATION_DIR, `${docId}.json`);
|
||||
const content = await fs.readFile(docPath, 'utf8');
|
||||
const doc = JSON.parse(content);
|
||||
console.log('Sending document:', doc); // 添加日志
|
||||
res.json(doc);
|
||||
} catch (error) {
|
||||
console.error('Error reading document:', error);
|
||||
@@ -391,6 +407,95 @@ app.get('/api/documentation/:id', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
// API端点来获取Docker容器状态
|
||||
app.get('/api/docker-status', requireLogin, async (req, res) => {
|
||||
try {
|
||||
const docker = await initDocker();
|
||||
if (!docker) {
|
||||
return res.status(503).json({ error: '无法连接到 Docker 守护进程' });
|
||||
}
|
||||
const containers = await docker.listContainers({ all: true });
|
||||
const containerStatus = await Promise.all(containers.map(async (container) => {
|
||||
const containerInfo = await docker.getContainer(container.Id).inspect();
|
||||
const stats = await docker.getContainer(container.Id).stats({ stream: false });
|
||||
|
||||
// 计算 CPU 使用率
|
||||
const cpuDelta = stats.cpu_stats.cpu_usage.total_usage - stats.precpu_stats.cpu_usage.total_usage;
|
||||
const systemDelta = stats.cpu_stats.system_cpu_usage - stats.precpu_stats.system_cpu_usage;
|
||||
const cpuUsage = (cpuDelta / systemDelta) * stats.cpu_stats.online_cpus * 100;
|
||||
|
||||
// 计算内存使用率
|
||||
const memoryUsage = stats.memory_stats.usage / stats.memory_stats.limit * 100;
|
||||
|
||||
return {
|
||||
id: container.Id.slice(0, 12),
|
||||
name: container.Names[0].replace(/^\//, ''),
|
||||
image: container.Image,
|
||||
state: containerInfo.State.Status,
|
||||
status: container.Status,
|
||||
cpu: cpuUsage.toFixed(2) + '%',
|
||||
memory: memoryUsage.toFixed(2) + '%',
|
||||
created: new Date(container.Created * 1000).toLocaleString()
|
||||
};
|
||||
}));
|
||||
res.json(containerStatus);
|
||||
} catch (error) {
|
||||
console.error('获取 Docker 状态时出错:', error);
|
||||
res.status(500).json({ error: '获取 Docker 状态失败', details: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// API端点:重启容器
|
||||
app.post('/api/docker/restart/:id', requireLogin, async (req, res) => {
|
||||
try {
|
||||
const docker = await initDocker();
|
||||
if (!docker) {
|
||||
return res.status(503).json({ error: '无法连接到 Docker 守护进程' });
|
||||
}
|
||||
const container = docker.getContainer(req.params.id);
|
||||
await container.restart();
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('重启容器失败:', error);
|
||||
res.status(500).json({ error: '重启容器失败', details: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// API端点:停止容器
|
||||
app.post('/api/docker/stop/:id', requireLogin, async (req, res) => {
|
||||
try {
|
||||
const docker = await initDocker();
|
||||
if (!docker) {
|
||||
return res.status(503).json({ error: '无法连接到 Docker 守护进程' });
|
||||
}
|
||||
const container = docker.getContainer(req.params.id);
|
||||
await container.stop();
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('停止容器失败:', error);
|
||||
res.status(500).json({ error: '停止容器失败', details: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// API端点:获取单个容器的状态
|
||||
app.get('/api/docker/status/:id', requireLogin, async (req, res) => {
|
||||
try {
|
||||
const docker = await initDocker();
|
||||
if (!docker) {
|
||||
return res.status(503).json({ error: '无法连接到 Docker 守护进程' });
|
||||
}
|
||||
const container = docker.getContainer(req.params.id);
|
||||
const containerInfo = await container.inspect();
|
||||
res.json({ state: containerInfo.State.Status });
|
||||
} catch (error) {
|
||||
console.error('获取容器状态失败:', error);
|
||||
res.status(500).json({ error: '获取容器状态失败', details: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 启动服务器
|
||||
const PORT = process.env.PORT || 3000;
|
||||
app.listen(PORT, () => {
|
||||
|
||||
@@ -86,6 +86,7 @@
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
.action-btn:hover {
|
||||
background-color: #0256b9;
|
||||
}
|
||||
@@ -400,6 +401,21 @@
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.loading {
|
||||
display: inline-block;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 3px solid rgba(0, 0, 0, 0.1);
|
||||
border-radius: 50%;
|
||||
border-top: 3px solid #0366d6;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -415,6 +431,7 @@
|
||||
<li data-section="ad-management">广告管理</li>
|
||||
<li data-section="documentation-management">文档管理</li>
|
||||
<li data-section="password-change">修改密码</li>
|
||||
<li data-section="docker-status">Docker 服务状态</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="content-area">
|
||||
@@ -490,6 +507,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 修改密码部分 -->
|
||||
<div id="password-change" class="content-section">
|
||||
<h2 class="menu-label">修改密码</h2>
|
||||
<label for="currentPassword">当前密码</label>
|
||||
@@ -500,6 +518,28 @@
|
||||
<span id="passwordStrength" style="color: red;"></span>
|
||||
<button type="button" onclick="changePassword()">修改密码</button>
|
||||
</div>
|
||||
|
||||
<!-- Docker服务状态 -->
|
||||
<div id="docker-status" class="content-section">
|
||||
<h1 class="admin-title">Docker 服务状态</h1>
|
||||
<table id="dockerStatusTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>容器 ID</th>
|
||||
<th>名称</th>
|
||||
<th>镜像</th>
|
||||
<th>状态</th>
|
||||
<th>CPU</th>
|
||||
<th>内存</th>
|
||||
<th>创建时间</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="dockerStatusTableBody">
|
||||
<!-- Docker 容器状态将在这里动态添加 -->
|
||||
</tbody>
|
||||
</table>
|
||||
<button type="button" onclick="refreshDockerStatus()">刷新状态</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1278,12 +1318,132 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function loadDockerStatus() {
|
||||
try {
|
||||
const response = await fetch('/api/docker-status');
|
||||
if (!response.ok) {
|
||||
if (response.status === 503) {
|
||||
throw new Error('无法连接到 Docker 守护进程');
|
||||
}
|
||||
throw new Error('Failed to fetch Docker status');
|
||||
}
|
||||
const containerStatus = await response.json();
|
||||
renderDockerStatus(containerStatus);
|
||||
} catch (error) {
|
||||
console.error('Error loading Docker status:', error);
|
||||
alert('加载 Docker 状态失败: ' + error.message);
|
||||
// 清空状态表格并显示错误信息
|
||||
const tbody = document.getElementById('dockerStatusTableBody');
|
||||
tbody.innerHTML = `<tr><td colspan="8" style="text-align: center; color: red;">${error.message}</td></tr>`;
|
||||
}
|
||||
}
|
||||
|
||||
function renderDockerStatus(containerStatus) {
|
||||
const tbody = document.getElementById('dockerStatusTableBody');
|
||||
tbody.innerHTML = '';
|
||||
|
||||
// 添加表头
|
||||
const thead = document.getElementById('dockerStatusTable').getElementsByTagName('thead')[0];
|
||||
thead.innerHTML = `
|
||||
<tr>
|
||||
<th>容器 ID</th>
|
||||
<th>名称</th>
|
||||
<th>镜像</th>
|
||||
<th>状态</th>
|
||||
<th>CPU</th>
|
||||
<th>内存</th>
|
||||
<th>创建时间</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
`;
|
||||
|
||||
containerStatus.forEach(container => {
|
||||
const row = `
|
||||
<tr>
|
||||
<td>${container.id}</td>
|
||||
<td>${container.name}</td>
|
||||
<td>${container.image}</td>
|
||||
<td id="status-${container.id}">${container.state}</td>
|
||||
<td>${container.cpu}</td>
|
||||
<td>${container.memory}</td>
|
||||
<td>${container.created}</td>
|
||||
<td>
|
||||
<button onclick="restartContainer('${container.id}')" class="action-btn">重启</button>
|
||||
<button onclick="stopContainer('${container.id}')" class="action-btn">停止</button>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
tbody.innerHTML += row;
|
||||
});
|
||||
}
|
||||
|
||||
async function restartContainer(id) {
|
||||
if (confirm('确定要重启这个容器吗?')) {
|
||||
try {
|
||||
const statusCell = document.getElementById(`status-${id}`);
|
||||
statusCell.innerHTML = '<div class="loading"></div>';
|
||||
|
||||
const response = await fetch(`/api/docker/restart/${id}`, { method: 'POST' });
|
||||
if (response.ok) {
|
||||
await new Promise(resolve => setTimeout(resolve, 2000)); // 等待2秒,确保状态已更新
|
||||
const newStatus = await getContainerStatus(id);
|
||||
statusCell.textContent = newStatus;
|
||||
} else {
|
||||
throw new Error('重启失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('重启容器失败:', error);
|
||||
alert('重启容器失败: ' + error.message);
|
||||
loadDockerStatus(); // 重新加载所有容器状态
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function stopContainer(id) {
|
||||
if (confirm('确定要停止这个容器吗?')) {
|
||||
try {
|
||||
const statusCell = document.getElementById(`status-${id}`);
|
||||
statusCell.innerHTML = '<div class="loading"></div>';
|
||||
|
||||
const response = await fetch(`/api/docker/stop/${id}`, { method: 'POST' });
|
||||
if (response.ok) {
|
||||
await new Promise(resolve => setTimeout(resolve, 2000)); // 等待2秒,确保状态已更新
|
||||
const newStatus = await getContainerStatus(id);
|
||||
statusCell.textContent = newStatus;
|
||||
} else {
|
||||
throw new Error('停止失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('停止容器失败:', error);
|
||||
alert('停止容器失败: ' + error.message);
|
||||
loadDockerStatus(); // 重新加载所有容器状态
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function getContainerStatus(id) {
|
||||
const response = await fetch(`/api/docker/status/${id}`);
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
return data.state;
|
||||
} else {
|
||||
throw new Error('获取容器状态失败');
|
||||
}
|
||||
}
|
||||
|
||||
function refreshDockerStatus() {
|
||||
loadDockerStatus();
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const sidebarItems = document.querySelectorAll('.sidebar li');
|
||||
const contentSections = document.querySelectorAll('.content-section');
|
||||
if (isLoggedIn) {
|
||||
initEditor();
|
||||
}
|
||||
if (isLoggedIn) {
|
||||
loadDockerStatus();
|
||||
}
|
||||
function showSection(sectionId) {
|
||||
contentSections.forEach(section => {
|
||||
if (section.id === sectionId) {
|
||||
@@ -1310,4 +1470,4 @@
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user