mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 09:31:20 -04:00
docker healthcheck api thingy
This commit is contained in:
@@ -176,6 +176,11 @@ USER multirover
|
|||||||
# `/data` on an anonymous volume rather than the replaceable image layer.
|
# `/data` on an anonymous volume rather than the replaceable image layer.
|
||||||
VOLUME ["/data"]
|
VOLUME ["/data"]
|
||||||
EXPOSE 8080/tcp 8554/tcp 8189/tcp 8189/udp
|
EXPOSE 8080/tcp 8554/tcp 8189/tcp 8189/udp
|
||||||
|
# Use Node's built-in fetch so container readiness does not require curl or a
|
||||||
|
# second probe binary in the runtime image. The endpoint verifies the writable
|
||||||
|
# data mount and MediaMTX; reaching it already proves Node is accepting HTTP.
|
||||||
|
HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \
|
||||||
|
CMD node -e "fetch('http://127.0.0.1:'+(process.env.PORT||8080)+'/health').then(response=>process.exit(response.ok?0:1)).catch(()=>process.exit(1))"
|
||||||
ENTRYPOINT ["/usr/bin/tini", "--"]
|
ENTRYPOINT ["/usr/bin/tini", "--"]
|
||||||
CMD ["node", "index.js"]
|
CMD ["node", "index.js"]
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ This document is the live implementation tracker for the migration.
|
|||||||
- [x] Phase 1, step 9: Complete the remaining legacy-deployment integration and hardware verification
|
- [x] Phase 1, step 9: Complete the remaining legacy-deployment integration and hardware verification
|
||||||
- [x] Phase 2, step 10: Build and locally verify the production application image
|
- [x] Phase 2, step 10: Build and locally verify the production application image
|
||||||
- [x] Phase 2, steps 11-12: Add the single-container Compose deployment and locally verify its host-access contract
|
- [x] Phase 2, steps 11-12: Add the single-container Compose deployment and locally verify its host-access contract
|
||||||
|
- [x] Phase 2, step 13: Add application and container health checks
|
||||||
- [x] Phase 2, step 15: Build pull requests and publish the main branch to the single GHCR `latest` channel
|
- [x] Phase 2, step 15: Build pull requests and publish the main branch to the single GHCR `latest` channel
|
||||||
- [ ] Phase 2: Containerization, GHCR publishing, and container lifecycle controls
|
- [ ] Phase 2: Containerization, GHCR publishing, and container lifecycle controls
|
||||||
|
|
||||||
@@ -645,6 +646,14 @@ Optional remote integrations should report degraded status to administrators wit
|
|||||||
|
|
||||||
Compose should use the health endpoint and a restart policy suitable for unattended operation.
|
Compose should use the health endpoint and a restart policy suitable for unattended operation.
|
||||||
|
|
||||||
|
### Health-check implementation notes
|
||||||
|
|
||||||
|
Implemented on 2026-09-15:
|
||||||
|
|
||||||
|
- Added an unauthenticated `GET /health` readiness endpoint that exposes only two non-sensitive booleans: whether the application user can read and write the configured data directory and whether MediaMTX answers through its loopback-only metrics listener.
|
||||||
|
- Treated successful route execution as proof that Node is accepting HTTP and that configuration initialization completed. This avoids repeatedly querying every SQLite database or turning optional integrations and currently offline media sources into container restart conditions.
|
||||||
|
- Added the image-level Docker health check using Node's built-in `fetch`, so Compose receives the readiness state without installing another command-line probe utility.
|
||||||
|
|
||||||
## 14. Restricted lifecycle container
|
## 14. Restricted lifecycle container
|
||||||
|
|
||||||
The main web application must not mount the Docker socket. Docker socket access is effectively host-root access.
|
The main web application must not mount the Docker socket. Docker socket access is effectively host-root access.
|
||||||
|
|||||||
@@ -16,6 +16,20 @@ app.use(morgan('dev'));
|
|||||||
*/
|
*/
|
||||||
app.use(PUBLIC_MEDIA_PREFIX, createMediaMtxProxy({ logger }));
|
app.use(PUBLIC_MEDIA_PREFIX, createMediaMtxProxy({ logger }));
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
|
/*
|
||||||
|
Docker and the later lifecycle controller need one stable readiness result,
|
||||||
|
but they do not need administrator credentials or application details. Load
|
||||||
|
the health service only when the route is called so the global HTTP module
|
||||||
|
remains safe to initialize before the service graph during bootstrap.
|
||||||
|
*/
|
||||||
|
app.get('/health', async (_req, res) => {
|
||||||
|
const { getContainerHealth } = require('../services/healthService');
|
||||||
|
const health = await getContainerHealth();
|
||||||
|
res.status(health.healthy ? 200 : 503).json({
|
||||||
|
status: health.healthy ? 'healthy' : 'unhealthy',
|
||||||
|
checks: health.checks,
|
||||||
|
});
|
||||||
|
});
|
||||||
app.use(express.static(config.staticDir, { index: false }));
|
app.use(express.static(config.staticDir, { index: false }));
|
||||||
|
|
||||||
const httpServer = http.createServer(app);
|
const httpServer = http.createServer(app);
|
||||||
|
|||||||
@@ -2,8 +2,9 @@
|
|||||||
// Purpose: Defines the health Service module and the helpers/state used by this service unit.
|
// Purpose: Defines the health Service module and the helpers/state used by this service unit.
|
||||||
// Scope: Keeps runtime behavior unchanged while isolating responsibilities into a clear module boundary.
|
// Scope: Keeps runtime behavior unchanged while isolating responsibilities into a clear module boundary.
|
||||||
const fsp = require('fs/promises');
|
const fsp = require('fs/promises');
|
||||||
|
const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const { resolveRoverSnapshotDir } = require('../../helpers/dataPaths');
|
const { resolveDataDir, resolveRoverSnapshotDir } = require('../../helpers/dataPaths');
|
||||||
const roverManager = require('../roverManager');
|
const roverManager = require('../roverManager');
|
||||||
const { getRoomCameras } = require('../roomCameraService');
|
const { getRoomCameras } = require('../roomCameraService');
|
||||||
const { getRoomCameraState } = require('../roomCameraService');
|
const { getRoomCameraState } = require('../roomCameraService');
|
||||||
@@ -13,6 +14,8 @@ const ROVER_SNAPSHOT_DIR = resolveRoverSnapshotDir();
|
|||||||
const HEALTH_INTERVAL_MS = 5000;
|
const HEALTH_INTERVAL_MS = 5000;
|
||||||
const ROOM_CAMERA_STALE_MS = 5000;
|
const ROOM_CAMERA_STALE_MS = 5000;
|
||||||
const ROVER_SNAPSHOT_STALE_MS = 5000;
|
const ROVER_SNAPSHOT_STALE_MS = 5000;
|
||||||
|
const MEDIAMTX_HEALTH_URL = 'http://127.0.0.1:9998/metrics';
|
||||||
|
const MEDIAMTX_HEALTH_TIMEOUT_MS = 1500;
|
||||||
|
|
||||||
let latest = {
|
let latest = {
|
||||||
updatedAt: Date.now(),
|
updatedAt: Date.now(),
|
||||||
@@ -88,6 +91,49 @@ function getHealthSnapshot() {
|
|||||||
return latest;
|
return latest;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getContainerHealth() {
|
||||||
|
let dataDirectory = false;
|
||||||
|
let mediaMtx = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
/*
|
||||||
|
The mounted data root is the container's only persistent storage. Check
|
||||||
|
the directory itself instead of creating a probe file on every request;
|
||||||
|
this proves that the running application user can reach the mount without
|
||||||
|
adding health-check writes to backups or administrative file listings.
|
||||||
|
*/
|
||||||
|
await fsp.access(resolveDataDir(), fs.constants.R_OK | fs.constants.W_OK);
|
||||||
|
dataDirectory = true;
|
||||||
|
} catch {
|
||||||
|
dataDirectory = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
/*
|
||||||
|
MediaMTX already exposes metrics only on loopback, so it is also the
|
||||||
|
smallest reliable readiness probe. Reading the response closes the body
|
||||||
|
before this request completes and avoids accumulating idle connections
|
||||||
|
across Docker's recurring health checks.
|
||||||
|
*/
|
||||||
|
const response = await fetch(MEDIAMTX_HEALTH_URL, {
|
||||||
|
signal: AbortSignal.timeout(MEDIAMTX_HEALTH_TIMEOUT_MS),
|
||||||
|
});
|
||||||
|
await response.text();
|
||||||
|
mediaMtx = response.ok;
|
||||||
|
} catch {
|
||||||
|
mediaMtx = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
healthy: dataDirectory && mediaMtx,
|
||||||
|
checks: {
|
||||||
|
dataDirectory,
|
||||||
|
mediaMtx,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
getContainerHealth,
|
||||||
getHealthSnapshot,
|
getHealthSnapshot,
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user