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
This commit is contained in:
Misbah Syed 2026-09-29 00:36:02 -07:00
parent 85dc7cb62e
commit 8ddea39408
2 changed files with 263 additions and 3 deletions

View file

@ -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"]

260
scripts/quickstart.sh Executable file
View file

@ -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/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
mkdir -p "$DIR"
cd "$DIR"
DIR="$(pwd)"
# Keeps the folder out of git if it sits inside a repository.
[ -f .gitignore ] || printf '*\n' >.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 "$@"