mirror of
https://github.com/khoj-ai/khoj.git
synced 2024-11-23 15:38:55 +01:00
[Multi-User Part 5]: Add a production Docker file and use a gunicorn configuration with it (#514)
- Add a productionized setup for the Khoj server using `gunicorn` with multiple workers for handling requests - Add a new Dockerfile meant for production config at `ghcr.io/khoj-ai/khoj:prod`; the existing Docker config should remain the same
This commit is contained in:
parent
9acc722f7f
commit
5f3f6b7c61
12 changed files with 117 additions and 11 deletions
48
.github/workflows/dockerize_production.yml
vendored
Normal file
48
.github/workflows/dockerize_production.yml
vendored
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
name: dockerize-prod
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- "*"
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
paths:
|
||||||
|
- src/khoj/**
|
||||||
|
- config/**
|
||||||
|
- pyproject.toml
|
||||||
|
- prod.Dockerfile
|
||||||
|
- .github/workflows/dockerize_production.yml
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
env:
|
||||||
|
DOCKER_IMAGE_TAG: 'prod'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build Production Docker Image, Push to Container Registry
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout Code
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v2
|
||||||
|
|
||||||
|
- name: Login to GitHub Container Registry
|
||||||
|
uses: docker/login-action@v2
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.repository_owner }}
|
||||||
|
password: ${{ secrets.PAT }}
|
||||||
|
|
||||||
|
- name: 📦 Build and Push Docker Image
|
||||||
|
uses: docker/build-push-action@v2
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: prod.Dockerfile
|
||||||
|
platforms: linux/amd64
|
||||||
|
push: true
|
||||||
|
tags: ghcr.io/${{ github.repository }}:${{ env.DOCKER_IMAGE_TAG }}
|
||||||
|
build-args: |
|
||||||
|
PORT=42110
|
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -21,7 +21,7 @@ todesktop.json
|
||||||
khoj_assistant.egg-info
|
khoj_assistant.egg-info
|
||||||
/config/khoj*.yml
|
/config/khoj*.yml
|
||||||
.pytest_cache
|
.pytest_cache
|
||||||
khoj.log
|
*.log
|
||||||
static
|
static
|
||||||
|
|
||||||
# Obsidian plugin artifacts
|
# Obsidian plugin artifacts
|
||||||
|
|
|
@ -39,6 +39,7 @@ services:
|
||||||
- ./tests/data/embeddings/:/root/.khoj/content/
|
- ./tests/data/embeddings/:/root/.khoj/content/
|
||||||
- ./tests/data/models/:/root/.khoj/search/
|
- ./tests/data/models/:/root/.khoj/search/
|
||||||
- khoj_config:/root/.khoj/
|
- khoj_config:/root/.khoj/
|
||||||
|
- khoj_models:/root/.cache/torch/sentence_transformers
|
||||||
# Use 0.0.0.0 to explicitly set the host ip for the service on the container. https://pythonspeed.com/articles/docker-connection-refused/
|
# Use 0.0.0.0 to explicitly set the host ip for the service on the container. https://pythonspeed.com/articles/docker-connection-refused/
|
||||||
environment:
|
environment:
|
||||||
- POSTGRES_DB=postgres
|
- POSTGRES_DB=postgres
|
||||||
|
@ -46,9 +47,12 @@ services:
|
||||||
- POSTGRES_PASSWORD=postgres
|
- POSTGRES_PASSWORD=postgres
|
||||||
- POSTGRES_HOST=database
|
- POSTGRES_HOST=database
|
||||||
- POSTGRES_PORT=5432
|
- POSTGRES_PORT=5432
|
||||||
|
- GOOGLE_CLIENT_SECRET=bar
|
||||||
|
- GOOGLE_CLIENT_ID=foo
|
||||||
command: --host="0.0.0.0" --port=42110 -vv
|
command: --host="0.0.0.0" --port=42110 -vv
|
||||||
|
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
khoj_config:
|
khoj_config:
|
||||||
khoj_db:
|
khoj_db:
|
||||||
|
khoj_models:
|
||||||
|
|
10
gunicorn-config.py
Normal file
10
gunicorn-config.py
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import multiprocessing
|
||||||
|
|
||||||
|
bind = "0.0.0.0:42110"
|
||||||
|
workers = 4
|
||||||
|
worker_class = "uvicorn.workers.UvicornWorker"
|
||||||
|
timeout = 120
|
||||||
|
keep_alive = 60
|
||||||
|
accesslog = "access.log"
|
||||||
|
errorlog = "error.log"
|
||||||
|
loglevel = "debug"
|
30
prod.Dockerfile
Normal file
30
prod.Dockerfile
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
# Use Nvidia's latest Ubuntu 22.04 image as the base image
|
||||||
|
FROM nvidia/cuda:12.2.0-devel-ubuntu22.04
|
||||||
|
|
||||||
|
LABEL org.opencontainers.image.source https://github.com/khoj-ai/khoj
|
||||||
|
|
||||||
|
# Install System Dependencies
|
||||||
|
RUN apt update -y && apt -y install python3-pip git
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install Application
|
||||||
|
COPY pyproject.toml .
|
||||||
|
COPY README.md .
|
||||||
|
RUN sed -i 's/dynamic = \["version"\]/version = "0.0.0"/' pyproject.toml && \
|
||||||
|
TMPDIR=/home/cache/ pip install --cache-dir=/home/cache/ -e .
|
||||||
|
|
||||||
|
# Copy Source Code
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
RUN apt install vim -y
|
||||||
|
|
||||||
|
# Set the PYTHONPATH environment variable in order for it to find the Django app.
|
||||||
|
ENV PYTHONPATH=/app/src:$PYTHONPATH
|
||||||
|
|
||||||
|
# Run the Application
|
||||||
|
# There are more arguments required for the application to run,
|
||||||
|
# but these should be passed in through the docker-compose.yml file.
|
||||||
|
ARG PORT
|
||||||
|
EXPOSE ${PORT}
|
||||||
|
ENTRYPOINT [ "gunicorn", "-c", "gunicorn-config.py", "src.khoj.main:app" ]
|
|
@ -52,7 +52,7 @@ dependencies = [
|
||||||
"schedule == 1.1.0",
|
"schedule == 1.1.0",
|
||||||
"sentence-transformers == 2.2.2",
|
"sentence-transformers == 2.2.2",
|
||||||
"transformers >= 4.28.0",
|
"transformers >= 4.28.0",
|
||||||
"torch >= 2.0.1",
|
"torch == 2.0.1",
|
||||||
"uvicorn == 0.17.6",
|
"uvicorn == 0.17.6",
|
||||||
"aiohttp == 3.8.5",
|
"aiohttp == 3.8.5",
|
||||||
"langchain >= 0.0.187",
|
"langchain >= 0.0.187",
|
||||||
|
@ -70,6 +70,7 @@ dependencies = [
|
||||||
"psycopg2-binary == 2.9.9",
|
"psycopg2-binary == 2.9.9",
|
||||||
"google-auth == 2.23.3",
|
"google-auth == 2.23.3",
|
||||||
"python-multipart == 0.0.6",
|
"python-multipart == 0.0.6",
|
||||||
|
"gunicorn == 21.2.0",
|
||||||
]
|
]
|
||||||
dynamic = ["version"]
|
dynamic = ["version"]
|
||||||
|
|
||||||
|
|
|
@ -103,6 +103,9 @@ def configure_server(
|
||||||
user: KhojUser = None,
|
user: KhojUser = None,
|
||||||
):
|
):
|
||||||
# Update Config
|
# Update Config
|
||||||
|
if config == None:
|
||||||
|
logger.info(f"🚨 Khoj is not configured.\nInitializing it with a default config.")
|
||||||
|
config = FullConfig()
|
||||||
state.config = config
|
state.config = config
|
||||||
|
|
||||||
# Initialize Search Models from Config and initialize content
|
# Initialize Search Models from Config and initialize content
|
||||||
|
|
|
@ -65,7 +65,7 @@ logging.basicConfig(handlers=[rich_handler])
|
||||||
logger = logging.getLogger("khoj")
|
logger = logging.getLogger("khoj")
|
||||||
|
|
||||||
|
|
||||||
def run():
|
def run(should_start_server=True):
|
||||||
# Turn Tokenizers Parallelism Off. App does not support it.
|
# Turn Tokenizers Parallelism Off. App does not support it.
|
||||||
os.environ["TOKENIZERS_PARALLELISM"] = "false"
|
os.environ["TOKENIZERS_PARALLELISM"] = "false"
|
||||||
|
|
||||||
|
@ -107,7 +107,10 @@ def run():
|
||||||
configure_middleware(app)
|
configure_middleware(app)
|
||||||
|
|
||||||
initialize_server(args.config)
|
initialize_server(args.config)
|
||||||
start_server(app, host=args.host, port=args.port, socket=args.socket)
|
|
||||||
|
# If the server is started through gunicorn (external to the script), don't start the server
|
||||||
|
if should_start_server:
|
||||||
|
start_server(app, host=args.host, port=args.port, socket=args.socket)
|
||||||
|
|
||||||
|
|
||||||
def set_state(args):
|
def set_state(args):
|
||||||
|
@ -139,3 +142,5 @@ def poll_task_scheduler():
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
run()
|
run()
|
||||||
|
else:
|
||||||
|
run(should_start_server=False)
|
||||||
|
|
|
@ -36,16 +36,17 @@ else:
|
||||||
|
|
||||||
@auth_router.get("/login")
|
@auth_router.get("/login")
|
||||||
async def login_get(request: Request):
|
async def login_get(request: Request):
|
||||||
redirect_uri = request.url_for("auth")
|
redirect_uri = str(request.app.url_path_for("auth"))
|
||||||
return await oauth.google.authorize_redirect(request, redirect_uri)
|
return await oauth.google.authorize_redirect(request, redirect_uri)
|
||||||
|
|
||||||
|
|
||||||
@auth_router.post("/login")
|
@auth_router.post("/login")
|
||||||
async def login(request: Request):
|
async def login(request: Request):
|
||||||
redirect_uri = request.url_for("auth")
|
redirect_uri = str(request.app.url_path_for("auth"))
|
||||||
return await oauth.google.authorize_redirect(request, redirect_uri)
|
return await oauth.google.authorize_redirect(request, redirect_uri)
|
||||||
|
|
||||||
|
|
||||||
|
@auth_router.post("/redirect")
|
||||||
@auth_router.post("/token")
|
@auth_router.post("/token")
|
||||||
@requires(["authenticated"], redirect="login_page")
|
@requires(["authenticated"], redirect="login_page")
|
||||||
async def generate_token(request: Request, token_name: Optional[str] = None) -> str:
|
async def generate_token(request: Request, token_name: Optional[str] = None) -> str:
|
||||||
|
|
|
@ -3,6 +3,9 @@ import argparse
|
||||||
import pathlib
|
import pathlib
|
||||||
from importlib.metadata import version
|
from importlib.metadata import version
|
||||||
import os
|
import os
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# Internal Packages
|
# Internal Packages
|
||||||
from khoj.utils.helpers import resolve_absolute_path
|
from khoj.utils.helpers import resolve_absolute_path
|
||||||
|
@ -17,7 +20,7 @@ def cli(args=None):
|
||||||
# Setup Argument Parser for the Commandline Interface
|
# Setup Argument Parser for the Commandline Interface
|
||||||
parser = argparse.ArgumentParser(description="Start Khoj; An AI personal assistant for your Digital Brain")
|
parser = argparse.ArgumentParser(description="Start Khoj; An AI personal assistant for your Digital Brain")
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--config-file", "-c", default="~/.khoj/khoj.yml", type=pathlib.Path, help="YAML file to configure Khoj"
|
"--config-file", default="~/.khoj/khoj.yml", type=pathlib.Path, help="YAML file to configure Khoj"
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--regenerate",
|
"--regenerate",
|
||||||
|
@ -42,7 +45,9 @@ def cli(args=None):
|
||||||
help="Run Khoj in anonymous mode. This does not require any login for connecting users.",
|
help="Run Khoj in anonymous mode. This does not require any login for connecting users.",
|
||||||
)
|
)
|
||||||
|
|
||||||
args = parser.parse_args(args)
|
args, remaining_args = parser.parse_known_args(args)
|
||||||
|
|
||||||
|
logger.debug(f"Ignoring unknown commandline args: {remaining_args}")
|
||||||
|
|
||||||
args.version_no = version("khoj-assistant")
|
args.version_no = version("khoj-assistant")
|
||||||
if args.version:
|
if args.version:
|
||||||
|
|
|
@ -25,7 +25,7 @@ def test_cli_invalid_config_file_path():
|
||||||
non_existent_config_file = f"non-existent-khoj-{random()}.yml"
|
non_existent_config_file = f"non-existent-khoj-{random()}.yml"
|
||||||
|
|
||||||
# Act
|
# Act
|
||||||
actual_args = cli([f"-c={non_existent_config_file}"])
|
actual_args = cli([f"--config-file={non_existent_config_file}"])
|
||||||
|
|
||||||
# Assert
|
# Assert
|
||||||
assert actual_args.config_file == resolve_absolute_path(non_existent_config_file)
|
assert actual_args.config_file == resolve_absolute_path(non_existent_config_file)
|
||||||
|
@ -35,7 +35,7 @@ def test_cli_invalid_config_file_path():
|
||||||
# ----------------------------------------------------------------------------------------------------
|
# ----------------------------------------------------------------------------------------------------
|
||||||
def test_cli_config_from_file():
|
def test_cli_config_from_file():
|
||||||
# Act
|
# Act
|
||||||
actual_args = cli(["-c=tests/data/config.yml", "--regenerate", "-vvv"])
|
actual_args = cli(["--config-file=tests/data/config.yml", "--regenerate", "-vvv"])
|
||||||
|
|
||||||
# Assert
|
# Assert
|
||||||
assert actual_args.config_file == resolve_absolute_path(Path("tests/data/config.yml"))
|
assert actual_args.config_file == resolve_absolute_path(Path("tests/data/config.yml"))
|
||||||
|
|
|
@ -17,7 +17,6 @@ from khoj.utils.fs_syncer import collect_files, get_org_files
|
||||||
from database.models import LocalOrgConfig, KhojUser, Embeddings, GithubConfig
|
from database.models import LocalOrgConfig, KhojUser, Embeddings, GithubConfig
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
from khoj.utils.rawconfig import ContentConfig, SearchConfig
|
|
||||||
|
|
||||||
|
|
||||||
# Test
|
# Test
|
||||||
|
|
Loading…
Reference in a new issue