Skip to content

Commit 22c0fd5

Browse files
committed
Split llama-stack container build into deps and app layers for faster rebuilds
Separate the monolithic container image into a deps base image (test.deps.containerfile) that installs system packages and Python dependencies, and a thin app image (test.containerfile) that only copies source code on top. The Makefile tracks a hash of pyproject.toml and uv.lock to skip rebuilding the deps image when dependencies haven't changed, making source-only iterations significantly faster. Remove existing images by name before building replacements to prevent dangling <none>:<none> images from accumulating and wasting disk space.
1 parent 7a44438 commit 22c0fd5

3 files changed

Lines changed: 85 additions & 15 deletions

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,9 @@ requirements.*.backup
195195
# Local run files
196196
local-run.yaml
197197

198+
# Deps image hash (auto-generated by make)
199+
.llama-stack-deps.hash
200+
198201
# Sisyphus planning files
199202
.sisyphus/
200203
# Per-developer feature design overrides (see docs/contributing/feature-design.config)

Makefile

Lines changed: 49 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,17 @@ LLAMA_STACK_CONFIG ?= run.yaml
1313

1414
# Container configuration
1515
LLAMA_STACK_CONTAINER_NAME ?= lightspeed-llama-stack
16+
LLAMA_STACK_DEPS_IMAGE ?= lightspeed-llama-stack-deps:local
1617
LLAMA_STACK_IMAGE ?= lightspeed-llama-stack:local
1718
LLAMA_STACK_PORT ?= 8321
1819
CONTAINER_RUNTIME ?= $(shell command -v podman 2>/dev/null || command -v docker 2>/dev/null)
1920

20-
.PHONY: run run-stack build-llama-stack-image remove-llama-stack-container stop-llama-stack-container start-llama-stack-container wait-for-llama-stack-health clean-llama-stack
21+
# Dependency change detection
22+
DEPS_HASH_FILE := .llama-stack-deps.hash
23+
CURRENT_DEPS_HASH := $(shell cat pyproject.toml uv.lock providers/pyproject.toml providers/uv.lock 2>/dev/null | shasum -a 256 | cut -d' ' -f1)
24+
STORED_DEPS_HASH := $(shell cat $(DEPS_HASH_FILE) 2>/dev/null)
25+
26+
.PHONY: run run-stack build-llama-stack-deps-image ensure-llama-stack-deps-image build-llama-stack-image remove-llama-stack-container stop-llama-stack-container start-llama-stack-container wait-for-llama-stack-health clean-llama-stack
2127

2228
run-stack: ## Run lightspeed-stack directly, without building dependent service/s
2329
uv run src/lightspeed_stack.py -c $(CONFIG)
@@ -27,13 +33,44 @@ run: start-llama-stack-container ## Run the service locally with dependent servi
2733
@trap 'echo ""; echo "Stopping services..."; $(MAKE) stop-llama-stack-container' EXIT INT TERM; \
2834
$(MAKE) run-stack
2935

