diff --git a/.env.docker.example b/.env.docker.example new file mode 100644 index 0000000..bef9e6d --- /dev/null +++ b/.env.docker.example @@ -0,0 +1,37 @@ +# ── Docker Compose (.env) — copy to .env in the repo root ───────────────────── +# cp .env.docker.example .env + +# Discord Bot +TOKEN=TOKEN_HERE +brand_name=HexaHost +OWNER_IDS=YOUR_DISCORD_USER_ID_HERE +DASHBOARD_ADMIN_IDS=YOUR_DISCORD_USER_ID_HERE + +# Shared secret — must match between bot API and dashboard +DASHBOARD_API_KEY=generate_a_long_random_secret_here + +# Lavalink (optional music) +LAVALINK_HOST=lavalink.jirayu.net +LAVALINK_PASSWORD=youshallnotpass +LAVALINK_SECURE=false +LAVALINK_PORT=13592 + +EMOJI_SYNC=false +JISHAKU_ENABLED=false + +# Traefik (optional override — default in compose: letsencrypt) +# TRAEFIK_CERT_RESOLVER=letsencrypt + +# ── Dashboard / NextAuth ────────────────────────────────────────────────────── +NEXTAUTH_SECRET=generate_a_long_random_secret_here +DISCORD_CLIENT_ID= +DISCORD_CLIENT_SECRET= + +# Same Discord user IDs as owners/admins (comma-separated) +ADMIN_IDS=YOUR_DISCORD_USER_ID_HERE +NEXT_PUBLIC_ADMIN_IDS=YOUR_DISCORD_USER_ID_HERE +NEXT_PUBLIC_BRAND_NAME=HexaHost +NEXT_PUBLIC_BRAND_NAME_WORD=HH + +# Discord OAuth Redirect URL in the Discord Developer Portal: +# https://bot.hexahost.de/api/auth/callback/discord diff --git a/.gitignore b/.gitignore index fdc8c53..c2cfd49 100644 --- a/.gitignore +++ b/.gitignore @@ -46,6 +46,8 @@ build/ .env .env.local .env.*.local +!.env.docker.example +!.env.example # Ignore SQLite runtime files bot/db/**/*.db-journal diff --git a/DOCKER.md b/DOCKER.md new file mode 100644 index 0000000..ae84e80 --- /dev/null +++ b/DOCKER.md @@ -0,0 +1,19 @@ +# Docker + +Copy `.env.docker.example` to `.env`, fill in secrets, then: + +```bash +docker network create traefik-network # only if it does not exist yet +docker compose up -d --build +``` + +| Service | Container | Exposure | +|---------|-----------|----------| +| Bot + FastAPI | `hexahost-bot` | Internal (`bot:8000`) — Discord Gateway outbound | +| Dashboard | `hexahost-dashboard` | Traefik → `https://bot.hexahost.de` | + +Data volumes: `bot-db`, `bot-jsondb`, `bot-logs`. + +Discord OAuth redirect: `https://bot.hexahost.de/api/auth/callback/discord` + +If your Traefik cert resolver is not named `letsencrypt`, set `TRAEFIK_CERT_RESOLVER` in `.env`. Entrypoints expected: `web` (80) and `websecure` (443). diff --git a/bot/.dockerignore b/bot/.dockerignore new file mode 100644 index 0000000..f8914c7 --- /dev/null +++ b/bot/.dockerignore @@ -0,0 +1,19 @@ +.env +.env.* +!.env.example +__pycache__ +*.py[cod] +*.db +*.db-journal +.venv +venv +myenv +.git +.gitignore +*.md +LICENSE +logs +*.log +.pytest_cache +.mypy_cache +CodeX.py diff --git a/bot/Dockerfile b/bot/Dockerfile new file mode 100644 index 0000000..8683ac2 --- /dev/null +++ b/bot/Dockerfile @@ -0,0 +1,29 @@ +# HexaHost Discord Bot + FastAPI dashboard backend +FROM python:3.12-slim-bookworm + +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + PIP_NO_CACHE_DIR=1 \ + PIP_DISABLE_PIP_VERSION_CHECK=1 + +WORKDIR /app + +RUN apt-get update && apt-get install -y --no-install-recommends \ + gcc \ + libffi-dev \ + libjpeg62-turbo-dev \ + zlib1g-dev \ + libsodium-dev \ + && rm -rf /var/lib/apt/lists/* + +COPY requirements.txt . +RUN pip install -r requirements.txt + +COPY . . + +# Persist SQLite / JSON data outside the image +RUN mkdir -p /app/db /app/jsondb /app/logs + +EXPOSE 8000 + +CMD ["python", "HexaHost.py"] diff --git a/dashboard/.dockerignore b/dashboard/.dockerignore new file mode 100644 index 0000000..66273b4 --- /dev/null +++ b/dashboard/.dockerignore @@ -0,0 +1,11 @@ +.env +.env.* +!.env.example +node_modules +.next +.git +.gitignore +*.md +LICENSE +npm-debug.log* +.eslintrc.json diff --git a/dashboard/Dockerfile b/dashboard/Dockerfile new file mode 100644 index 0000000..63609b9 --- /dev/null +++ b/dashboard/Dockerfile @@ -0,0 +1,44 @@ +# HexaHost Dashboard (Next.js) +FROM node:20-alpine AS deps +WORKDIR /app +COPY package.json package-lock.json* ./ +RUN npm install + +FROM node:20-alpine AS builder +WORKDIR /app +COPY --from=deps /app/node_modules ./node_modules +COPY . . + +# Build-time public env (overridden at runtime where possible) +ARG NEXT_PUBLIC_BRAND_NAME=HexaHost +ARG NEXT_PUBLIC_BRAND_NAME_WORD=HH +ARG NEXT_PUBLIC_ADMIN_IDS= +ARG NEXT_PUBLIC_API_URL= + +ENV NEXT_TELEMETRY_DISABLED=1 \ + NEXT_PUBLIC_BRAND_NAME=$NEXT_PUBLIC_BRAND_NAME \ + NEXT_PUBLIC_BRAND_NAME_WORD=$NEXT_PUBLIC_BRAND_NAME_WORD \ + NEXT_PUBLIC_ADMIN_IDS=$NEXT_PUBLIC_ADMIN_IDS \ + NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL + +RUN mkdir -p public && npm run build + +FROM node:20-alpine AS runner +WORKDIR /app + +ENV NODE_ENV=production \ + NEXT_TELEMETRY_DISABLED=1 \ + PORT=3000 \ + HOSTNAME=0.0.0.0 + +RUN addgroup --system --gid 1001 nodejs \ + && adduser --system --uid 1001 nextjs + +COPY --from=builder /app/public ./public +COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ +COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static + +USER nextjs +EXPOSE 3000 + +CMD ["node", "server.js"] diff --git a/dashboard/next.config.js b/dashboard/next.config.js index 92e11e1..e97b154 100644 --- a/dashboard/next.config.js +++ b/dashboard/next.config.js @@ -1,5 +1,6 @@ /** @type {import('next').NextConfig} */ const nextConfig = { + output: "standalone", images: { remotePatterns: [ { diff --git a/dashboard/next.config.mjs b/dashboard/next.config.mjs index 21f889b..7d57eb9 100644 --- a/dashboard/next.config.mjs +++ b/dashboard/next.config.mjs @@ -1,6 +1,7 @@ /** @type {import('next').NextConfig} */ const nextConfig = { reactStrictMode: true, + output: "standalone", images: { remotePatterns: [ { diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..df2c78f --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,99 @@ +# HexaHost — Bot + Dashboard behind Traefik +# +# Prerequisites: +# - External Docker network: traefik-network +# - Traefik already running and attached to that network +# - Copy .env.docker.example → .env and fill secrets +# +# Start: +# docker compose up -d --build + +name: hexahost + +services: + bot: + build: + context: ./bot + dockerfile: Dockerfile + container_name: hexahost-bot + restart: unless-stopped + env_file: + - .env + environment: + API_ENABLED: "true" + API_HOST: "0.0.0.0" + API_PORT: "8000" + TUNNEL_ENABLED: "false" + CORS_ORIGINS: "https://bot.hexahost.de" + volumes: + - bot-db:/app/db + - bot-jsondb:/app/jsondb + - bot-logs:/app/logs + networks: + - traefik-network + - hexahost-internal + healthcheck: + test: + [ + "CMD", + "python", + "-c", + "import socket; s=socket.create_connection(('127.0.0.1',8000),5); s.close()", + ] + interval: 30s + timeout: 10s + retries: 5 + start_period: 90s + + dashboard: + build: + context: ./dashboard + dockerfile: Dockerfile + args: + NEXT_PUBLIC_BRAND_NAME: ${NEXT_PUBLIC_BRAND_NAME:-HexaHost} + NEXT_PUBLIC_BRAND_NAME_WORD: ${NEXT_PUBLIC_BRAND_NAME_WORD:-HH} + NEXT_PUBLIC_ADMIN_IDS: ${NEXT_PUBLIC_ADMIN_IDS:-} + container_name: hexahost-dashboard + restart: unless-stopped + depends_on: + bot: + condition: service_started + env_file: + - .env + environment: + # Server-side proxy → Bot API (Docker-intern, nicht öffentlich) + DASHBOARD_API_URL: http://bot:8000/api/v1 + NEXTAUTH_URL: https://bot.hexahost.de/ + NEXT_PUBLIC_BRAND_NAME: ${NEXT_PUBLIC_BRAND_NAME:-HexaHost} + NEXT_PUBLIC_BRAND_NAME_WORD: ${NEXT_PUBLIC_BRAND_NAME_WORD:-HH} + PORT: "3000" + HOSTNAME: "0.0.0.0" + networks: + - traefik-network + - hexahost-internal + labels: + - traefik.enable=true + - traefik.docker.network=traefik-network + # HTTP → HTTPS redirect (optional, falls Entrypoint "web" existiert) + - traefik.http.routers.hexahost-dashboard-http.rule=Host(`bot.hexahost.de`) + - traefik.http.routers.hexahost-dashboard-http.entrypoints=web + - traefik.http.routers.hexahost-dashboard-http.middlewares=hexahost-https-redirect + - traefik.http.middlewares.hexahost-https-redirect.redirectscheme.scheme=https + - traefik.http.middlewares.hexahost-https-redirect.redirectscheme.permanent=true + # HTTPS + - traefik.http.routers.hexahost-dashboard.rule=Host(`bot.hexahost.de`) + - traefik.http.routers.hexahost-dashboard.entrypoints=websecure + - traefik.http.routers.hexahost-dashboard.tls=true + - traefik.http.routers.hexahost-dashboard.tls.certresolver=${TRAEFIK_CERT_RESOLVER:-letsencrypt} + - traefik.http.services.hexahost-dashboard.loadbalancer.server.port=3000 + +networks: + traefik-network: + external: true + hexahost-internal: + driver: bridge + +volumes: + bot-db: + bot-jsondb: + bot-logs: