chore: make docker

This commit is contained in:
eric
2026-03-22 02:35:10 +01:00
parent 49857e620e
commit 74042182ed
11 changed files with 371 additions and 6 deletions

11
.dockerignore Normal file
View File

@@ -0,0 +1,11 @@
.git
.gitignore
.env
.envrc
node_modules
data
build
.DS_Store
README.md
flake.nix
flake.lock

View File

@@ -8,7 +8,7 @@ OPENAI_API_KEY=""
OPENROUTER_API_KEY=""
AI_CLASSIFICATION_MODEL="google/gemma-3-12b-it:free"
AI_CLASSIFICATION_FALLBACK_MODELS="meta-llama/llama-3.3-70b-instruct:free,mistralai/mistral-small-3.1-24b-instruct:free"
REPLICATE_API_TOKEN=""
REPLICATE_API_KEY=""
ELEVENLABS_API_KEY=""
ELEVENLABS_VOICE_ID=""
ELEVENLABS_MODEL="eleven_multilingual_v2"

View File

@@ -18,6 +18,8 @@ RUN bun install --frozen-lockfile
COPY src ./src
COPY tsconfig.json drizzle.config.ts ./
RUN bun run css:build
# Production stage
FROM oven/bun:1-slim
@@ -25,7 +27,7 @@ WORKDIR /app
# Install runtime dependencies
RUN apt-get update \
&& apt-get install -y --no-install-recommends ffmpeg \
&& apt-get install -y --no-install-recommends ffmpeg ca-certificates \
&& rm -rf /var/lib/apt/lists/*
# Copy from builder
@@ -33,14 +35,18 @@ COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/src ./src
COPY --from=builder /app/package.json ./
COPY --from=builder /app/tsconfig.json ./
COPY --from=builder /app/drizzle.config.ts ./
# Set environment variables
ENV NODE_ENV=production
ENV LOG_LEVEL=info
ENV DATABASE_PATH=/data/db.sqlite3
ENV WEB_PORT=3000
# Persist runtime data (SQLite)
VOLUME ["/data"]
EXPOSE 3000
# Run the bot
CMD ["bun", "run", "src/index.ts"]

View File

@@ -72,6 +72,56 @@ docker run -d \
your-image:latest
```
## Docker
Build the image:
```bash
docker build -t ghcr.io/your-org/joel-bot:latest .
```
Run it locally:
```bash
docker run -d \
--name joel-bot \
-p 3000:3000 \
-v joel_data:/data \
--env-file .env \
ghcr.io/your-org/joel-bot:latest
```
The container now starts in production mode and does not enable the CSS file watcher.
## k3s
The repo includes starter manifests in [`k8s/deployment.yaml`](/Users/eric/Projects/joel-discord/k8s/deployment.yaml), [`k8s/secret.example.yaml`](/Users/eric/Projects/joel-discord/k8s/secret.example.yaml), and [`k8s/ingress.example.yaml`](/Users/eric/Projects/joel-discord/k8s/ingress.example.yaml).
Typical flow:
```bash
# Build and push the image
docker build -t ghcr.io/your-org/joel-bot:latest .
docker push ghcr.io/your-org/joel-bot:latest
# Create your secret manifest from the example and fill in real values
cp k8s/secret.example.yaml k8s/secret.yaml
# Apply namespace, PVC, deployment, and service
kubectl apply -f k8s/deployment.yaml
kubectl apply -f k8s/secret.yaml
# Optional if you want the web UI exposed through Traefik
kubectl apply -f k8s/ingress.example.yaml
```
Notes:
- Update the image in `k8s/deployment.yaml` to your registry path.
- Set `WEB_BASE_URL` in the secret to the public HTTPS URL you expose through ingress.
- The deployment mounts `/data` on a PVC and stores SQLite at `/data/db.sqlite3`.
- k3s usually provisions the PVC automatically via the default `local-path` storage class.
## Scripts
| Script | Description |

148
flake.lock generated
View File

@@ -16,6 +16,22 @@
"type": "github"
}
},
"flake-compat_2": {
"flake": false,
"locked": {
"lastModified": 1767039857,
"narHash": "sha256-vNpUSpF5Nuw8xvDLj2KCwwksIbjua2LZCqhV1LNRDns=",
"owner": "NixOS",
"repo": "flake-compat",
"rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab",
"type": "github"
},
"original": {
"owner": "NixOS",
"repo": "flake-compat",
"type": "github"
}
},
"git-hooks": {
"inputs": {
"flake-compat": "flake-compat",
@@ -36,6 +52,26 @@
"type": "github"
}
},
"git-hooks_2": {
"inputs": {
"flake-compat": "flake-compat_2",
"gitignore": "gitignore_2",
"nixpkgs": "nixpkgs_3"
},
"locked": {
"lastModified": 1772024342,
"narHash": "sha256-+eXlIc4/7dE6EcPs9a2DaSY3fTA9AE526hGqkNID3Wg=",
"owner": "cachix",
"repo": "git-hooks.nix",
"rev": "6e34e97ed9788b17796ee43ccdbaf871a5c2b476",
"type": "github"
},
"original": {
"owner": "cachix",
"repo": "git-hooks.nix",
"type": "github"
}
},
"gitignore": {
"inputs": {
"nixpkgs": [
@@ -57,6 +93,28 @@
"type": "github"
}
},
"gitignore_2": {
"inputs": {
"nixpkgs": [
"repo-lib",
"git-hooks",
"nixpkgs"
]
},
"locked": {
"lastModified": 1709087332,
"narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=",
"owner": "hercules-ci",
"repo": "gitignore.nix",
"rev": "637db329424fd7e46cf4185293b9cc8c88c95394",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "gitignore.nix",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1764947035,
@@ -89,10 +147,98 @@
"type": "github"
}
},
"nixpkgs_3": {
"locked": {
"lastModified": 1770073757,
"narHash": "sha256-Vy+G+F+3E/Tl+GMNgiHl9Pah2DgShmIUBJXmbiQPHbI=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "47472570b1e607482890801aeaf29bfb749884f6",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_4": {
"locked": {
"lastModified": 1772542754,
"narHash": "sha256-WGV2hy+VIeQsYXpsLjdr4GvHv5eECMISX1zKLTedhdg=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "8c809a146a140c5c8806f13399592dbcb1bb5dc4",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_5": {
"locked": {
"lastModified": 1770107345,
"narHash": "sha256-tbS0Ebx2PiA1FRW8mt8oejR0qMXmziJmPaU1d4kYY9g=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "4533d9293756b63904b7238acb84ac8fe4c8c2c4",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"repo-lib": {
"inputs": {
"git-hooks": "git-hooks_2",
"nixpkgs": "nixpkgs_4",
"treefmt-nix": "treefmt-nix"
},
"locked": {
"lastModified": 1772866275,
"narHash": "sha256-lsJrFIbq6OO5wUC648VnvOmJm3qgJrlEugbdjeZsP34=",
"ref": "refs/tags/v3.0.0",
"rev": "96d2d190466dddcb9e652c38b70152f09b9fcb05",
"revCount": 50,
"type": "git",
"url": "https://git.dgren.dev/eric/nix-flake-lib"
},
"original": {
"ref": "refs/tags/v3.0.0",
"type": "git",
"url": "https://git.dgren.dev/eric/nix-flake-lib"
}
},
"root": {
"inputs": {
"git-hooks": "git-hooks",
"nixpkgs": "nixpkgs_2"
"nixpkgs": "nixpkgs_2",
"repo-lib": "repo-lib"
}
},
"treefmt-nix": {
"inputs": {
"nixpkgs": "nixpkgs_5"
},
"locked": {
"lastModified": 1770228511,
"narHash": "sha256-wQ6NJSuFqAEmIg2VMnLdCnUc0b7vslUohqqGGD+Fyxk=",
"owner": "numtide",
"repo": "treefmt-nix",
"rev": "337a4fe074be1042a35086f15481d763b8ddc0e7",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "treefmt-nix",
"type": "github"
}
}
},

View File

@@ -4,6 +4,7 @@
inputs = {
nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
git-hooks.url = "github:cachix/git-hooks.nix";
repo-lib.url = "git+https://git.dgren.dev/eric/nix-flake-lib?ref=refs/tags/v3.0.0";
};
outputs =

91
k8s/deployment.yaml Normal file
View File

@@ -0,0 +1,91 @@
apiVersion: v1
kind: Namespace
metadata:
name: joel-bot
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: joel-bot-data
namespace: joel-bot
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 5Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: joel-bot
namespace: joel-bot
spec:
replicas: 1
revisionHistoryLimit: 2
selector:
matchLabels:
app: joel-bot
template:
metadata:
labels:
app: joel-bot
spec:
containers:
- name: joel-bot
image: ghcr.io/your-org/joel-bot:latest
imagePullPolicy: IfNotPresent
env:
- name: NODE_ENV
value: "production"
- name: LOG_LEVEL
value: "info"
- name: DATABASE_PATH
value: /data/db.sqlite3
- name: WEB_PORT
value: "3000"
envFrom:
- secretRef:
name: joel-bot-env
ports:
- name: http
containerPort: 3000
readinessProbe:
httpGet:
path: /health
port: http
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
httpGet:
path: /health
port: http
initialDelaySeconds: 15
periodSeconds: 20
volumeMounts:
- name: data
mountPath: /data
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
cpu: "1"
memory: 1Gi
volumes:
- name: data
persistentVolumeClaim:
claimName: joel-bot-data
---
apiVersion: v1
kind: Service
metadata:
name: joel-bot
namespace: joel-bot
spec:
selector:
app: joel-bot
ports:
- name: http
port: 3000
targetPort: http

23
k8s/ingress.example.yaml Normal file
View File

@@ -0,0 +1,23 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: joel-bot
namespace: joel-bot
annotations:
kubernetes.io/ingress.class: traefik
spec:
rules:
- host: joel.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: joel-bot
port:
number: 3000
tls:
- hosts:
- joel.example.com
secretName: joel-bot-tls

23
k8s/secret.example.yaml Normal file
View File

@@ -0,0 +1,23 @@
apiVersion: v1
kind: Secret
metadata:
name: joel-bot-env
namespace: joel-bot
type: Opaque
stringData:
DISCORD_TOKEN: ""
DISCORD_CLIENT_ID: ""
DISCORD_CLIENT_SECRET: ""
OPENROUTER_API_KEY: ""
OPENAI_API_KEY: ""
HF_TOKEN: ""
REPLICATE_API_KEY: ""
FAL_KEY: ""
KLIPY_API_KEY: ""
ELEVENLABS_API_KEY: ""
ELEVENLABS_VOICE_ID: ""
ELEVENLABS_MODEL: "eleven_multilingual_v2"
AI_CLASSIFICATION_MODEL: "google/gemma-3-12b-it:free"
AI_CLASSIFICATION_FALLBACK_MODELS: "meta-llama/llama-3.3-70b-instruct:free,mistralai/mistral-small-3.1-24b-instruct:free"
WEB_BASE_URL: "https://joel.example.com"
SESSION_SECRET: "replace-with-a-long-random-secret"

View File

@@ -66,6 +66,17 @@ function getEnvOrDefault(key: string, defaultValue: string): string {
return Bun.env[key] ?? defaultValue;
}
function getFirstEnvOrDefault(keys: string[], defaultValue: string): string {
for (const key of keys) {
const value = Bun.env[key];
if (value !== undefined) {
return value;
}
}
return defaultValue;
}
function getBooleanEnvOrDefault(key: string, defaultValue: boolean): boolean {
const raw = Bun.env[key];
if (raw === undefined) {
@@ -112,7 +123,7 @@ export const config: BotConfig = {
temperature: parseFloat(getEnvOrDefault("AI_TEMPERATURE", "1.2")),
},
replicate: {
apiKey: getEnvOrDefault("REPLICATE_API_KEY", ""),
apiKey: getFirstEnvOrDefault(["REPLICATE_API_KEY", "REPLICATE_API_TOKEN"], ""),
},
fal: {
apiKey: getEnvOrDefault("FAL_KEY", ""),

View File

@@ -23,6 +23,7 @@ import type { FSWatcher } from "fs";
const logger = createLogger("Main");
let webCssWatcher: FSWatcher | null = null;
const isProduction = Bun.env.NODE_ENV === "production";
// Create the Discord client with required intents
const client = new BotClient({
@@ -53,7 +54,9 @@ async function main(): Promise<void> {
// Start web server after bot is logged in
await buildWebCss();
webCssWatcher = startWebCssWatcher();
if (!isProduction) {
webCssWatcher = startWebCssWatcher();
}
await startWebServer(client);
} catch (error) {
logger.error("Failed to start bot", error);