mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-04 02:31:27 +00:00
feat(docker): one-command quickstart that starts the gateway, Postgres, and the admin UI (#43673)
* 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 * 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 * 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 /<path>/.env to the clone's local exclude list (.git/info/exclude) instead of only warning. Tracked files, including .gitignore, are not touched * 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 * 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
This commit is contained in:
parent
276fc9c63a
commit
a7eb4bad81
2 changed files with 319 additions and 3 deletions
|
|
@ -13,11 +13,13 @@ services:
|
|||
litellm:
|
||||
image: docker.litellm.ai/berriai/litellm:main-stable
|
||||
ports:
|
||||
- "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}
|
||||
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 +29,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"]
|
||||
|
|
|
|||
314
scripts/quickstart.sh
Executable file
314
scripts/quickstart.sh
Executable file
|
|
@ -0,0 +1,314 @@
|
|||
#!/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
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
# --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)
|
||||
#
|
||||
# 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.
|
||||
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/tty) 2>/dev/null && [ "${TERM:-dumb}" != "dumb" ]; then
|
||||
INTERACTIVE=1
|
||||
if STTY_SAVED="$(stty -g </dev/tty 2>/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/tty 2>/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 </dev/tty)"
|
||||
if [ "$k" = "$(printf '\033')" ]; then
|
||||
rest="$(dd bs=1 count=2 2>/dev/null </dev/tty)"
|
||||
case "$rest" in
|
||||
'[A' | 'OA') k=up ;;
|
||||
'[B' | 'OB') k=down ;;
|
||||
*) k=other ;;
|
||||
esac
|
||||
fi
|
||||
printf '%s' "$k"
|
||||
}
|
||||
|
||||
# menu "Question" DEFAULT OPTION... -> 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
|
||||
printf '\033[?25l' >/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 </dev/tty || answer=""
|
||||
case "$answer" in
|
||||
'' | *[!0-9]*) ;;
|
||||
*) [ "$answer" -ge 1 ] && [ "$answer" -le "$count" ] && CHOICE="$answer" ;;
|
||||
esac
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------- steps
|
||||
|
||||
port_free() {
|
||||
# curl exits 7 when nothing accepts the connection.
|
||||
rc=0
|
||||
curl -s -o /dev/null --max-time 2 "http://127.0.0.1:$1/" || rc=$?
|
||||
[ "$rc" = 7 ]
|
||||
}
|
||||
|
||||
pick_folder() {
|
||||
home_dir="$HOME/litellm-gateway"
|
||||
here_dir="$(pwd)/litellm-gateway"
|
||||
if [ -n "${LITELLM_DIR:-}" ]; then
|
||||
DIR="$LITELLM_DIR"
|
||||
elif [ -f "$here_dir/.env" ]; then
|
||||
DIR="$here_dir" # installed in this folder before
|
||||
elif [ -f "$home_dir/.env" ]; then
|
||||
DIR="$home_dir" # installed in the home folder before
|
||||
elif [ "$here_dir" = "$home_dir" ]; then
|
||||
DIR="$home_dir"
|
||||
else
|
||||
menu "Where should LiteLLM keep its files (.env with your keys, and the compose file)?" 1 \
|
||||
"$home_dir recommended, reruns always find it" \
|
||||
"$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)"
|
||||
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
|
||||
# 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."
|
||||
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
|
||||
}
|
||||
|
||||
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"
|
||||
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
|
||||
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"
|
||||
}
|
||||
|
||||
# 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 <<EOF
|
||||
Found a database from an earlier install (Docker volume ${project}_postgres_data)
|
||||
but no $DIR/.env with its password.
|
||||
|
||||
Restore that .env file and run this again to keep your models and keys, or
|
||||
delete the old database and start fresh (this removes its models and keys):
|
||||
docker volume rm ${project}_postgres_data
|
||||
EOF
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
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
|
||||
[ -f .env ] || check_new_install
|
||||
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
|
||||
(umask 077 && printf 'LITELLM_MASTER_KEY=sk-%s\nLITELLM_SALT_KEY=sk-%s\nPOSTGRES_PASSWORD=%s\nLITELLM_PORT=%s\nLITELLM_BIND=127.0.0.1:\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
|
||||
# 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
|
||||
|
||||
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 "$@"
|
||||
Loading…
Add table
Reference in a new issue