diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..3412291 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,8 @@ +FROM node:20-alpine +WORKDIR /app +COPY package.json ./ +RUN npm install --production +COPY server.js ./ +COPY public ./public +EXPOSE 3000 +CMD ["node", "server.js"] diff --git a/README.md b/README.md index a3000d8..96dd0fc 100644 --- a/README.md +++ b/README.md @@ -1 +1,46 @@ # MacAI + +AI-powered search assistant using Cerebras LLM and SearXNG. + +## Quick Start (Docker) + +Run both the Node server and SearXNG together: + +```bash +# Create a .env file with your API key +echo "CEREBRAS_API_KEY=your-key-here" > .env + +# Start everything +docker compose up -d +``` + +Open [http://localhost:3000](http://localhost:3000) in your browser. + +Inside Docker Compose the Node server reaches SearXNG via `http://searxng:8080` +(the Docker service name and internal port). This is set automatically by +`docker-compose.yml`. + +## Running the Node Server on the Host + +If you prefer to run the Node server directly on your machine while SearXNG +stays in Docker: + +```bash +# Start only SearXNG +docker compose up -d searxng + +# Install dependencies and start the server +npm install +npm start +``` + +When running on the host, the default `SEARXNG_URL` is `http://localhost:8888`, +which maps to the container's port 8080 via the published Docker port. + +## Environment Variables + +| Variable | Default | Description | +| ------------------ | ------------------------ | ------------------------------- | +| `CEREBRAS_API_KEY` | *(required)* | API key for Cerebras | +| `SEARXNG_URL` | `http://localhost:8888` | SearXNG base URL | +| `PORT` | `3000` | Port for the Express server | diff --git a/docker-compose.yml b/docker-compose.yml index c63c14b..fc1f7ae 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,5 +8,26 @@ services: - ./searxng/settings.yml:/etc/searxng/settings.yml:ro - ./searxng/limiter.toml:/etc/searxng/limiter.toml:ro environment: - - SEARXNG_BASE_URL=http://localhost:8888/ + - SEARXNG_BASE_URL=http://searxng:8080/ restart: unless-stopped + networks: + - macai-net + + server: + build: . + container_name: macai-server + ports: + - "3000:3000" + environment: + - SEARXNG_URL=http://searxng:8080 + env_file: + - .env + depends_on: + - searxng + restart: unless-stopped + networks: + - macai-net + +networks: + macai-net: + driver: bridge diff --git a/server.js b/server.js index 1bf4064..6996915 100644 --- a/server.js +++ b/server.js @@ -324,15 +324,27 @@ app.listen(PORT, async () => { console.log(`✓ http://localhost:${PORT}`); if (!CEREBRAS_KEY_RESOLVED) console.warn('⚠ CEREBRAS_API_KEY not set'); - // Check SearXNG connectivity on startup - try { - const ctrl = new AbortController(); - const timer = setTimeout(() => ctrl.abort(), 3000); - const resp = await fetch(`${SEARXNG_URL}/healthz`, { signal: ctrl.signal }); - clearTimeout(timer); - if (resp.ok) console.log(`✓ SearXNG reachable at ${SEARXNG_URL}`); - else console.warn(`⚠ SearXNG returned HTTP ${resp.status} at ${SEARXNG_URL}`); - } catch (_) { - console.warn(`⚠ SearXNG not reachable at ${SEARXNG_URL} — run: docker compose up -d`); + // Check SearXNG connectivity on startup (retry up to 5 times for Docker cold-start) + const maxRetries = 5; + for (let attempt = 1; attempt <= maxRetries; attempt++) { + try { + const ctrl = new AbortController(); + const timer = setTimeout(() => ctrl.abort(), 5000); + const resp = await fetch(`${SEARXNG_URL}/healthz`, { signal: ctrl.signal }); + clearTimeout(timer); + if (resp.ok) { + console.log(`✓ SearXNG reachable at ${SEARXNG_URL}`); + break; + } + console.warn(`⚠ SearXNG returned HTTP ${resp.status} at ${SEARXNG_URL}`); + break; + } catch (_) { + if (attempt < maxRetries) { + console.warn(`⚠ SearXNG not reachable at ${SEARXNG_URL} (attempt ${attempt}/${maxRetries}), retrying in 3s…`); + await new Promise(r => setTimeout(r, 3000)); + } else { + console.warn(`⚠ SearXNG not reachable at ${SEARXNG_URL} after ${maxRetries} attempts — run: docker compose up -d`); + } + } } });