30-
build-llama-stack-image: remove-llama-stack-container ## Build llama-stack container image
31-
@echo "Building llama-stack container image..."
36+
build-llama-stack-deps-image: ## Force rebuild the deps base image
37+
@echo "Building llama-stack deps image..."
3238
@if [ -z "$(CONTAINER_RUNTIME)" ]; then \
3339
echo "ERROR: No container runtime found. Install podman or docker."; \
3440
exit 1; \
3541
fi
36-
$(CONTAINER_RUNTIME) build -f deploy/llama-stack/test.containerfile -t $(LLAMA_STACK_IMAGE) .
42+
@if $(CONTAINER_RUNTIME) image inspect $(LLAMA_STACK_DEPS_IMAGE) >/dev/null 2>&1; then \
43+
echo "Removing existing deps image to avoid dangling images..."; \
44+
$(CONTAINER_RUNTIME) rmi $(LLAMA_STACK_DEPS_IMAGE); \
45+
fi
46+
$(CONTAINER_RUNTIME) build -f deploy/llama-stack/test.containerfile --target deps-builder -t $(LLAMA_STACK_DEPS_IMAGE) .
47+
@echo "$(CURRENT_DEPS_HASH)" > $(DEPS_HASH_FILE)
48+
@echo "✓ Deps image built and hash saved"
49+
50+
ensure-llama-stack-deps-image: ## Build deps image only if missing or dependencies changed
51+
@if [ -z "$(CONTAINER_RUNTIME)" ]; then \
52+
echo "ERROR: No container runtime found. Install podman or docker."; \
53+
exit 1; \
54+
fi
55+
@if ! $(CONTAINER_RUNTIME) image inspect $(LLAMA_STACK_DEPS_IMAGE) >/dev/null 2>&1; then \
56+
echo "Deps image not found, building..."; \
57+
$(MAKE) build-llama-stack-deps-image; \
58+
elif [ "$(CURRENT_DEPS_HASH)" != "$(STORED_DEPS_HASH)" ]; then \
59+
echo "Dependencies changed (pyproject.toml or uv.lock), rebuilding deps image..."; \
60+
$(MAKE) build-llama-stack-deps-image; \
61+
else \
62+
echo "✓ Deps image is up-to-date (skipping rebuild)"; \
63+
fi
64+
65+
build-llama-stack-image: ensure-llama-stack-deps-image ## Build llama-stack app image (source-only layer on top of deps)
66+
@echo "Building llama-stack app image..."
67+
@if $(CONTAINER_RUNTIME) image inspect $(LLAMA_STACK_IMAGE) >/dev/null 2>&1; then \
68+
echo "Removing existing app image to avoid dangling images..."; \
69+
$(CONTAINER_RUNTIME) rmi $(LLAMA_STACK_IMAGE); \
70+
fi
71+
$(CONTAINER_RUNTIME) build -f deploy/llama-stack/test.containerfile \
72+
--build-arg DEPS_IMAGE=$(LLAMA_STACK_DEPS_IMAGE) \
73+
-t $(LLAMA_STACK_IMAGE) .
3774

3875
stop-llama-stack-container: ## Gracefully stop llama-stack container
3976
@if [ -n "$(CONTAINER_RUNTIME)" ] && $(CONTAINER_RUNTIME) inspect $(LLAMA_STACK_CONTAINER_NAME) >/dev/null 2>&1; then \
@@ -57,7 +94,7 @@ remove-llama-stack-container: ## Remove llama-stack container (saves logs first)
5794
echo "✓ Container removed (logs saved to /tmp/llama-stack-last-run.log)"; \
5895
fi
5996

60-
start-llama-stack-container: build-llama-stack-image ## Start llama-stack container
97+
start-llama-stack-container: remove-llama-stack-container build-llama-stack-image ## Start llama-stack container
6198
@echo "Starting llama-stack container..."
6299
$(CONTAINER_RUNTIME) run -d \
63100
--name $(LLAMA_STACK_CONTAINER_NAME) \
@@ -122,11 +159,16 @@ wait-for-llama-stack-health: ## Wait for llama-stack container to be healthy
122159
$(CONTAINER_RUNTIME) logs $(LLAMA_STACK_CONTAINER_NAME); \
123160
exit 1
124161

125-
clean-llama-stack: remove-llama-stack-container ## Remove container and image
162+
clean-llama-stack: remove-llama-stack-container ## Remove containers, images, and deps hash
126163
@if [ -n "$(CONTAINER_RUNTIME)" ] && $(CONTAINER_RUNTIME) images -q $(LLAMA_STACK_IMAGE) | grep -q .; then \
127-
echo "Removing llama-stack image..."; \
164+
echo "Removing llama-stack app image..."; \
128165
$(CONTAINER_RUNTIME) rmi $(LLAMA_STACK_IMAGE); \
129166
fi
167+
@if [ -n "$(CONTAINER_RUNTIME)" ] && $(CONTAINER_RUNTIME) images -q $(LLAMA_STACK_DEPS_IMAGE) | grep -q .; then \
168+
echo "Removing llama-stack deps image..."; \
169+
$(CONTAINER_RUNTIME) rmi $(LLAMA_STACK_DEPS_IMAGE); \
170+
fi
171+
@rm -f $(DEPS_HASH_FILE)
130172

