|
| 1 | +#!/usr/bin/env bash |
| 2 | +set -euo pipefail |
| 3 | + |
| 4 | +APP_ROOT="/root/cross-subject-knowledge" |
| 5 | +CONTAINER_NAME="textbook-knowledge" |
| 6 | +REPO_NAME="textbook-knowledge" |
| 7 | +EMBEDDER_NAME="${EMBEDDER_NAME:-BAAI/bge-m3}" |
| 8 | + |
| 9 | +cd "$APP_ROOT" |
| 10 | + |
| 11 | +mkdir -p \ |
| 12 | + data/index \ |
| 13 | + state/logs \ |
| 14 | + state/cache/huggingface \ |
| 15 | + state/cache/sentence_transformers \ |
| 16 | + state/tmp \ |
| 17 | + state/batch |
| 18 | + |
| 19 | +if [ ! -f data/index/textbook_mineru_fts.db ]; then |
| 20 | + echo "ERROR: missing runtime DB data/index/textbook_mineru_fts.db" |
| 21 | + exit 1 |
| 22 | +fi |
| 23 | + |
| 24 | +if [ ! -f data/index/textbook_chunks.index ]; then |
| 25 | + echo "ERROR: missing runtime FAISS index data/index/textbook_chunks.index" |
| 26 | + exit 1 |
| 27 | +fi |
| 28 | + |
| 29 | +commit_sha="$(git rev-parse --short HEAD)" |
| 30 | +build_stamp="$(date +%Y%m%d_%H%M%S)" |
| 31 | +build_tag="${REPO_NAME}:build-${commit_sha}-${build_stamp}" |
| 32 | +backup_tag="${REPO_NAME}:pre-${build_stamp}" |
| 33 | +rollback_image="" |
| 34 | + |
| 35 | +host_cache_has_model() { |
| 36 | + find "${APP_ROOT}/state/cache/huggingface" -type f | grep -q . |
| 37 | +} |
| 38 | + |
| 39 | +echo "=== deploy start $(date -u '+%Y-%m-%d %H:%M:%S UTC') commit=${commit_sha} ===" |
| 40 | +df -h / |
| 41 | + |
| 42 | +if docker ps -a --format '{{.Names}}' | grep -qx "${CONTAINER_NAME}"; then |
| 43 | + rollback_image="$(docker inspect --format '{{.Image}}' "${CONTAINER_NAME}")" |
| 44 | +fi |
| 45 | + |
| 46 | +if docker image inspect "${REPO_NAME}:latest" >/dev/null 2>&1; then |
| 47 | + docker tag "${REPO_NAME}:latest" "${backup_tag}" || true |
| 48 | +fi |
| 49 | + |
| 50 | +export DOCKER_BUILDKIT=1 |
| 51 | +docker build --pull -t "${build_tag}" -t "${REPO_NAME}:latest" . |
| 52 | + |
| 53 | +if ! host_cache_has_model; then |
| 54 | + echo "=== bootstrapping host model cache ===" |
| 55 | + if docker ps -a --format '{{.Names}}' | grep -qx "${CONTAINER_NAME}"; then |
| 56 | + docker cp "${CONTAINER_NAME}:/root/.cache/huggingface/." "${APP_ROOT}/state/cache/huggingface/" >/dev/null 2>&1 || true |
| 57 | + fi |
| 58 | +fi |
| 59 | + |
| 60 | +if ! host_cache_has_model; then |
| 61 | + echo "=== warming model cache with ${EMBEDDER_NAME} ===" |
| 62 | + docker run --rm \ |
| 63 | + -e HF_HOME=/state/cache/huggingface \ |
| 64 | + -e SENTENCE_TRANSFORMERS_HOME=/state/cache/sentence_transformers \ |
| 65 | + -e TRANSFORMERS_CACHE=/state/cache/huggingface/transformers \ |
| 66 | + -v "${APP_ROOT}/state:/state" \ |
| 67 | + "${build_tag}" \ |
| 68 | + python -c "from sentence_transformers import SentenceTransformer; SentenceTransformer('${EMBEDDER_NAME}')" |
| 69 | +fi |
| 70 | + |
| 71 | +docker stop "${CONTAINER_NAME}" >/dev/null 2>&1 || true |
| 72 | +docker rm "${CONTAINER_NAME}" >/dev/null 2>&1 || true |
| 73 | + |
| 74 | +docker run -d \ |
| 75 | + --name "${CONTAINER_NAME}" \ |
| 76 | + --restart unless-stopped \ |
| 77 | + -p 8080:8080 \ |
| 78 | + -e PROJECT_ROOT=/app \ |
| 79 | + -e DATA_ROOT=/data \ |
| 80 | + -e STATE_ROOT=/state \ |
| 81 | + -e HF_HOME=/state/cache/huggingface \ |
| 82 | + -e SENTENCE_TRANSFORMERS_HOME=/state/cache/sentence_transformers \ |
| 83 | + -e TRANSFORMERS_CACHE=/state/cache/huggingface/transformers \ |
| 84 | + -v "${APP_ROOT}/data:/data" \ |
| 85 | + -v "${APP_ROOT}/state:/state" \ |
| 86 | + "${build_tag}" >/dev/null |
| 87 | + |
| 88 | +healthy="false" |
| 89 | +for _ in $(seq 1 24); do |
| 90 | + status="$(docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' "${CONTAINER_NAME}" 2>/dev/null || echo "missing")" |
| 91 | + if [ "${status}" = "healthy" ] || [ "${status}" = "running" ]; then |
| 92 | + if curl -sf http://127.0.0.1:8080/api/health >/tmp/textbook_health.json; then |
| 93 | + healthy="true" |
| 94 | + break |
| 95 | + fi |
| 96 | + fi |
| 97 | + sleep 5 |
| 98 | +done |
| 99 | + |
| 100 | +if [ "${healthy}" != "true" ]; then |
| 101 | + echo "ERROR: new container failed health checks" |
| 102 | + docker logs --tail 200 "${CONTAINER_NAME}" || true |
| 103 | + docker stop "${CONTAINER_NAME}" >/dev/null 2>&1 || true |
| 104 | + docker rm "${CONTAINER_NAME}" >/dev/null 2>&1 || true |
| 105 | + if [ -n "${rollback_image}" ]; then |
| 106 | + echo "=== rolling back to previous image ${rollback_image} ===" |
| 107 | + docker run -d \ |
| 108 | + --name "${CONTAINER_NAME}" \ |
| 109 | + --restart unless-stopped \ |
| 110 | + -p 8080:8080 \ |
| 111 | + -e PROJECT_ROOT=/app \ |
| 112 | + -e DATA_ROOT=/data \ |
| 113 | + -e STATE_ROOT=/state \ |
| 114 | + -e HF_HOME=/state/cache/huggingface \ |
| 115 | + -e SENTENCE_TRANSFORMERS_HOME=/state/cache/sentence_transformers \ |
| 116 | + -e TRANSFORMERS_CACHE=/state/cache/huggingface/transformers \ |
| 117 | + -v "${APP_ROOT}/data:/data" \ |
| 118 | + -v "${APP_ROOT}/state:/state" \ |
| 119 | + "${rollback_image}" >/dev/null |
| 120 | + fi |
| 121 | + exit 1 |
| 122 | +fi |
| 123 | + |
| 124 | +echo "=== health ===" |
| 125 | +cat /tmp/textbook_health.json |
| 126 | +echo |
| 127 | + |
| 128 | +docker image prune -f >/dev/null 2>&1 || true |
| 129 | + |
| 130 | +old_pre_tags="$(docker images --format '{{.Repository}}:{{.Tag}}' | grep '^textbook-knowledge:pre-' | sort -r | tail -n +4 || true)" |
| 131 | +if [ -n "${old_pre_tags}" ]; then |
| 132 | + while IFS= read -r image_ref; do |
| 133 | + [ -n "${image_ref}" ] || continue |
| 134 | + docker rmi "${image_ref}" >/dev/null 2>&1 || true |
| 135 | + done <<< "${old_pre_tags}" |
| 136 | +fi |
| 137 | + |
| 138 | +echo "=== disk after deploy ===" |
| 139 | +df -h / |
| 140 | +docker system df |
| 141 | +echo "=== deploy ok $(date -u '+%Y-%m-%d %H:%M:%S UTC') tag=${build_tag} ===" |
0 commit comments