File size: 5,748 Bytes
077865a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
#!/usr/bin/env bash
#
# FreeLLMAPI one-line installer (#250, idea by @StealthTensor).
#
#   curl -fsSL https://tashfeenahmed.github.io/freellmapi/install.sh | bash
#
# What it does:
#   1. Checks for Docker + Compose.
#   2. Creates an install dir (default ~/freellmapi, override FREELLMAPI_DIR).
#   3. Writes a docker-compose.yml pinned to ghcr.io/tashfeenahmed/freellmapi:latest
#      and a .env with a freshly generated ENCRYPTION_KEY (kept on re-runs).
#   4. Pulls the image, starts the container, waits for the health endpoint.
#
# Options (env vars):
#   FREELLMAPI_DIR   install directory            (default: ~/freellmapi)
#   PORT             host port for the dashboard  (default: 3001)
#   HOST_BIND        host interface to publish on (default: 127.0.0.1).
#                    Set HOST_BIND=0.0.0.0 to reach it from your LAN β€” only on
#                    a trusted network; the proxy is single-user.
#
# Re-running is safe: existing .env (and your encryption key) is preserved,
# the compose file is refreshed, and the container is updated to :latest.

set -euo pipefail

INSTALL_DIR="${FREELLMAPI_DIR:-$HOME/freellmapi}"
PORT="${PORT:-3001}"
HOST_BIND="${HOST_BIND:-127.0.0.1}"
IMAGE="ghcr.io/tashfeenahmed/freellmapi:latest"

say()  { printf '\033[1;32m==>\033[0m %s\n' "$*"; }
warn() { printf '\033[1;33mWARN\033[0m %s\n' "$*" >&2; }
die()  { printf '\033[1;31mERROR\033[0m %s\n' "$*" >&2; exit 1; }

# ── 1. prerequisites ─────────────────────────────────────────────────────────
command -v docker >/dev/null 2>&1 || die "Docker is required. Install it from https://docs.docker.com/get-docker/ and re-run."

if docker compose version >/dev/null 2>&1; then
  COMPOSE=(docker compose)
elif command -v docker-compose >/dev/null 2>&1; then
  COMPOSE=(docker-compose)
else
  die "Docker Compose is required (the 'docker compose' plugin or 'docker-compose')."
fi

docker info >/dev/null 2>&1 || die "Docker daemon isn't running (or you lack permission). Start Docker and re-run."

# ── 2. install dir ───────────────────────────────────────────────────────────
mkdir -p "$INSTALL_DIR"
cd "$INSTALL_DIR"
say "Installing into $INSTALL_DIR"

# ── 3. .env (generated once β€” your at-rest encryption key lives here) ────────
if [ -f .env ]; then
  say "Keeping existing .env (encryption key preserved)"
else
  if command -v openssl >/dev/null 2>&1; then
    KEY="$(openssl rand -hex 32)"
  else
    # /dev/urandom fallback when openssl isn't installed
    KEY="$(od -vN 32 -An -tx1 /dev/urandom | tr -d ' \n')"
  fi
  umask 077
  cat > .env <<EOF
# Generated by install.sh on $(date -u +%Y-%m-%dT%H:%M:%SZ)
# At-rest encryption key for your stored provider keys. Losing it means
# re-entering your provider keys, so back this file up.
ENCRYPTION_KEY=$KEY
PORT=$PORT
HOST_BIND=$HOST_BIND
EOF
  umask 022
  say "Wrote .env with a fresh ENCRYPTION_KEY"
fi

# ── 4. compose file (image-only; refreshed on every run) ─────────────────────
cat > docker-compose.yml <<EOF
# Written by install.sh β€” safe to edit; re-running the installer rewrites it.
services:
  freellmapi:
    image: $IMAGE
    env_file:
      - .env
    environment:
      NODE_ENV: production
      PORT: 3001
    # Bound to localhost by default β€” FreeLLMAPI is single-user and must not
    # be exposed to the internet. Set HOST_BIND=0.0.0.0 in .env for LAN access.
    ports:
      - "\${HOST_BIND:-127.0.0.1}:\${PORT:-3001}:3001"
    volumes:
      - freellmapi-data:/app/server/data
    restart: unless-stopped
    healthcheck:
      test:
        [
          "CMD",
          "node",
          "-e",
          "fetch('http://127.0.0.1:3001/api/ping').then((res) => { if (!res.ok) process.exit(1); }).catch(() => process.exit(1));"
        ]
      interval: 30s
      timeout: 5s
      start_period: 15s
      retries: 3

volumes:
  freellmapi-data:
EOF

# ── 5. pull + start ──────────────────────────────────────────────────────────
say "Pulling $IMAGE"
"${COMPOSE[@]}" pull --quiet 2>/dev/null || "${COMPOSE[@]}" pull
say "Starting FreeLLMAPI"
"${COMPOSE[@]}" up -d

# ── 6. wait for health ───────────────────────────────────────────────────────
say "Waiting for the server to come up"
HEALTH_HOST="127.0.0.1"
[ "$HOST_BIND" != "0.0.0.0" ] && [ "$HOST_BIND" != "127.0.0.1" ] && HEALTH_HOST="$HOST_BIND"
for i in $(seq 1 30); do
  if curl -fsS "http://$HEALTH_HOST:$PORT/api/ping" >/dev/null 2>&1; then
    HEALTHY=1
    break
  fi
  sleep 2
done

if [ "${HEALTHY:-0}" = "1" ]; then
  cat <<EOF

  FreeLLMAPI is running.

    Dashboard      http://localhost:$PORT
    Next steps     add provider keys on the Keys page, then grab your
                   unified API key from the page header β€” that's the
                   Bearer token your OpenAI-compatible client uses.
    API endpoint   http://localhost:$PORT/v1

    Update         cd $INSTALL_DIR && docker compose pull && docker compose up -d
    Logs           cd $INSTALL_DIR && docker compose logs -f
    Uninstall      cd $INSTALL_DIR && docker compose down -v && cd .. && rm -rf $INSTALL_DIR

EOF
else
  warn "Container started but the health endpoint didn't answer within 60s."
  warn "Check logs with: cd $INSTALL_DIR && ${COMPOSE[*]} logs -f"
  exit 1
fi