mirror of
https://github.com/BrenBroZAYT/uptime-kuma-discord-bot.git
synced 2026-06-13 16:40:03 +00:00
refactor: docker implementation + health endpoint
This commit is contained in:
+25
-24
@@ -1,46 +1,47 @@
|
||||
# Build stage
|
||||
FROM node:20-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy package files
|
||||
# Install deps
|
||||
COPY package*.json ./
|
||||
COPY tsconfig.json ./
|
||||
|
||||
# Install dependencies
|
||||
RUN npm ci
|
||||
|
||||
# Copy source code
|
||||
# Build
|
||||
COPY src ./src
|
||||
|
||||
# Build TypeScript
|
||||
RUN npm run build
|
||||
|
||||
# Production stage
|
||||
# Runtime stage
|
||||
FROM node:20-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy package files
|
||||
ENV NODE_ENV=production
|
||||
|
||||
# Copy package files and install production deps
|
||||
COPY package*.json ./
|
||||
RUN npm ci --only=production && npm cache clean --force
|
||||
|
||||
# Install production dependencies only
|
||||
RUN npm ci --only=production && \
|
||||
npm cache clean --force
|
||||
|
||||
# Copy built application
|
||||
# Copy built app (make sure dist exists)
|
||||
COPY --from=builder /app/dist ./dist
|
||||
|
||||
# Create non-root user
|
||||
RUN addgroup -g 1001 -S appuser && \
|
||||
adduser -u 1001 -S appuser -G appuser
|
||||
# Create a data directory now (final perms fixed at runtime)
|
||||
RUN mkdir -p /app/data
|
||||
|
||||
# Change ownership
|
||||
RUN chown -R appuser:appuser /app
|
||||
# Install su-exec for privilege drop and wget for health checks
|
||||
RUN apk add --no-cache su-exec wget
|
||||
|
||||
# Switch to non-root user
|
||||
USER appuser
|
||||
# Add entrypoint
|
||||
COPY docker/entrypoint.sh /entrypoint.sh
|
||||
RUN chmod +x /entrypoint.sh
|
||||
|
||||
# Start the application
|
||||
# Encourage a named volume by default (optional but nice)
|
||||
VOLUME ["/app/data"]
|
||||
|
||||
# Default PUID/PGID can be overridden at runtime
|
||||
ENV PUID=1001 PGID=1001 DATA_DIR=/app/data HEALTH_PORT=3000
|
||||
|
||||
EXPOSE 3000
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
CMD ["node", "dist/index.js"]
|
||||
|
||||
|
||||
@@ -41,6 +41,14 @@ docker-compose up -d
|
||||
# or: npm install && npm run build && npm start
|
||||
```
|
||||
|
||||
### Docker Environment Variables
|
||||
|
||||
| Variable | Description | Default |
|
||||
|----------|-------------|---------|
|
||||
| `PUID`/`PGID` | User/group IDs | `1001` |
|
||||
| `DATA_DIR` | Data directory path | `/app/data` |
|
||||
| `HEALTH_PORT` | Health check port | `3000` |
|
||||
|
||||
## Step 3: Configure in Discord (1 minute)
|
||||
|
||||
Type these commands in Discord:
|
||||
@@ -95,6 +103,8 @@ The bot supports multiple Discord servers independently! Each server has its own
|
||||
- Check `.env` credentials
|
||||
- View logs: `docker-compose logs -f`
|
||||
- Try `/config` in Discord
|
||||
- Check container health: `docker-compose ps`
|
||||
- Test health endpoint: `curl http://localhost:3000/health`
|
||||
|
||||
### Permission denied
|
||||
- Add your Discord user ID to `ADMIN_USER_IDS` in `.env`
|
||||
|
||||
@@ -82,7 +82,10 @@ docker run -d \
|
||||
-e UPTIME_KUMA_URL=your_url \
|
||||
-e UPTIME_KUMA_USERNAME=your_username \
|
||||
-e UPTIME_KUMA_PASSWORD=your_password \
|
||||
-e PUID=1001 \
|
||||
-e PGID=1001 \
|
||||
-v bot-data:/app/data \
|
||||
--restart unless-stopped \
|
||||
boker02/uptime-kuma-discord-bot:latest
|
||||
```
|
||||
|
||||
@@ -220,15 +223,31 @@ docker network connect uptime-kuma-network uptime-kuma
|
||||
UPTIME_KUMA_URL=http://uptime-kuma:3001
|
||||
```
|
||||
|
||||
### Health Check
|
||||
|
||||
The bot includes a health check endpoint at `/health` that returns:
|
||||
- `200` if both Discord and Uptime Kuma are connected
|
||||
- `503` if either service is disconnected
|
||||
|
||||
Docker health check runs every 30 seconds and will mark the container as unhealthy if the bot can't connect to both services.
|
||||
|
||||
### Persistent Data
|
||||
|
||||
Configuration saved to Docker volume `bot-data`:
|
||||
Configuration saved to Docker volume `botdata`:
|
||||
- Channel ID
|
||||
- Message IDs (for reuse)
|
||||
- Tracked monitors
|
||||
- Groups and assignments
|
||||
- Custom title
|
||||
|
||||
### Docker Environment Variables
|
||||
|
||||
| Variable | Description | Default |
|
||||
|----------|-------------|---------|
|
||||
| `PUID`/`PGID` | User/group IDs | `1001` |
|
||||
| `DATA_DIR` | Data directory path | `/app/data` |
|
||||
| `HEALTH_PORT` | Health check port | `3000` |
|
||||
|
||||
## Security
|
||||
|
||||
1. **Secure `.env`**: Never commit to version control
|
||||
@@ -263,6 +282,12 @@ Configuration saved to Docker volume `bot-data`:
|
||||
- Message IDs saved to `data/bot-config.json`
|
||||
- Check logs for message handling status
|
||||
|
||||
### Docker-specific issues
|
||||
- **Container unhealthy**: Check health endpoint `curl http://localhost:3000/health`
|
||||
- **Container won't start**: Verify environment variables are set correctly
|
||||
- **Data not persisting**: Ensure the `botdata` volume is properly mounted
|
||||
- **Permission errors**: Adjust `PUID`/`PGID` in docker-compose.yml if needed
|
||||
|
||||
## Documentation Files
|
||||
|
||||
- **[README.md](README.md)** - This file
|
||||
|
||||
+6
-31
@@ -1,52 +1,27 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
uptime-kuma-discord-bot:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
image: boker02/uptime-kuma-discord-bot:latest
|
||||
container_name: uptime-kuma-discord-bot
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
# Discord Configuration
|
||||
DISCORD_BOT_TOKEN: ${DISCORD_BOT_TOKEN}
|
||||
|
||||
# Uptime Kuma Configuration
|
||||
UPTIME_KUMA_URL: ${UPTIME_KUMA_URL:-http://localhost:3001}
|
||||
UPTIME_KUMA_USERNAME: ${UPTIME_KUMA_USERNAME}
|
||||
UPTIME_KUMA_PASSWORD: ${UPTIME_KUMA_PASSWORD}
|
||||
|
||||
# Bot Configuration
|
||||
UPDATE_INTERVAL: ${UPDATE_INTERVAL:-60}
|
||||
EMBED_COLOR: ${EMBED_COLOR:-5814783}
|
||||
|
||||
# Admin User IDs (comma-separated Discord user IDs)
|
||||
ADMIN_USER_IDS: ${ADMIN_USER_IDS:-}
|
||||
|
||||
# Data directory for persistent configuration
|
||||
PUID: 1001
|
||||
PGID: 1001
|
||||
DATA_DIR: /app/data
|
||||
|
||||
# Persistent volume for bot configuration
|
||||
volumes:
|
||||
- bot-data:/app/data
|
||||
|
||||
# If Uptime Kuma is running in the same Docker network
|
||||
# networks:
|
||||
# - uptime-kuma-network
|
||||
|
||||
# Logging configuration
|
||||
- botdata:/app/data
|
||||
logging:
|
||||
driver: "json-file"
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
|
||||
volumes:
|
||||
bot-data:
|
||||
driver: local
|
||||
|
||||
# Uncomment if using Docker network
|
||||
# networks:
|
||||
# uptime-kuma-network:
|
||||
# external: true
|
||||
|
||||
botdata:
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
# Defaults (can be overridden via env)
|
||||
PUID="${PUID:-1001}"
|
||||
PGID="${PGID:-1001}"
|
||||
DATA_DIR="${DATA_DIR:-/app/data}"
|
||||
|
||||
# Create group/user if they don't exist yet
|
||||
# -S: system user/group (no home), -H: no home dir
|
||||
if ! getent group "${PGID}" >/dev/null 2>&1; then
|
||||
addgroup -g "${PGID}" -S appgroup || true
|
||||
else
|
||||
# If group with that GID exists, reuse its name
|
||||
appgroup="$(getent group "${PGID}" | cut -d: -f1)"
|
||||
[ -n "$appgroup" ] || appgroup="appgroup"
|
||||
fi
|
||||
|
||||
if ! getent passwd "${PUID}" >/dev/null 2>&1; then
|
||||
adduser -u "${PUID}" -S appuser -G appgroup -H || true
|
||||
else
|
||||
appuser="$(getent passwd "${PUID}" | cut -d: -f1)"
|
||||
[ -n "$appuser" ] || appuser="appuser"
|
||||
fi
|
||||
|
||||
# Ensure data dir exists
|
||||
mkdir -p "${DATA_DIR}"
|
||||
|
||||
# Try to chown when needed (bind mounts included). If it fails (e.g., read-only), keep going but warn.
|
||||
if [ -n "${CHOWN_DATA_DIR:-1}" ]; then
|
||||
if ! chown -R "${PUID}:${PGID}" "${DATA_DIR}" 2>/dev/null; then
|
||||
echo "[WARN] Could not chown ${DATA_DIR} to ${PUID}:${PGID}. If you see EACCES, fix host perms."
|
||||
fi
|
||||
fi
|
||||
|
||||
# Sanity: is it writable now?
|
||||
if ! su-exec "${PUID}:${PGID}" sh -c "test -w '${DATA_DIR}'"; then
|
||||
echo "[ERROR] ${DATA_DIR} is not writable by ${PUID}:${PGID}. Check your bind-mount ownership/ACLs."
|
||||
exit 13
|
||||
fi
|
||||
|
||||
# Run the app as the requested UID/GID
|
||||
exec su-exec "${PUID}:${PGID}" "$@"
|
||||
@@ -3,6 +3,7 @@ import { configStorage } from './config/storage';
|
||||
import { UptimeKumaService } from './services/uptime-kuma.service';
|
||||
import { DiscordService } from './services/discord.service';
|
||||
import { Logger } from './utils/logger';
|
||||
import * as http from 'http';
|
||||
|
||||
class UptimeKumaDiscordBot {
|
||||
private uptimeKuma: UptimeKumaService;
|
||||
@@ -10,6 +11,7 @@ class UptimeKumaDiscordBot {
|
||||
private updateInterval: NodeJS.Timeout | null = null;
|
||||
private logger: Logger;
|
||||
private isShuttingDown = false;
|
||||
private healthServer: http.Server | null = null;
|
||||
|
||||
constructor() {
|
||||
this.uptimeKuma = new UptimeKumaService();
|
||||
@@ -55,6 +57,8 @@ class UptimeKumaDiscordBot {
|
||||
|
||||
this.startUpdateInterval();
|
||||
|
||||
this.startHealthServer();
|
||||
|
||||
this.logger.info('Bot started successfully!');
|
||||
} catch (error: any) {
|
||||
this.logger.error(`Failed to start bot: ${error.message}`);
|
||||
@@ -130,6 +134,37 @@ class UptimeKumaDiscordBot {
|
||||
}
|
||||
}
|
||||
|
||||
private startHealthServer(): void {
|
||||
const port = parseInt(process.env.HEALTH_PORT || '3000', 10);
|
||||
|
||||
this.healthServer = http.createServer((req, res) => {
|
||||
if (req.url === '/health' && req.method === 'GET') {
|
||||
const isHealthy = this.discord.isConnected() && this.uptimeKuma.isConnected();
|
||||
const status = isHealthy ? 'healthy' : 'unhealthy';
|
||||
const statusCode = isHealthy ? 200 : 503;
|
||||
|
||||
res.writeHead(statusCode, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({
|
||||
status,
|
||||
discord: this.discord.isConnected() ? 'connected' : 'disconnected',
|
||||
uptimeKuma: this.uptimeKuma.isConnected() ? 'connected' : 'disconnected',
|
||||
timestamp: new Date().toISOString()
|
||||
}));
|
||||
} else {
|
||||
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
||||
res.end('Not Found');
|
||||
}
|
||||
});
|
||||
|
||||
this.healthServer.listen(port, '0.0.0.0', () => {
|
||||
this.logger.info(`Health check server listening on port ${port}`);
|
||||
});
|
||||
|
||||
this.healthServer.on('error', (error: any) => {
|
||||
this.logger.error(`Health server error: ${error.message}`);
|
||||
});
|
||||
}
|
||||
|
||||
private async shutdown(): Promise<void> {
|
||||
if (this.isShuttingDown) {
|
||||
return;
|
||||
@@ -145,6 +180,11 @@ class UptimeKumaDiscordBot {
|
||||
this.updateInterval = null;
|
||||
}
|
||||
|
||||
if (this.healthServer) {
|
||||
this.healthServer.close();
|
||||
this.healthServer = null;
|
||||
}
|
||||
|
||||
this.uptimeKuma.disconnect();
|
||||
this.discord.disconnect();
|
||||
|
||||
|
||||
@@ -33,6 +33,10 @@ export class DiscordService {
|
||||
return this.client;
|
||||
}
|
||||
|
||||
public isConnected(): boolean {
|
||||
return this.client.isReady();
|
||||
}
|
||||
|
||||
public async connect(): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.client.once('ready', async (client) => {
|
||||
|
||||
Reference in New Issue
Block a user