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
|
# Build stage
|
||||||
FROM node:20-alpine AS builder
|
FROM node:20-alpine AS builder
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Copy package files
|
# Install deps
|
||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
COPY tsconfig.json ./
|
COPY tsconfig.json ./
|
||||||
|
|
||||||
# Install dependencies
|
|
||||||
RUN npm ci
|
RUN npm ci
|
||||||
|
|
||||||
# Copy source code
|
# Build
|
||||||
COPY src ./src
|
COPY src ./src
|
||||||
|
|
||||||
# Build TypeScript
|
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
# Production stage
|
# Runtime stage
|
||||||
FROM node:20-alpine
|
FROM node:20-alpine
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Copy package files
|
ENV NODE_ENV=production
|
||||||
|
|
||||||
|
# Copy package files and install production deps
|
||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
|
RUN npm ci --only=production && npm cache clean --force
|
||||||
|
|
||||||
# Install production dependencies only
|
# Copy built app (make sure dist exists)
|
||||||
RUN npm ci --only=production && \
|
|
||||||
npm cache clean --force
|
|
||||||
|
|
||||||
# Copy built application
|
|
||||||
COPY --from=builder /app/dist ./dist
|
COPY --from=builder /app/dist ./dist
|
||||||
|
|
||||||
# Create non-root user
|
# Create a data directory now (final perms fixed at runtime)
|
||||||
RUN addgroup -g 1001 -S appuser && \
|
RUN mkdir -p /app/data
|
||||||
adduser -u 1001 -S appuser -G appuser
|
|
||||||
|
|
||||||
# Change ownership
|
# Install su-exec for privilege drop and wget for health checks
|
||||||
RUN chown -R appuser:appuser /app
|
RUN apk add --no-cache su-exec wget
|
||||||
|
|
||||||
# Switch to non-root user
|
# Add entrypoint
|
||||||
USER appuser
|
COPY docker/entrypoint.sh /entrypoint.sh
|
||||||
|
RUN chmod +x /entrypoint.sh
|
||||||
|
|
||||||
# Start the application
|
# Encourage a named volume by default (optional but nice)
|
||||||
CMD ["node", "dist/index.js"]
|
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
|
# 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)
|
## Step 3: Configure in Discord (1 minute)
|
||||||
|
|
||||||
Type these commands in Discord:
|
Type these commands in Discord:
|
||||||
@@ -95,6 +103,8 @@ The bot supports multiple Discord servers independently! Each server has its own
|
|||||||
- Check `.env` credentials
|
- Check `.env` credentials
|
||||||
- View logs: `docker-compose logs -f`
|
- View logs: `docker-compose logs -f`
|
||||||
- Try `/config` in Discord
|
- Try `/config` in Discord
|
||||||
|
- Check container health: `docker-compose ps`
|
||||||
|
- Test health endpoint: `curl http://localhost:3000/health`
|
||||||
|
|
||||||
### Permission denied
|
### Permission denied
|
||||||
- Add your Discord user ID to `ADMIN_USER_IDS` in `.env`
|
- 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_URL=your_url \
|
||||||
-e UPTIME_KUMA_USERNAME=your_username \
|
-e UPTIME_KUMA_USERNAME=your_username \
|
||||||
-e UPTIME_KUMA_PASSWORD=your_password \
|
-e UPTIME_KUMA_PASSWORD=your_password \
|
||||||
|
-e PUID=1001 \
|
||||||
|
-e PGID=1001 \
|
||||||
-v bot-data:/app/data \
|
-v bot-data:/app/data \
|
||||||
|
--restart unless-stopped \
|
||||||
boker02/uptime-kuma-discord-bot:latest
|
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
|
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
|
### Persistent Data
|
||||||
|
|
||||||
Configuration saved to Docker volume `bot-data`:
|
Configuration saved to Docker volume `botdata`:
|
||||||
- Channel ID
|
- Channel ID
|
||||||
- Message IDs (for reuse)
|
- Message IDs (for reuse)
|
||||||
- Tracked monitors
|
- Tracked monitors
|
||||||
- Groups and assignments
|
- Groups and assignments
|
||||||
- Custom title
|
- 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
|
## Security
|
||||||
|
|
||||||
1. **Secure `.env`**: Never commit to version control
|
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`
|
- Message IDs saved to `data/bot-config.json`
|
||||||
- Check logs for message handling status
|
- 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
|
## Documentation Files
|
||||||
|
|
||||||
- **[README.md](README.md)** - This file
|
- **[README.md](README.md)** - This file
|
||||||
|
|||||||
+7
-32
@@ -1,52 +1,27 @@
|
|||||||
version: '3.8'
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
uptime-kuma-discord-bot:
|
uptime-kuma-discord-bot:
|
||||||
build:
|
image: boker02/uptime-kuma-discord-bot:latest
|
||||||
context: .
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
container_name: uptime-kuma-discord-bot
|
container_name: uptime-kuma-discord-bot
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
# Discord Configuration
|
|
||||||
DISCORD_BOT_TOKEN: ${DISCORD_BOT_TOKEN}
|
DISCORD_BOT_TOKEN: ${DISCORD_BOT_TOKEN}
|
||||||
|
|
||||||
# Uptime Kuma Configuration
|
|
||||||
UPTIME_KUMA_URL: ${UPTIME_KUMA_URL:-http://localhost:3001}
|
UPTIME_KUMA_URL: ${UPTIME_KUMA_URL:-http://localhost:3001}
|
||||||
UPTIME_KUMA_USERNAME: ${UPTIME_KUMA_USERNAME}
|
UPTIME_KUMA_USERNAME: ${UPTIME_KUMA_USERNAME}
|
||||||
UPTIME_KUMA_PASSWORD: ${UPTIME_KUMA_PASSWORD}
|
UPTIME_KUMA_PASSWORD: ${UPTIME_KUMA_PASSWORD}
|
||||||
|
|
||||||
# Bot Configuration
|
|
||||||
UPDATE_INTERVAL: ${UPDATE_INTERVAL:-60}
|
UPDATE_INTERVAL: ${UPDATE_INTERVAL:-60}
|
||||||
EMBED_COLOR: ${EMBED_COLOR:-5814783}
|
EMBED_COLOR: ${EMBED_COLOR:-5814783}
|
||||||
|
|
||||||
# Admin User IDs (comma-separated Discord user IDs)
|
|
||||||
ADMIN_USER_IDS: ${ADMIN_USER_IDS:-}
|
ADMIN_USER_IDS: ${ADMIN_USER_IDS:-}
|
||||||
|
|
||||||
# Data directory for persistent configuration
|
PUID: 1001
|
||||||
|
PGID: 1001
|
||||||
DATA_DIR: /app/data
|
DATA_DIR: /app/data
|
||||||
|
|
||||||
# Persistent volume for bot configuration
|
|
||||||
volumes:
|
volumes:
|
||||||
- bot-data:/app/data
|
- botdata:/app/data
|
||||||
|
|
||||||
# If Uptime Kuma is running in the same Docker network
|
|
||||||
# networks:
|
|
||||||
# - uptime-kuma-network
|
|
||||||
|
|
||||||
# Logging configuration
|
|
||||||
logging:
|
logging:
|
||||||
driver: "json-file"
|
driver: json-file
|
||||||
options:
|
options:
|
||||||
max-size: "10m"
|
max-size: "10m"
|
||||||
max-file: "3"
|
max-file: "3"
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
bot-data:
|
botdata:
|
||||||
driver: local
|
|
||||||
|
|
||||||
# Uncomment if using Docker network
|
|
||||||
# networks:
|
|
||||||
# uptime-kuma-network:
|
|
||||||
# external: true
|
|
||||||
|
|
||||||
|
|||||||
@@ -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 { UptimeKumaService } from './services/uptime-kuma.service';
|
||||||
import { DiscordService } from './services/discord.service';
|
import { DiscordService } from './services/discord.service';
|
||||||
import { Logger } from './utils/logger';
|
import { Logger } from './utils/logger';
|
||||||
|
import * as http from 'http';
|
||||||
|
|
||||||
class UptimeKumaDiscordBot {
|
class UptimeKumaDiscordBot {
|
||||||
private uptimeKuma: UptimeKumaService;
|
private uptimeKuma: UptimeKumaService;
|
||||||
@@ -10,6 +11,7 @@ class UptimeKumaDiscordBot {
|
|||||||
private updateInterval: NodeJS.Timeout | null = null;
|
private updateInterval: NodeJS.Timeout | null = null;
|
||||||
private logger: Logger;
|
private logger: Logger;
|
||||||
private isShuttingDown = false;
|
private isShuttingDown = false;
|
||||||
|
private healthServer: http.Server | null = null;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.uptimeKuma = new UptimeKumaService();
|
this.uptimeKuma = new UptimeKumaService();
|
||||||
@@ -55,6 +57,8 @@ class UptimeKumaDiscordBot {
|
|||||||
|
|
||||||
this.startUpdateInterval();
|
this.startUpdateInterval();
|
||||||
|
|
||||||
|
this.startHealthServer();
|
||||||
|
|
||||||
this.logger.info('Bot started successfully!');
|
this.logger.info('Bot started successfully!');
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
this.logger.error(`Failed to start bot: ${error.message}`);
|
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> {
|
private async shutdown(): Promise<void> {
|
||||||
if (this.isShuttingDown) {
|
if (this.isShuttingDown) {
|
||||||
return;
|
return;
|
||||||
@@ -145,6 +180,11 @@ class UptimeKumaDiscordBot {
|
|||||||
this.updateInterval = null;
|
this.updateInterval = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.healthServer) {
|
||||||
|
this.healthServer.close();
|
||||||
|
this.healthServer = null;
|
||||||
|
}
|
||||||
|
|
||||||
this.uptimeKuma.disconnect();
|
this.uptimeKuma.disconnect();
|
||||||
this.discord.disconnect();
|
this.discord.disconnect();
|
||||||
|
|
||||||
|
|||||||
@@ -33,6 +33,10 @@ export class DiscordService {
|
|||||||
return this.client;
|
return this.client;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public isConnected(): boolean {
|
||||||
|
return this.client.isReady();
|
||||||
|
}
|
||||||
|
|
||||||
public async connect(): Promise<void> {
|
public async connect(): Promise<void> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
this.client.once('ready', async (client) => {
|
this.client.once('ready', async (client) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user