From 8ddea3940872f43890e1e860a253101575af9d64 Mon Sep 17 00:00:00 2001 From: Misbah Syed <35427888+misbahsy@users.noreply.github.com> Date: Tue, 29 Sep 2026 00:36:02 -0700 Subject: [PATCH 1/5] feat(docker): one-command quickstart that starts the gateway, Postgres, and the admin UI scripts/quickstart.sh downloads the quickstart compose file into ~/litellm-gateway, generates the master key, salt key, and a random Postgres password into .env, picks a free port, starts the stack, waits for it to be ready, and prints where to log in. It asks at most two questions (install folder, open the browser) and asks nothing without a terminal, under CI or Claude Code, or with --yes The compose file reads POSTGRES_PASSWORD and LITELLM_PORT from .env and falls back to the current values, so existing installs keep working unchanged --- docker/docker-compose.quickstart.yml | 6 +- scripts/quickstart.sh | 260 +++++++++++++++++++++++++++ 2 files changed, 263 insertions(+), 3 deletions(-) create mode 100755 scripts/quickstart.sh diff --git a/docker/docker-compose.quickstart.yml b/docker/docker-compose.quickstart.yml index 11631603a72..4181f7b60d5 100644 --- a/docker/docker-compose.quickstart.yml +++ b/docker/docker-compose.quickstart.yml @@ -13,11 +13,11 @@ services: litellm: image: docker.litellm.ai/berriai/litellm:main-stable ports: - - "4000:4000" + - "${LITELLM_PORT:-4000}:4000" environment: LITELLM_MASTER_KEY: ${LITELLM_MASTER_KEY:?set it in .env - see the header of this file} LITELLM_SALT_KEY: ${LITELLM_SALT_KEY:?set it in .env - see the header of this file} - DATABASE_URL: postgresql://litellm:litellm@db:5432/litellm + DATABASE_URL: postgresql://litellm:${POSTGRES_PASSWORD:-litellm}@db:5432/litellm STORE_MODEL_IN_DB: "True" depends_on: db: @@ -27,7 +27,7 @@ services: image: postgres:16 environment: POSTGRES_USER: litellm - POSTGRES_PASSWORD: litellm + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-litellm} POSTGRES_DB: litellm healthcheck: test: ["CMD-SHELL", "pg_isready -U litellm"] diff --git a/scripts/quickstart.sh b/scripts/quickstart.sh new file mode 100755 index 00000000000..44921455157 --- /dev/null +++ b/scripts/quickstart.sh @@ -0,0 +1,260 @@ +#!/bin/sh +# LiteLLM Gateway quickstart: the gateway, Postgres, and the admin UI in one command. +# curl -fsSL https://raw.githubusercontent.com/BerriAI/litellm/main/scripts/quickstart.sh | sh +# +# Asks at most two questions (where to keep the files, and whether to open the +# admin UI), each with a default you accept by pressing Enter. It asks nothing +# when there is no terminal, under CI or Claude Code, or when run with --yes. +# +# --yes, -y no questions: install to ~/litellm-gateway, don't open a browser +# LITELLM_DIR folder to install into (skips the folder question) +# LITELLM_PORT port for the gateway (default 4000, or the next free one) +# +# Keys and the database password are random (openssl rand), written only to +# .env with permissions 600, and never printed. Needs Docker with Compose v2. +# Everything runs inside main(), so a partial download runs nothing. +set -eu + +COMPOSE_URL="${LITELLM_COMPOSE_URL:-https://raw.githubusercontent.com/BerriAI/litellm/main/docker/docker-compose.quickstart.yml}" + +# ---------------------------------------------------------------- terminal + +INTERACTIVE=0 # a person is at a terminal we can ask +ARROWS=0 # that terminal supports the arrow-key menu +STTY_SAVED="" +POINTER='>' + +detect_terminal() { + # Piped from curl, stdin is the script itself, so questions go to /dev/tty. + if (exec /dev/null && [ "${TERM:-dumb}" != "dumb" ]; then + INTERACTIVE=1 + if STTY_SAVED="$(stty -g /dev/null)" && [ -n "$STTY_SAVED" ]; then + ARROWS=1 + fi + fi + case "${LC_ALL:-${LC_CTYPE:-${LANG:-}}}" in + *UTF-8* | *utf-8* | *UTF8* | *utf8*) POINTER='❯' ;; + esac +} + +restore_terminal() { + if [ -n "$STTY_SAVED" ]; then + stty "$STTY_SAVED" /dev/null || true + printf '\033[?25h' >/dev/tty 2>/dev/null || true + fi +} + +on_interrupt() { + restore_terminal + printf '\nCancelled.\n' >&2 + exit 130 +} + +read_key() { + # One keypress in raw mode. Enter comes back empty (command substitution + # drops the newline); arrows come back as "up" or "down". + k="$(dd bs=1 count=1 2>/dev/null sets CHOICE to the 1-based pick. +menu() { + question="$1" + CHOICE="$2" + shift 2 + count=$# + if [ "$INTERACTIVE" != 1 ]; then return 0; fi + + printf '\n%s\n' "$question" >/dev/tty + if [ "$ARROWS" = 1 ]; then + trap on_interrupt INT TERM + stty -icanon -echo min 1 time 0 /dev/tty + first=1 + while :; do + [ "$first" = 1 ] || printf '\033[%sA' "$count" >/dev/tty + first=0 + i=1 + for opt in "$@"; do + if [ "$i" = "$CHOICE" ]; then + printf '\033[2K \033[1;36m%s %s\033[0m\n' "$POINTER" "$opt" >/dev/tty + else + printf '\033[2K %s\n' "$opt" >/dev/tty + fi + i=$((i + 1)) + done + key="$(read_key)" + case "$key" in + up | k) [ "$CHOICE" -gt 1 ] && CHOICE=$((CHOICE - 1)) ;; + down | j) [ "$CHOICE" -lt "$count" ] && CHOICE=$((CHOICE + 1)) ;; + [1-9]) [ "$key" -le "$count" ] && CHOICE="$key" ;; + '' | "$(printf '\r')") break ;; + esac + done + restore_terminal + trap - INT TERM + else + i=1 + for opt in "$@"; do + printf ' %s) %s\n' "$i" "$opt" >/dev/tty + i=$((i + 1)) + done + printf 'Choose [%s]: ' "$CHOICE" >/dev/tty + answer="" + read -r answer .gitignore +} + +pick_port() { + saved="" + [ -f .env ] && saved="$(sed -n 's/^LITELLM_PORT=//p' .env | tail -n 1)" + if [ -n "${LITELLM_PORT:-}" ]; then + PORT="$LITELLM_PORT" + elif [ -n "$saved" ]; then + PORT="$saved" + else + PORT=4000 + while ! port_free "$PORT"; do + PORT=$((PORT + 1)) + if [ "$PORT" -gt 4099 ]; then + echo "Ports 4000 to 4099 are all in use. Set LITELLM_PORT to a free port and run this again." >&2 + exit 1 + fi + done + [ "$PORT" = 4000 ] || echo "Port 4000 is in use, so LiteLLM will use $PORT." + fi + export LITELLM_PORT="$PORT" +} + +open_browser() { + url="$1" + menu "Open the admin UI in your browser?" 1 "Yes" "No" + [ "$INTERACTIVE" = 1 ] && [ "$CHOICE" = 1 ] || return 0 + if command -v open >/dev/null 2>&1; then + open "$url" >/dev/null 2>&1 || true + elif command -v xdg-open >/dev/null 2>&1; then + xdg-open "$url" >/dev/null 2>&1 || true + fi +} + +main() { + NO_QUESTIONS=0 + for arg in "$@"; do + case "$arg" in + -y | --yes) NO_QUESTIONS=1 ;; + *) echo "Unknown option: $arg" >&2; exit 1 ;; + esac + done + + detect_terminal + # Agents and CI get the defaults even inside a terminal, so nothing waits on a keypress. + if [ "$NO_QUESTIONS" = 1 ] || [ -n "${CI:-}" ] || [ -n "${CLAUDECODE:-}" ]; then INTERACTIVE=0; fi + trap restore_terminal EXIT + + if ! command -v docker >/dev/null 2>&1; then + cat >&2 <<'EOF' +Docker is not installed. The LiteLLM Gateway runs in Docker alongside a Postgres database. + + Install Docker, then run this again: https://docs.docker.com/get-docker/ + Or deploy in one click (Railway or Render): https://docs.litellm.ai/docs/proxy/docker_quick_start + Only need to call models from Python? pip install litellm +EOF + exit 1 + fi + docker compose version >/dev/null 2>&1 || { echo "Docker Compose v2 ('docker compose') is required." >&2; exit 1; } + docker info >/dev/null 2>&1 || { echo "Docker is installed but not running. Start it and run this again." >&2; exit 1; } + command -v openssl >/dev/null 2>&1 || { echo "openssl is required to generate keys." >&2; exit 1; } + + echo "LiteLLM quickstart" + pick_folder + curl -fsSL -o docker-compose.quickstart.yml "$COMPOSE_URL" + pick_port + + if [ -f .env ]; then + echo "Reusing $DIR/.env, so existing keys and data keep working." + else + # Docker names containers and the database volume after the project, so + # an install outside the home folder gets its own name and never shares a + # database with another litellm-gateway folder. + project=litellm-gateway + [ "$DIR" = "$HOME/litellm-gateway" ] || project="litellm-gateway-$(printf '%s' "$DIR" | cksum | cut -d ' ' -f 1)" + (umask 077 && printf 'LITELLM_MASTER_KEY=sk-%s\nLITELLM_SALT_KEY=sk-%s\nPOSTGRES_PASSWORD=%s\nLITELLM_PORT=%s\nCOMPOSE_PROJECT_NAME=%s\n' \ + "$(openssl rand -hex 32)" "$(openssl rand -hex 32)" "$(openssl rand -hex 24)" "$PORT" "$project" >.env) + echo "Generated $DIR/.env with your master key, salt key, and database password. Keep this file." + fi + + # Compose prefers values already set in the shell over .env, so drop any + # inherited ones: .env stays the only source for keys and the project name. + unset LITELLM_MASTER_KEY LITELLM_SALT_KEY POSTGRES_PASSWORD COMPOSE_PROJECT_NAME + + echo "Starting LiteLLM and Postgres (the first run downloads the images)..." + docker compose -f docker-compose.quickstart.yml up -d + + i=0 + until curl -fsS "http://127.0.0.1:$PORT/health/readiness" >/dev/null 2>&1; do + i=$((i + 1)) + if [ "$i" -gt 90 ]; then + echo "The gateway did not become ready in 3 minutes. Check: cd $DIR && docker compose -f docker-compose.quickstart.yml logs litellm" >&2 + exit 1 + fi + sleep 2 + done + + echo + echo "LiteLLM is running." + echo " Admin UI: http://localhost:$PORT/ui" + echo " Username: admin" + echo " Password: the LITELLM_MASTER_KEY value in $DIR/.env" + echo " Next: in the UI, open Models + Endpoints > Add Model and paste a provider API key" + echo " Stop it: cd $DIR && docker compose -f docker-compose.quickstart.yml down" + + open_browser "http://localhost:$PORT/ui" +} + +main "$@" From 3e726bb19cd446c1a36c596bf5a06dc88352afc2 Mon Sep 17 00:00:00 2001 From: Misbah Syed <35427888+misbahsy@users.noreply.github.com> Date: Tue, 29 Sep 2026 01:21:35 -0700 Subject: [PATCH 2/5] fix(quickstart): address review findings on reinstall, ports, gitignore, and binding Stop with instructions instead of generating a new password when a database volume from an earlier install is still there, since Postgres keeps the original password Keep port 4000 for an existing .env that has no saved port, and only search for a free port on fresh installs Only write the catch-all .gitignore into a folder the script created, and warn instead of writing into a folder that already existed Add LITELLM_BIND to the compose port mapping. It is empty by default, so existing installs keep "4000:4000", and the script sets it to 127.0.0.1: so new installs listen on this machine only --- docker/docker-compose.quickstart.yml | 4 ++- scripts/quickstart.sh | 47 +++++++++++++++++++++++----- 2 files changed, 42 insertions(+), 9 deletions(-) diff --git a/docker/docker-compose.quickstart.yml b/docker/docker-compose.quickstart.yml index 4181f7b60d5..a1d47e323ff 100644 --- a/docker/docker-compose.quickstart.yml +++ b/docker/docker-compose.quickstart.yml @@ -13,7 +13,9 @@ services: litellm: image: docker.litellm.ai/berriai/litellm:main-stable ports: - - "${LITELLM_PORT:-4000}:4000" + # LITELLM_BIND is empty by default, so this stays "4000:4000". The quickstart + # script sets it to "127.0.0.1:" so new installs listen on this machine only. + - "${LITELLM_BIND:-}${LITELLM_PORT:-4000}:4000" environment: LITELLM_MASTER_KEY: ${LITELLM_MASTER_KEY:?set it in .env - see the header of this file} LITELLM_SALT_KEY: ${LITELLM_SALT_KEY:?set it in .env - see the header of this file} diff --git a/scripts/quickstart.sh b/scripts/quickstart.sh index 44921455157..b7829552515 100755 --- a/scripts/quickstart.sh +++ b/scripts/quickstart.sh @@ -10,6 +10,9 @@ # LITELLM_DIR folder to install into (skips the folder question) # LITELLM_PORT port for the gateway (default 4000, or the next free one) # +# New installs listen on this machine only (127.0.0.1). To reach the gateway +# from other machines, remove LITELLM_BIND from .env and put it behind TLS. +# # Keys and the database password are random (openssl rand), written only to # .env with permissions 600, and never printed. Needs Docker with Compose v2. # Everything runs inside main(), so a partial download runs nothing. @@ -144,11 +147,19 @@ pick_folder() { "$here_dir this folder" if [ "$CHOICE" = 2 ]; then DIR="$here_dir"; else DIR="$home_dir"; fi fi + created=0 + [ -d "$DIR" ] || created=1 mkdir -p "$DIR" cd "$DIR" DIR="$(pwd)" - # Keeps the folder out of git if it sits inside a repository. - [ -f .gitignore ] || printf '*\n' >.gitignore + if [ "$created" = 1 ]; then + # A folder this script made holds only its own files, so keep all of it out of git. + printf '*\n' >.gitignore + elif command -v git >/dev/null 2>&1 && git rev-parse --is-inside-work-tree >/dev/null 2>&1 && + ! git check-ignore -q .env 2>/dev/null; then + # Never write ignore rules into a folder that already existed, such as a repository root. + echo "Note: $DIR/.env will hold your keys and is not ignored by git. Add .env to your .gitignore." + fi } pick_port() { @@ -158,6 +169,9 @@ pick_port() { PORT="$LITELLM_PORT" elif [ -n "$saved" ]; then PORT="$saved" + elif [ -f .env ]; then + # An existing install without a saved port runs on the compose default. + PORT=4000 else PORT=4000 while ! port_free "$PORT"; do @@ -172,6 +186,27 @@ pick_port() { export LITELLM_PORT="$PORT" } +# Docker names containers and the database volume after the project, so an +# install outside the home folder gets its own name and never shares a +# database with another litellm-gateway folder. +check_new_install() { + project=litellm-gateway + [ "$DIR" = "$HOME/litellm-gateway" ] || project="litellm-gateway-$(printf '%s' "$DIR" | cksum | cut -d ' ' -f 1)" + # Postgres keeps the password it was created with, so a new password over an + # old database volume would lock the gateway out. Stop and explain instead. + if docker volume inspect "${project}_postgres_data" >/dev/null 2>&1; then + cat >&2 <.env) echo "Generated $DIR/.env with your master key, salt key, and database password. Keep this file." fi From 06ad304286e900a8de8745f658dd0200db9643bf Mon Sep 17 00:00:00 2001 From: Misbah Syed <35427888+misbahsy@users.noreply.github.com> Date: Tue, 29 Sep 2026 01:26:21 -0700 Subject: [PATCH 3/5] fix(quickstart): keep generated .env out of git in an existing repository folder When LITELLM_DIR is inside a git repository that does not ignore .env, add //.env to the clone's local exclude list (.git/info/exclude) instead of only warning. Tracked files, including .gitignore, are not touched --- scripts/quickstart.sh | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/scripts/quickstart.sh b/scripts/quickstart.sh index b7829552515..3b2bef209f7 100755 --- a/scripts/quickstart.sh +++ b/scripts/quickstart.sh @@ -157,8 +157,14 @@ pick_folder() { printf '*\n' >.gitignore elif command -v git >/dev/null 2>&1 && git rev-parse --is-inside-work-tree >/dev/null 2>&1 && ! git check-ignore -q .env 2>/dev/null; then - # Never write ignore rules into a folder that already existed, such as a repository root. - echo "Note: $DIR/.env will hold your keys and is not ignored by git. Add .env to your .gitignore." + # In a folder that already existed, such as a repository root, leave the + # tracked .gitignore alone and add only .env to this clone's local exclude + # list, so the generated keys cannot be committed. + exclude="$(git rev-parse --git-path info/exclude)" + mkdir -p "$(dirname "$exclude")" + exclude="$(cd "$(dirname "$exclude")" && pwd)/exclude" + printf '/%s.env\n' "$(git rev-parse --show-prefix)" >>"$exclude" + echo "Added .env to this repository's local git exclude list ($exclude), so your keys stay out of commits." fi } From e6132b4d334c46a74d87e0b78f1fb1445a347029 Mon Sep 17 00:00:00 2001 From: Misbah Syed <35427888+misbahsy@users.noreply.github.com> Date: Tue, 29 Sep 2026 02:15:21 -0700 Subject: [PATCH 4/5] fix(quickstart): ignore an inherited LITELLM_BIND and keep .env ignored outside git Clear LITELLM_BIND from the environment before starting Compose, like the keys and project name, so .env decides the bind address and new installs stay on 127.0.0.1 In an existing folder that is not inside a git repository, add a single .env line to its .gitignore (creating it if needed, without a duplicate), so the keys stay out of commits if the folder later becomes a repository --- scripts/quickstart.sh | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/scripts/quickstart.sh b/scripts/quickstart.sh index 3b2bef209f7..c8ca325b6a5 100755 --- a/scripts/quickstart.sh +++ b/scripts/quickstart.sh @@ -165,6 +165,14 @@ pick_folder() { exclude="$(cd "$(dirname "$exclude")" && pwd)/exclude" printf '/%s.env\n' "$(git rev-parse --show-prefix)" >>"$exclude" echo "Added .env to this repository's local git exclude list ($exclude), so your keys stay out of commits." + elif ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then + # An existing folder outside git: ignore only .env, so it stays out of + # commits if the folder becomes a repository later. + if ! grep -qxF '.env' .gitignore 2>/dev/null; then + # Start on a new line if the file does not end with one. + if [ -s .gitignore ] && [ -n "$(tail -c 1 .gitignore)" ]; then printf '\n' >>.gitignore; fi + printf '.env\n' >>.gitignore + fi fi } @@ -267,8 +275,9 @@ EOF fi # Compose prefers values already set in the shell over .env, so drop any - # inherited ones: .env stays the only source for keys and the project name. - unset LITELLM_MASTER_KEY LITELLM_SALT_KEY POSTGRES_PASSWORD COMPOSE_PROJECT_NAME + # inherited ones: .env stays the only source for keys, the bind address, + # and the project name. + unset LITELLM_MASTER_KEY LITELLM_SALT_KEY POSTGRES_PASSWORD COMPOSE_PROJECT_NAME LITELLM_BIND echo "Starting LiteLLM and Postgres (the first run downloads the images)..." docker compose -f docker-compose.quickstart.yml up -d From f38521398c377c922bac55c18baa6b4549e90239 Mon Sep 17 00:00:00 2001 From: Misbah Syed <35427888+misbahsy@users.noreply.github.com> Date: Tue, 29 Sep 2026 09:04:12 -0700 Subject: [PATCH 5/5] fix(quickstart): keep an exported LITELLM_BIND for an .env without one, and show a read-first install The bind address now follows .env only when .env sets it, which every install this script creates does. For an older .env without a bind line, a LITELLM_BIND exported in the shell is kept, so an intentional 127.0.0.1: is not dropped The header shows how to download and read the script before running it --- scripts/quickstart.sh | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/scripts/quickstart.sh b/scripts/quickstart.sh index c8ca325b6a5..469f8f2a37f 100755 --- a/scripts/quickstart.sh +++ b/scripts/quickstart.sh @@ -2,6 +2,11 @@ # LiteLLM Gateway quickstart: the gateway, Postgres, and the admin UI in one command. # curl -fsSL https://raw.githubusercontent.com/BerriAI/litellm/main/scripts/quickstart.sh | sh # +# To read it before running it: +# curl -fsSL https://raw.githubusercontent.com/BerriAI/litellm/main/scripts/quickstart.sh -o quickstart.sh +# less quickstart.sh +# sh quickstart.sh +# # Asks at most two questions (where to keep the files, and whether to open the # admin UI), each with a default you accept by pressing Enter. It asks nothing # when there is no terminal, under CI or Claude Code, or when run with --yes. @@ -275,9 +280,12 @@ EOF fi # Compose prefers values already set in the shell over .env, so drop any - # inherited ones: .env stays the only source for keys, the bind address, - # and the project name. - unset LITELLM_MASTER_KEY LITELLM_SALT_KEY POSTGRES_PASSWORD COMPOSE_PROJECT_NAME LITELLM_BIND + # inherited ones: .env stays the only source for keys and the project name. + unset LITELLM_MASTER_KEY LITELLM_SALT_KEY POSTGRES_PASSWORD COMPOSE_PROJECT_NAME + # The bind address follows .env when .env sets it (every install this script + # creates does). For an older .env without it, a value exported in the shell + # is kept, so an intentional LITELLM_BIND=127.0.0.1: is not dropped. + if grep -q '^LITELLM_BIND=' .env; then unset LITELLM_BIND; fi echo "Starting LiteLLM and Postgres (the first run downloads the images)..." docker compose -f docker-compose.quickstart.yml up -d