131173
run-llama-stack: ## Start Llama Stack with enriched config (for local service mode)
132174
uv run src/llama_stack_configuration.py -c $(CONFIG) -i $(LLAMA_STACK_CONFIG) -o $(LLAMA_STACK_CONFIG) && \

deploy/llama-stack/test.containerfile

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,37 @@
1-
# Upstream llama-stack built from Red Hat UBI Python 3.12 image
2-
FROM registry.access.redhat.com/ubi9/python-312
1+
# DEPS_IMAGE selects the base layer.
2+
# Default: build deps inline (slow but self-contained).
3+
# Override with --build-arg DEPS_IMAGE=lightspeed-llama-stack-deps:local
4+
# to use a pre-built deps image (fast rebuilds, used by `make build-llama-stack-image`).
5+
ARG DEPS_IMAGE=deps-builder
6+
7+
# --- Stage 1: deps (skipped by BuildKit when DEPS_IMAGE is overridden) ---
8+
FROM registry.access.redhat.com/ubi9/python-312 AS deps-builder
39

410
USER root
511

612
# Install additional build tools
713
RUN dnf install -y --nodocs --setopt=keepcache=0 --setopt=tsflags=nodocs \
814
git tar gcc gcc-c++ make && \
915
dnf clean all
10-
16+
1117
# Install uv
1218
ENV PATH="/root/.local/bin:${PATH}"
1319
RUN curl -LsSf https://astral.sh/uv/install.sh | sh
1420

15-
# Copy project files for dependency installation
21+
# Copy only dependency-related files
1622
WORKDIR /opt/app-root
1723
COPY pyproject.toml uv.lock LICENSE README.md ./
18-
COPY src ./src
19-
COPY providers ./providers
24+
COPY src/version.py ./src/version.py
2025

21-
# Install dependencies using uv sync
26+
# Copy submodule dependency files only (source copied in app stage)
27+
COPY providers/pyproject.tom[l] providers/uv.loc[k] ./providers/
28+
29+
# Install dependencies (not the project itself)
2230
RUN uv sync --locked --no-install-project --group llslibdev
31+
RUN if [ -f providers/pyproject.toml ]; then \
32+
cd providers && uv export --locked --no-hashes > /tmp/providers-reqs.txt \
33+
&& uv pip install -r /tmp/providers-reqs.txt; \
34+
fi
2335

2436
# Add virtual environment to PATH for llama command
2537
# Add providers to PYTHONPATH so lightspeed_stack_providers modules can be imported
@@ -39,11 +51,24 @@ RUN mkdir -p /opt/app-root/src/.llama/storage \
3951
chown -R 1001:0 /opt/app-root && \
4052
chmod -R 775 /opt/app-root
4153

54+
USER 1001
55+
56+
# --- Stage 2: app (thin source-only layer) ---
57+
FROM ${DEPS_IMAGE}
58+
59+
USER root
60+
61+
# Copy source code and providers submodule
62+
COPY src ./src
63+
COPY provider[s] ./providers
64+
4265
# Copy enrichment scripts for runtime config enrichment
4366
COPY src/llama_stack_configuration.py /opt/app-root/llama_stack_configuration.py
4467
COPY scripts/llama-stack-entrypoint.sh /opt/app-root/enrich-entrypoint.sh
4568
RUN chmod +x /opt/app-root/enrich-entrypoint.sh && \
46-
chown 1001:0 /opt/app-root/enrich-entrypoint.sh /opt/app-root/llama_stack_configuration.py
69+
chown 1001:0 /opt/app-root/enrich-entrypoint.sh /opt/app-root/llama_stack_configuration.py && \
70+
chown -R 1001:0 /opt/app-root/src && \
71+
chmod -R 775 /opt/app-root/src
4772

4873
# Switch back to the original user
4974
USER 1001

0 commit comments

Comments
 (0)