Hostityourself/builder/build.sh
Claude 944feb39ec
fix: pass PORT env var to app container
Apps follow the Heroku convention of binding to $PORT at runtime.
Without --env PORT=$PORT, containers use their default port which
doesn't match what Caddy is configured to dial, causing 502s.
2026-03-19 11:34:33 +00:00

185 lines
7.3 KiB
Bash
Executable file

#!/usr/bin/env bash
# HIY Build Engine
# Environment variables injected by hiy-server:
# APP_ID, APP_NAME, REPO_URL, BRANCH, PORT, ENV_FILE, SHA, BUILD_DIR
set -euo pipefail
log() { echo "[hiy] $*"; }
log "=== HostItYourself Build Engine ==="
log "App: $APP_NAME ($APP_ID)"
log "Repo: $REPO_URL"
log "Branch: $BRANCH"
log "Build dir: $BUILD_DIR"
# ── 1. Clone or pull ───────────────────────────────────────────────────────────
mkdir -p "$BUILD_DIR"
cd "$BUILD_DIR"
if [ -d ".git" ]; then
log "Updating existing clone…"
git fetch origin "$BRANCH" --depth=50
git checkout "$BRANCH"
git reset --hard "origin/$BRANCH"
else
log "Cloning repository…"
git clone --depth=50 --branch "$BRANCH" "$REPO_URL" .
fi
ACTUAL_SHA=$(git rev-parse HEAD)
log "SHA: $ACTUAL_SHA"
# ── 2. Detect build strategy ──────────────────────────────────────────────────
IMAGE_TAG="hiy/${APP_ID}:${ACTUAL_SHA}"
CONTAINER_NAME="hiy-${APP_ID}"
if [ -f "Dockerfile" ]; then
log "Strategy: Dockerfile"
docker build --tag "$IMAGE_TAG" .
elif [ -f "package.json" ] || [ -f "yarn.lock" ]; then
log "Strategy: Node.js (Cloud Native Buildpack)"
if ! command -v pack &>/dev/null; then
log "ERROR: 'pack' CLI not found. Install it: https://buildpacks.io/docs/tools/pack/"
exit 1
fi
pack build "$IMAGE_TAG" --builder paketobuildpacks/builder-jammy-base
elif [ -f "requirements.txt" ] || [ -f "pyproject.toml" ]; then
log "Strategy: Python (Cloud Native Buildpack)"
if ! command -v pack &>/dev/null; then
log "ERROR: 'pack' CLI not found. Install it: https://buildpacks.io/docs/tools/pack/"
exit 1
fi
pack build "$IMAGE_TAG" --builder paketobuildpacks/builder-jammy-base
elif [ -f "go.mod" ]; then
log "Strategy: Go (Cloud Native Buildpack)"
if ! command -v pack &>/dev/null; then
log "ERROR: 'pack' CLI not found. Install it: https://buildpacks.io/docs/tools/pack/"
exit 1
fi
pack build "$IMAGE_TAG" --builder paketobuildpacks/builder-jammy-base
elif [ -d "static" ] || [ -d "public" ]; then
STATIC_DIR="static"
[ -d "public" ] && STATIC_DIR="public"
log "Strategy: Static files (Caddy) from ./$STATIC_DIR"
cat > Dockerfile.hiy <<EOF
FROM caddy:2-alpine
COPY $STATIC_DIR /srv
EOF
docker build --file Dockerfile.hiy --tag "$IMAGE_TAG" .
rm -f Dockerfile.hiy
else
log "ERROR: Could not detect build strategy."
log "Add a Dockerfile, package.json, requirements.txt, go.mod, or a static/ directory."
exit 1
fi
# ── 3. Ensure Docker network ───────────────────────────────────────────────────
docker network create hiy-net 2>/dev/null || true
# ── 4. Stop & remove previous container ───────────────────────────────────────
if docker ps -a --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then
log "Stopping old container…"
docker stop "$CONTAINER_NAME" >/dev/null 2>&1 || true
docker rm "$CONTAINER_NAME" >/dev/null 2>&1 || true
fi
# ── 5. Start new container ────────────────────────────────────────────────────
log "Starting container ${CONTAINER_NAME}"
ENV_FILE_ARG=()
if [ -n "${ENV_FILE:-}" ] && [ -f "$ENV_FILE" ]; then
ENV_FILE_ARG=(--env-file "$ENV_FILE")
else
log "No env file found at '${ENV_FILE:-}'; starting without one."
fi
docker run --detach \
--name "$CONTAINER_NAME" \
--network hiy-net \
"${ENV_FILE_ARG[@]+"${ENV_FILE_ARG[@]}"}" \
--env "PORT=${PORT}" \
--expose "$PORT" \
--label "hiy.app=${APP_ID}" \
--label "hiy.port=${PORT}" \
--restart unless-stopped \
--memory="512m" \
--cpus="0.5" \
"$IMAGE_TAG"
# ── 6. Update Caddy via its admin API ─────────────────────────────────────────
CADDY_API="${CADDY_API_URL:-http://localhost:2019}"
DOMAIN_SUFFIX="${DOMAIN_SUFFIX:-localhost}"
if curl --silent --fail "${CADDY_API}/config/" >/dev/null 2>&1; then
CONTAINER_IP=$(docker inspect "$CONTAINER_NAME" \
--format '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}')
UPSTREAM="${CONTAINER_IP}:${PORT}"
log "Updating Caddy: ${APP_ID}.${DOMAIN_SUFFIX}${UPSTREAM}"
ROUTE_JSON=$(cat <<EOF
{
"match": [{"host": ["${APP_ID}.${DOMAIN_SUFFIX}"]}],
"handle": [{"handler": "reverse_proxy", "upstreams": [{"dial": "${UPSTREAM}"}]}]
}
EOF
)
# Upsert the route for this app.
ROUTES=$(curl --silent --fail "${CADDY_API}/config/apps/http/servers/hiy/routes" 2>/dev/null || echo "[]")
# Remove existing route for the same host, rebuild list, keep dashboard as catch-all.
UPDATED=$(echo "$ROUTES" | python3 -c "
import sys, json
try:
routes = json.loads(sys.stdin.read())
if not isinstance(routes, list):
routes = []
except Exception:
routes = []
DASHBOARD = {'handle': [{'handler': 'reverse_proxy', 'upstreams': [{'dial': 'server:3000'}]}]}
new_host = '${APP_ID}.${DOMAIN_SUFFIX}'
# Keep host-matched routes that are NOT for this app
routes = [r for r in routes
if r.get('match') and not any(
isinstance(m, dict) and new_host in m.get('host', [])
for m in r.get('match', []))]
routes.insert(0, json.loads(sys.argv[1]))
routes.append(DASHBOARD)
print(json.dumps(routes))
" "$ROUTE_JSON")
log "Upstream: ${UPSTREAM}"
log "Routes JSON: ${UPDATED}"
set +e
CADDY_RESP=$(curl --silent --show-error \
--write-out "\nHTTP_STATUS:%{http_code}" \
"${CADDY_API}/config/apps/http/servers/hiy/routes" \
--header "Content-Type: application/json" \
--request PATCH \
--data "$UPDATED" 2>&1)
set -e
if echo "$CADDY_RESP" | grep -q "HTTP_STATUS:2"; then
log "Caddy updated."
else
log "WARNING: Caddy update failed (app is running; fix routing manually)."
log "Caddy response: ${CADDY_RESP}"
fi
else
log "Caddy admin API not reachable; skipping route update."
log "Container ${CONTAINER_NAME} is running on port ${PORT} but not publicly routed."
log "To reach it directly, re-run the container with a published port:"
log " docker rm -f ${CONTAINER_NAME}"
log " docker run -d --name ${CONTAINER_NAME} -p ${PORT}:${PORT} ${IMAGE_TAG}"
log "Or point any reverse proxy at: \$(docker inspect ${CONTAINER_NAME} --format '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}'):${PORT}"
fi
# ── 7. Prune old images ───────────────────────────────────────────────────────
log "Pruning old images (keeping last 3)…"
docker images "hiy/${APP_ID}" --format "{{.ID}}\t{{.CreatedAt}}" \
| sort --reverse --key=2 \
| tail -n +4 \
| awk '{print $1}' \
| xargs --no-run-if-empty docker rmi 2>/dev/null || true
log "=== Build complete: $APP_NAME @ $ACTUAL_SHA ==="