diff --git a/backend/deployment/README.md b/backend/deployment/README.md deleted file mode 100644 index 5636c6cf3b7..00000000000 --- a/backend/deployment/README.md +++ /dev/null @@ -1,5 +0,0 @@ -This serves as an example for how to deploy everything in a single box. This is far -from optimal, but can get you started easily and cheaply. To run: - -1. Set up a `.env` file in this directory with relevant environment variables (TODO: document) -2. `docker compose up -d --build` \ No newline at end of file diff --git a/deployment/.gitignore b/deployment/.gitignore new file mode 100644 index 00000000000..81a38be68dc --- /dev/null +++ b/deployment/.gitignore @@ -0,0 +1 @@ +.env* diff --git a/deployment/README.md b/deployment/README.md new file mode 100644 index 00000000000..d70202cbe49 --- /dev/null +++ b/deployment/README.md @@ -0,0 +1,7 @@ +This serves as an example for how to deploy everything on a single machine. This is +not optimal, but can get you started easily and cheaply. To run: + +1. Set up a `.env` + `.env.nginx` file in this directory with relevant environment variables + a. TODO: add description of required variables +2. `chmod +x init-letsencrypt.sh` + `./init-letsencrypt.sh` to setup https certificate +2. `docker compose up -d --build` to start nginx, postgres, web/api servers, and the background indexing job diff --git a/backend/deployment/data/nginx/app.conf b/deployment/data/nginx/app.conf.template similarity index 59% rename from backend/deployment/data/nginx/app.conf rename to deployment/data/nginx/app.conf.template index 099e256190c..3d2d66c4d8a 100644 --- a/backend/deployment/data/nginx/app.conf +++ b/deployment/data/nginx/app.conf.template @@ -6,14 +6,20 @@ upstream app_server { #server unix:/tmp/gunicorn.sock fail_timeout=0; # for a TCP configuration + # TODO: use gunicorn to manage multiple processes server api:8080 fail_timeout=0; } +upstream web_server { + server web:3000 fail_timeout=0; +} + server { listen 80; - server_name api.danswer.dev; + server_name ${DOMAIN}; - location / { + location ~ ^/api(.*)$ { + rewrite ^/api(/.*)$ $1 break; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header Host $http_host; @@ -23,6 +29,14 @@ server { proxy_pass http://app_server; } + location / { + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Host $http_host; + proxy_redirect off; + proxy_pass http://web_server; + } + location /.well-known/acme-challenge/ { root /var/www/certbot; } @@ -30,14 +44,14 @@ server { server { listen 443 ssl; - server_name api.danswer.dev; + server_name ${DOMAIN}; location / { - proxy_pass http://api.danswer.dev; + proxy_pass http://${DOMAIN}; } - ssl_certificate /etc/letsencrypt/live/api.danswer.dev/fullchain.pem; - ssl_certificate_key /etc/letsencrypt/live/api.danswer.dev/privkey.pem; + ssl_certificate /etc/letsencrypt/live/${DOMAIN}/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/${DOMAIN}/privkey.pem; include /etc/letsencrypt/options-ssl-nginx.conf; ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; } \ No newline at end of file diff --git a/backend/deployment/docker-compose.yml b/deployment/docker-compose.yml similarity index 71% rename from backend/deployment/docker-compose.yml rename to deployment/docker-compose.yml index dddb2316efa..cf78dd252b6 100644 --- a/backend/deployment/docker-compose.yml +++ b/deployment/docker-compose.yml @@ -3,13 +3,13 @@ version: '3' services: api: build: - context: .. + context: ../backend dockerfile: Dockerfile depends_on: - db - # just for local testing - ports: - - "8080:8080" + # uncomment for local testing + # ports: + # - "8080:8080" env_file: - .env environment: @@ -18,7 +18,7 @@ services: - local_dynamic_storage:/home/storage background: build: - context: .. + context: ../backend dockerfile: Dockerfile.background depends_on: - db @@ -28,6 +28,14 @@ services: - POSTGRES_HOST=db volumes: - local_dynamic_storage:/home/storage + web: + build: + context: ../web + dockerfile: Dockerfile + args: + NEXT_PUBLIC_API_URL: /api + depends_on: + - api db: image: postgres:15.2-alpine restart: always @@ -47,7 +55,11 @@ services: - ./data/nginx:/etc/nginx/conf.d - ./data/certbot/conf:/etc/letsencrypt - ./data/certbot/www:/var/www/certbot - command: "/bin/sh -c 'while :; do sleep 6h & wait $${!}; nginx -s reload; done & nginx -g \"daemon off;\"'" + command: > + /bin/sh -c "envsubst '$$\{DOMAIN\}' < /etc/nginx/conf.d/app.conf.template > /etc/nginx/conf.d/app.conf + && while :; do sleep 6h & wait $${!}; nginx -s reload; done & nginx -g \"daemon off;\"" + env_file: + - .env.nginx depends_on: - api certbot: diff --git a/backend/deployment/init-letsencrypt.sh b/deployment/init-letsencrypt.sh similarity index 89% rename from backend/deployment/init-letsencrypt.sh rename to deployment/init-letsencrypt.sh index df607a94456..4341e775e16 100644 --- a/backend/deployment/init-letsencrypt.sh +++ b/deployment/init-letsencrypt.sh @@ -1,14 +1,20 @@ #!/bin/bash +# .env.nginx file must be present in the same directory as this script and +# must set DOMAIN (and optionally EMAIL) +set -o allexport +source .env.nginx +set +o allexport + if ! [ -x "$(command -v docker compose)" ]; then echo 'Error: docker compose is not installed.' >&2 exit 1 fi -domains=(api.danswer.dev www.api.danswer.dev) +domains=("$DOMAIN" "www.$DOMAIN") rsa_key_size=4096 data_path="./data/certbot" -email="" # Adding a valid address is strongly recommended +email="$EMAIL" # Adding a valid address is strongly recommended staging=0 # Set to 1 if you're testing your setup to avoid hitting request limits if [ -d "$data_path" ]; then diff --git a/web/Dockerfile b/web/Dockerfile new file mode 100644 index 00000000000..9a023ec4921 --- /dev/null +++ b/web/Dockerfile @@ -0,0 +1,55 @@ +FROM node:18-alpine AS base + +# Step 1. Install dependencies only when needed +FROM base AS deps +# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed. +RUN apk add --no-cache libc6-compat +WORKDIR /app + +# Install dependencies based on the preferred package manager +COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./ +RUN \ + if [ -f yarn.lock ]; then yarn --frozen-lockfile; \ + elif [ -f package-lock.json ]; then npm ci; \ + elif [ -f pnpm-lock.yaml ]; then yarn global add pnpm && pnpm i --frozen-lockfile; \ + else echo "Lockfile not found." && exit 1; \ + fi + + +# Step 2. Rebuild the source code only when needed +FROM base AS builder +WORKDIR /app +COPY --from=deps /app/node_modules ./node_modules +COPY . . + +# Disable automatic telemetry collection +ENV NEXT_TELEMETRY_DISABLED 1 + +RUN npm run build + +# Step 3. Production image, copy all the files and run next +FROM base AS runner +WORKDIR /app + +ENV NODE_ENV production +# Disable automatic telemetry collection +ENV NEXT_TELEMETRY_DISABLED 1 + +# Don't run production as root +RUN addgroup --system --gid 1001 nodejs +RUN adduser --system --uid 1001 nextjs +USER nextjs + +# Add back in if we add anything to `public` +# COPY --from=builder /app/public ./public + +# Automatically leverage output traces to reduce image size +# https://nextjs.org/docs/advanced-features/output-file-tracing +COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ +COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static + +# Note: Don't expose ports here, Compose will handle that for us if necessary. +# If you want to run this without compose, specify the ports to +# expose via cli + +CMD ["node", "server.js"] diff --git a/web/next.config.js b/web/next.config.js index 4436b22b5b3..93c48a2e342 100644 --- a/web/next.config.js +++ b/web/next.config.js @@ -3,6 +3,23 @@ const nextConfig = { experimental: { appDir: true, }, + output: "standalone", + redirects: async () => { + // In production, something else (nginx in the one box setup) takes care + // of this redirect + // NOTE: this may get adjusted later if we want to support hosting of the + // API server on a different domain without requring some kind of proxy + if (process.env.NODE_ENV === "development") { + return [ + { + source: "/api/:path*", + destination: "http://localhost:8080/:path*", // Proxy to Backend + permanent: true, + }, + ]; + } + return []; + }, }; module.exports = nextConfig; diff --git a/web/src/components/SearchBar.tsx b/web/src/components/SearchBar.tsx index ff34e48174e..bc7e1b7be0c 100644 --- a/web/src/components/SearchBar.tsx +++ b/web/src/components/SearchBar.tsx @@ -6,11 +6,8 @@ import "tailwindcss/tailwind.css"; import { SearchResultsDisplay } from "./SearchResultsDisplay"; import { SearchResponse } from "./types"; -const BACKEND_URL = - process.env.NEXT_PUBLIC_BACKEND_URL || "http://localhost:8080"; // "http://servi-lb8a1-jhqpsz92kbm2-1605938866.us-east-2.elb.amazonaws.com/direct-qa"; - const searchRequest = async (query: string): Promise => { - const response = await fetch(BACKEND_URL + "/direct-qa", { + const response = await fetch("/api/direct-qa", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ diff --git a/web/src/components/admin/connectors/SlackForm.tsx b/web/src/components/admin/connectors/SlackForm.tsx index 10b19a4f404..845f1cd1171 100644 --- a/web/src/components/admin/connectors/SlackForm.tsx +++ b/web/src/components/admin/connectors/SlackForm.tsx @@ -1,7 +1,6 @@ import React, { useEffect, useState } from "react"; import { Formik, Form, Field, ErrorMessage, FormikHelpers } from "formik"; import * as Yup from "yup"; -import { BACKEND_URL } from "@/lib/constants"; import { Popup } from "./Popup"; interface FormData { @@ -18,7 +17,7 @@ const validationSchema = Yup.object().shape({ }); const getConfig = async (): Promise => { - const response = await fetch(BACKEND_URL + "/admin/slack_connector_config"); + const response = await fetch("/api/admin/slack_connector_config"); return response.json(); }; @@ -31,17 +30,13 @@ const handleSubmit = async ( ) => { setSubmitting(true); try { - // Replace this with your actual API call - const response = await fetch( - BACKEND_URL + "/admin/slack_connector_config", - { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(values), - } - ); + const response = await fetch("/api/admin/slack_connector_config", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(values), + }); if (response.ok) { setPopup({ message: "Success!", type: "success" }); diff --git a/web/src/lib/constants.ts b/web/src/lib/constants.ts deleted file mode 100644 index 07877b46267..00000000000 --- a/web/src/lib/constants.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const BACKEND_URL = - process.env.NEXT_PUBLIC_BACKEND_URL || "http://localhost:8080"; // "http://servi-lb8a1-jhqpsz92kbm2-1605938866.us-east-2.elb.amazonaws.com/direct-qa";