diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..e09631a0a --- /dev/null +++ b/.dockerignore @@ -0,0 +1,19 @@ +.git +.gitignore +.DS_Store + +node_modules +**/node_modules + +dist +**/dist +coverage +**/coverage + +.env +.env.local +.env.*.local + +.gitnexus +gitnexus-web/playwright-report +gitnexus-web/test-results diff --git a/docker/.env.example b/.env.example similarity index 84% rename from docker/.env.example rename to .env.example index 9eb399fc1..9631b4c70 100644 --- a/docker/.env.example +++ b/.env.example @@ -1,3 +1,3 @@ IMAGE_NAME=ghcr.io/abhigyanpatwari/gitnexus-web:latest CONTAINER_NAME=gitnexus-web -HOST_PORT=8080 +HOST_PORT=4173 diff --git a/.gitignore b/.gitignore index 4c2df272c..a19a6a556 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,7 @@ Thumbs.db .env .env.local .env.*.local +docker/.env # Logs *.log @@ -95,4 +96,4 @@ GitNexus.sln .swarm/ -local_docs/ \ No newline at end of file +local_docs/ diff --git a/docker/Dockerfile b/Dockerfile similarity index 59% rename from docker/Dockerfile rename to Dockerfile index 6ab998124..817918f7b 100644 --- a/docker/Dockerfile +++ b/Dockerfile @@ -1,4 +1,7 @@ -FROM node:20-alpine AS builder +ARG BUILDPLATFORM +ARG TARGETPLATFORM + +FROM --platform=$BUILDPLATFORM node:20-bookworm-slim AS builder WORKDIR /app @@ -15,11 +18,13 @@ RUN npm ci --prefix gitnexus-web COPY gitnexus-web ./gitnexus-web RUN npm run build --prefix gitnexus-web -FROM nginx:1.27-alpine AS runtime +FROM --platform=$TARGETPLATFORM node:20-bookworm-slim AS runtime -COPY docker/nginx.conf /etc/nginx/conf.d/default.conf -COPY --from=builder /app/gitnexus-web/dist /usr/share/nginx/html +WORKDIR /app -EXPOSE 80 +COPY --from=builder /app/gitnexus-web/dist ./dist +COPY docker-server.mjs ./docker-server.mjs -CMD ["nginx", "-g", "daemon off;"] +EXPOSE 4173 + +CMD ["node", "docker-server.mjs"] diff --git a/README.md b/README.md index 70181581f..c2c59a16d 100644 --- a/README.md +++ b/README.md @@ -340,17 +340,36 @@ npm run dev ```bash docker run --rm \ --name gitnexus-web \ - -p 8080:80 \ + -p 4173:4173 \ ghcr.io/abhigyanpatwari/gitnexus-web:latest ``` Or with Docker Compose: ```bash -docker compose -f docker/compose.yaml up -d +docker compose -f docker-compose.yaml up -d ``` -Docker details, including the example `.env`, compose file, and the source [docker/Dockerfile](docker/Dockerfile), live in [docker/README.md](docker/README.md). +Optional env file: + +```bash +cp .env.example .env +set -a +source .env +set +a +``` + +Docker files: + +- [Dockerfile](Dockerfile) is the source for the published `gitnexus-web` image. It builds `gitnexus-shared` and `gitnexus-web`, then runs the app directly with Vite preview. +- [docker-compose.yaml](docker-compose.yaml) starts the published image with Docker Compose. +- [.env.example](.env.example) sets the image name, container name, and exposed port for the example commands. + +Notes: + +- The published image serves the production frontend only. It does not start `gitnexus serve`. +- In backend mode, the app still defaults to `http://localhost:4747` unless you change the server URL in the UI. +- If you do not want an env file, the defaults are `ghcr.io/abhigyanpatwari/gitnexus-web:latest`, container name `gitnexus-web`, and port `4173`. The web UI uses the same indexing pipeline as the CLI but runs entirely in WebAssembly (Tree-sitter WASM, LadybugDB WASM, in-browser embeddings). It's great for quick exploration but limited by browser memory for larger repos. diff --git a/docker/compose.yaml b/docker-compose.yaml similarity index 53% rename from docker/compose.yaml rename to docker-compose.yaml index ec3f1c9f2..b0fba541c 100644 --- a/docker/compose.yaml +++ b/docker-compose.yaml @@ -1,7 +1,7 @@ services: gitnexus-web: - image: ${IMAGE_NAME:-ghcr.io/abhigyanpatwari/gitnexus-web:latest} + image: ${IMAGE_NAME:-ghcr.io/abhigyanpatwari/gitnexus:latest} container_name: ${CONTAINER_NAME:-gitnexus-web} ports: - - "${HOST_PORT:-8080}:80" + - "${HOST_PORT:-4173}:4173" restart: unless-stopped diff --git a/docker-server.mjs b/docker-server.mjs new file mode 100644 index 000000000..911488ce0 --- /dev/null +++ b/docker-server.mjs @@ -0,0 +1,67 @@ +import { createReadStream, existsSync } from 'node:fs'; +import { stat } from 'node:fs/promises'; +import { createServer } from 'node:http'; +import { extname, join, normalize } from 'node:path'; + +const host = '0.0.0.0'; +const port = Number(process.env.PORT || '4173'); +const root = join(process.cwd(), 'dist'); + +const contentTypes = { + '.css': 'text/css; charset=utf-8', + '.html': 'text/html; charset=utf-8', + '.js': 'text/javascript; charset=utf-8', + '.json': 'application/json; charset=utf-8', + '.map': 'application/json; charset=utf-8', + '.png': 'image/png', + '.svg': 'image/svg+xml', + '.txt': 'text/plain; charset=utf-8', + '.woff': 'font/woff', + '.woff2': 'font/woff2', +}; + +function resolvePath(urlPath) { + const cleanPath = normalize(decodeURIComponent(urlPath).replace(/^\/+/, '')); + const candidate = join(root, cleanPath); + if (!candidate.startsWith(root)) return null; + return candidate; +} + +const server = createServer(async (req, res) => { + const requestPath = req.url?.split('?')[0] || '/'; + let filePath = resolvePath(requestPath); + + if (!filePath) { + res.writeHead(400); + res.end('Bad request'); + return; + } + + try { + const fileStat = await stat(filePath).catch(() => null); + if (fileStat?.isDirectory()) { + filePath = join(filePath, 'index.html'); + } else if (!fileStat?.isFile()) { + filePath = join(root, 'index.html'); + } + + if (!existsSync(filePath)) { + res.writeHead(404); + res.end('Not found'); + return; + } + + res.writeHead(200, { + 'Cache-Control': filePath.includes('/assets/') ? 'public, max-age=31536000, immutable' : 'no-cache', + 'Content-Type': contentTypes[extname(filePath)] || 'application/octet-stream', + }); + createReadStream(filePath).pipe(res); + } catch (error) { + res.writeHead(500); + res.end(error instanceof Error ? error.message : 'Internal server error'); + } +}); + +server.listen(port, host, () => { + console.log(`gitnexus-web listening on http://${host}:${port}`); +}); diff --git a/docker/README.md b/docker/README.md deleted file mode 100644 index d6ab86577..000000000 --- a/docker/README.md +++ /dev/null @@ -1,55 +0,0 @@ -# Docker - -This folder contains the Docker assets for `gitnexus-web`. - -## Files - -- `Dockerfile` is the source for the published `gitnexus-web` image. It builds `gitnexus-shared` and `gitnexus-web` in a Node builder stage, then copies only the compiled static site into a tiny `nginx:alpine` runtime image. -- `nginx.conf` serves the built assets and falls back to `index.html` so client-side routes keep working. -- `compose.yaml` starts the published image with Docker Compose. -- `.env.example` is an optional shell env file for the example commands below. It sets the image name, container name, and exposed port. It does not inject application config into the frontend bundle. - -## Optional env file - -Create a local env file if you want reusable image/container names and host port settings: - -```bash -cp docker/.env.example docker/.env -set -a -source docker/.env -set +a -``` - -`docker/.env` is ignored by git via the repository-wide `.env` rules. - -## Run with Docker - -```bash -docker run --rm \ - --name "${CONTAINER_NAME:-gitnexus-web}" \ - -p "${HOST_PORT:-8080}:80" \ - "${IMAGE_NAME:-ghcr.io/abhigyanpatwari/gitnexus-web:latest}" -``` - -Then open `http://localhost:${HOST_PORT:-8080}`. - -## Run with Docker Compose - -```bash -docker compose --env-file docker/.env -f docker/compose.yaml up -d -``` - -If you do not want an env file, the defaults in `compose.yaml` are already set to: - -```text -image: ghcr.io/abhigyanpatwari/gitnexus-web:latest -container_name: gitnexus-web -port: 8080 -> 80 -``` - -## Notes - -- The published image serves the production frontend only. It does not start `gitnexus serve`. -- In backend mode, the app still defaults to `http://localhost:4747` unless you change the server URL in the UI. -- The Dockerfile is kept here so the published image definition is versioned with the app, even if most users only need `docker run` or `docker compose`. -- For iterative frontend development, `cd gitnexus-web && npm run dev` is still the faster workflow. diff --git a/docker/nginx.conf b/docker/nginx.conf deleted file mode 100644 index 0ee6c38a4..000000000 --- a/docker/nginx.conf +++ /dev/null @@ -1,16 +0,0 @@ -server { - listen 80; - server_name _; - - root /usr/share/nginx/html; - index index.html; - - location / { - try_files $uri $uri/ /index.html; - } - - location /assets/ { - expires 1y; - add_header Cache-Control "public, immutable"; - } -}