chore: make docker
This commit is contained in:
11
.dockerignore
Normal file
11
.dockerignore
Normal file
@@ -0,0 +1,11 @@
|
||||
.git
|
||||
.gitignore
|
||||
.env
|
||||
.envrc
|
||||
node_modules
|
||||
data
|
||||
build
|
||||
.DS_Store
|
||||
README.md
|
||||
flake.nix
|
||||
flake.lock
|
||||
@@ -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"
|
||||
|
||||
@@ -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"]
|
||||
|
||||
50
README.md
50
README.md
@@ -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
148
flake.lock
generated
@@ -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"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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
91
k8s/deployment.yaml
Normal 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
23
k8s/ingress.example.yaml
Normal 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
23
k8s/secret.example.yaml
Normal 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"
|
||||
@@ -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", ""),
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user