Skip to content

Commit e011cf4

Browse files
drewstoneclaude
andauthored
feat: add package cache warming for faster container startup (#13)
* feat: add package cache warming for faster container startup Pre-populate npm, cargo, and pip caches during Docker image build time so that first-time package installations are significantly faster. Changes: - base-system.Dockerfile: npm cache add for ~25 common packages (React, Next, Vite, TypeScript, testing, styling, utilities) - intermediate/rust.Dockerfile: cargo fetch for common crates (tokio, serde, anyhow, reqwest, clap, etc.) - generate_docker.js: new generateCacheWarmCommands() function with support for npm, cargo, and pip cache warming in generated Dockerfiles - config.json: updated rust template + added cache_warm configs to ethereum, solana, tangle, universal, and langchain projects Expected impact: - npm install react: ~5s → <1s - npm create vite: ~15s → ~2s - cargo build (new project): ~60s → ~10s Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: ensure cargo cache permissions and pip compatibility - Add chmod -R a+w $CARGO_HOME after cargo fetch to ensure cache files are writable by the project user (not just root) - Add --break-system-packages flag to pip download for consistency with other pip commands and compatibility with newer pip versions Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent efe8cde commit e011cf4

File tree

4 files changed

+164
-7
lines changed

4 files changed

+164
-7
lines changed

base/base-system.Dockerfile

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,32 @@ RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \
2020
&& npm install -g opencode-ai \
2121
&& rm -rf /var/lib/apt/lists/*
2222

23+
# Pre-warm npm cache with commonly used packages
24+
# This populates the npm cache so first `npm install` for these packages is instant
25+
# These packages cover: React ecosystem, build tools, testing, styling, utilities
26+
RUN npm cache add \
27+
# React ecosystem
28+
react@latest react-dom@latest @types/react@latest @types/react-dom@latest \
29+
next@latest @next/env@latest \
30+
# Build & bundling
31+
vite@latest @vitejs/plugin-react@latest esbuild@latest rollup@latest \
32+
# TypeScript
33+
typescript@latest @types/node@latest ts-node@latest \
34+
# Testing
35+
vitest@latest @vitest/ui@latest jest@latest @types/jest@latest \
36+
# Styling
37+
tailwindcss@latest postcss@latest autoprefixer@latest \
38+
# Server frameworks
39+
express@latest @types/express@latest fastify@latest hono@latest \
40+
# Utilities
41+
zod@latest dotenv@latest axios@latest lodash@latest @types/lodash@latest \
42+
# Database/ORM
43+
drizzle-orm@latest prisma@latest @prisma/client@latest \
44+
# Linting & formatting
45+
eslint@latest prettier@latest @typescript-eslint/parser@latest @typescript-eslint/eslint-plugin@latest \
46+
# Monorepo tools
47+
turbo@latest
48+
2349
# Install Claude Code
2450
RUN curl -fsSL https://claude.ai/install.sh | bash
2551

config.json

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"intermediate_templates": {
3-
"rust": "FROM base-system:latest\n\nENV RUSTUP_HOME=/usr/local/rustup \\\n CARGO_HOME=/usr/local/cargo \\\n PATH=/usr/local/cargo/bin:$PATH\n\nUSER root\nRUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y \\\n && chmod -R a+w $RUSTUP_HOME $CARGO_HOME\n\nUSER project\n\nLABEL description=\"Rust intermediate layer\"\n",
3+
"rust": "FROM base-system:latest\n\nENV RUSTUP_HOME=/usr/local/rustup \\\n CARGO_HOME=/usr/local/cargo \\\n PATH=/usr/local/cargo/bin:$PATH\n\nUSER root\nRUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y \\\n && chmod -R a+w $RUSTUP_HOME $CARGO_HOME\n\n# Pre-warm cargo cache with commonly used crates\nRUN mkdir -p /tmp/cargo-warm && \\\n printf '[package]\\nname = \"warm\"\\nversion = \"0.0.0\"\\nedition = \"2021\"\\n\\n[dependencies]\\ntokio = { version = \"1\", features = [\"full\"] }\\nserde = { version = \"1\", features = [\"derive\"] }\\nserde_json = \"1\"\\nthiserror = \"1\"\\nanyhow = \"1\"\\ntracing = \"0.1\"\\ntracing-subscriber = \"0.3\"\\nasync-trait = \"0.1\"\\nfutures = \"0.3\"\\nreqwest = { version = \"0.12\", features = [\"json\"] }\\nclap = { version = \"4\", features = [\"derive\"] }\\n' > /tmp/cargo-warm/Cargo.toml && \\\n mkdir -p /tmp/cargo-warm/src && \\\n echo 'fn main() {}' > /tmp/cargo-warm/src/main.rs && \\\n cd /tmp/cargo-warm && cargo fetch && \\\n rm -rf /tmp/cargo-warm && \\\n chmod -R a+w $CARGO_HOME\n\nUSER project\n\nLABEL description=\"Rust intermediate layer\"\n",
44
"foundry": "FROM rust:latest\n\nENV PATH=/root/.foundry/bin:/usr/local/cargo/bin:$PATH\n\nUSER root\nRUN curl -L https://foundry.paradigm.xyz | bash \\\n && /root/.foundry/bin/foundryup \\\n && chmod -R a+rx /root/.foundry\n\nUSER project\n\nLABEL description=\"Foundry intermediate layer (forge, cast, anvil, chisel)\"\n",
55
"scientific-python": "FROM base-system:latest\n\nUSER root\nRUN pip3 install --no-cache-dir --break-system-packages \\\n numpy scipy pandas matplotlib seaborn plotly \\\n scikit-learn scikit-image \\\n jupyter jupyterlab ipython notebook \\\n pillow opencv-python-headless \\\n h5py pyarrow fastparquet \\\n tqdm rich typer click \\\n httpx aiohttp requests \\\n pydantic pydantic-settings \\\n python-dotenv PyYAML toml \\\n && jupyter --version\n\nUSER project\n\nLABEL description=\"Scientific Python intermediate layer (NumPy, SciPy, Pandas, Jupyter, ML basics)\"\n"
66
},
@@ -51,6 +51,15 @@
5151
"base": "foundry",
5252
"packages": {
5353
"npm": ["ethers", "viem", "@wagmi/core", "hardhat", "@nomicfoundation/hardhat-toolbox"]
54+
},
55+
"cache_warm": {
56+
"npm": [
57+
"@openzeppelin/contracts@latest",
58+
"@openzeppelin/contracts-upgradeable@latest",
59+
"wagmi@latest",
60+
"@rainbow-me/rainbowkit@latest",
61+
"abitype@latest"
62+
]
5463
}
5564
},
5665
"polygon": {
@@ -89,6 +98,21 @@
8998
"curl --proto '=https' --tlsv1.2 -sSfL https://solana-install.solana.workers.dev | bash || echo 'Solana installation may not support this architecture. Consider building from source.'",
9099
"if [ -f /root/.local/share/solana/install/active_release/bin/solana ]; then /root/.local/share/solana/install/active_release/bin/solana --version && chmod -R a+rx /root/.local/share/solana; else echo 'Solana CLI not installed - platform may not be supported'; fi"
91100
]
101+
},
102+
"cache_warm": {
103+
"npm": [
104+
"@solana/web3.js@latest",
105+
"@coral-xyz/anchor@latest",
106+
"@solana/spl-token@latest",
107+
"@metaplex-foundation/js@latest",
108+
"@solana/wallet-adapter-base@latest",
109+
"@solana/wallet-adapter-react@latest"
110+
],
111+
"cargo": [
112+
"solana-program@1",
113+
"anchor-lang@0.30",
114+
"spl-token@4"
115+
]
92116
}
93117
},
94118
"sui": {
@@ -137,6 +161,14 @@
137161
"packages": {
138162
"cargo": ["subxt-cli@0.39.0"],
139163
"npm": ["@tangle-network/tangle-substrate-types"]
164+
},
165+
"cache_warm": {
166+
"cargo": [
167+
"subxt@0.39",
168+
"sp-core@*",
169+
"sp-runtime@*",
170+
"frame-support@*"
171+
]
140172
}
141173
},
142174

@@ -180,6 +212,21 @@
180212
"dotenv"
181213
]
182214
},
215+
"cache_warm": {
216+
"npm": [
217+
"react@latest",
218+
"react-dom@latest",
219+
"next@latest",
220+
"vite@latest",
221+
"express@latest",
222+
"fastify@latest",
223+
"hono@latest",
224+
"zod@latest",
225+
"drizzle-orm@latest",
226+
"prisma@latest",
227+
"tailwindcss@latest"
228+
]
229+
},
183230
"custom_install": {
184231
"env": {
185232
"GOROOT": "/usr/local/go",
@@ -460,6 +507,15 @@
460507
"pip3 install --no-cache-dir --break-system-packages chromadb faiss-cpu sentence-transformers",
461508
"python3 -c 'import langchain; print(f\"LangChain {langchain.__version__}\")'"
462509
]
510+
},
511+
"cache_warm": {
512+
"pip": [
513+
"openai",
514+
"anthropic",
515+
"tiktoken",
516+
"chromadb",
517+
"sentence-transformers"
518+
]
463519
}
464520
},
465521

generate_docker.js

Lines changed: 72 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,48 @@ const config = loadConfig();
1313
const PROJECT_CONFIGS = config.projects;
1414
const INTERMEDIATE_TEMPLATES = config.intermediate_templates;
1515

16+
/**
17+
* Generate cache warming commands for a project
18+
* @param {Object} cacheWarm - The cache_warm configuration object
19+
* @returns {string} Dockerfile RUN commands for cache warming
20+
*/
21+
function generateCacheWarmCommands(cacheWarm) {
22+
if (!cacheWarm) return '';
23+
24+
const commands = [];
25+
26+
// NPM cache warming
27+
if (cacheWarm.npm && cacheWarm.npm.length > 0) {
28+
commands.push(`# Pre-warm npm cache with project-specific packages`);
29+
commands.push(`RUN npm cache add ${cacheWarm.npm.join(' ')}`);
30+
}
31+
32+
// Cargo cache warming (fetch crates without building)
33+
if (cacheWarm.cargo && cacheWarm.cargo.length > 0) {
34+
// Create a temporary Cargo.toml to fetch dependencies
35+
const deps = cacheWarm.cargo.map(crate => {
36+
const [name, version] = crate.split('@');
37+
return `${name} = "${version || '*'}"`;
38+
}).join('\\n');
39+
40+
commands.push(`# Pre-warm cargo cache with project-specific crates`);
41+
commands.push(`RUN mkdir -p /tmp/cargo-warm && \\`);
42+
commands.push(` printf '[package]\\nname = "warm"\\nversion = "0.0.0"\\nedition = "2021"\\n\\n[dependencies]\\n${deps}\\n' > /tmp/cargo-warm/Cargo.toml && \\`);
43+
commands.push(` mkdir -p /tmp/cargo-warm/src && echo 'fn main() {}' > /tmp/cargo-warm/src/main.rs && \\`);
44+
commands.push(` cd /tmp/cargo-warm && cargo fetch && \\`);
45+
commands.push(` rm -rf /tmp/cargo-warm && \\`);
46+
commands.push(` chmod -R a+w $CARGO_HOME`);
47+
}
48+
49+
// pip cache warming
50+
if (cacheWarm.pip && cacheWarm.pip.length > 0) {
51+
commands.push(`# Pre-warm pip cache with project-specific packages`);
52+
commands.push(`RUN pip download --break-system-packages --dest /tmp/pip-warm ${cacheWarm.pip.join(' ')} && rm -rf /tmp/pip-warm`);
53+
}
54+
55+
return commands.length > 0 ? '\n' + commands.join('\n') + '\n' : '';
56+
}
57+
1658
function generateIntermediateDockerfile(base, outputDir) {
1759
if (!(base in INTERMEDIATE_TEMPLATES)) {
1860
throw new Error(`Unknown base: ${base}`);
@@ -72,13 +114,18 @@ function generateInfraDockerfile(project, config, outputDir) {
72114

73115
if (packages.cargo && packages.cargo.length > 0) {
74116
for (const pkg of packages.cargo) {
75-
const cargoCmd = pkg.includes('@')
117+
const cargoCmd = pkg.includes('@')
76118
? `cargo install ${pkg.split('@')[0]} --version ${pkg.split('@')[1]}`
77119
: `cargo install ${pkg}`;
78120
dockerfileLines.push(`\nRUN ${cargoCmd}\n`);
79121
}
80122
}
81-
123+
124+
// Cache warming (pre-fetch packages without installing)
125+
if (config.cache_warm) {
126+
dockerfileLines.push(generateCacheWarmCommands(config.cache_warm));
127+
}
128+
82129
dockerfileLines.push(`\nLABEL description="${project} infrastructure layer"\n`);
83130

84131
const filepath = path.join(outputDir, `${project}.Dockerfile`);
@@ -127,7 +174,8 @@ function generateCombinedDockerfile(projectNames, outputDir) {
127174
const allAptPackages = [];
128175
const allRootCommands = [];
129176
const allCommands = [];
130-
177+
const allCacheWarm = { npm: [], cargo: [], pip: [] };
178+
131179
for (const [name, config] of configs) {
132180
const packages = config.packages;
133181
if (packages.npm && packages.npm.length > 0) {
@@ -136,7 +184,7 @@ function generateCombinedDockerfile(projectNames, outputDir) {
136184
if (packages.cargo && packages.cargo.length > 0) {
137185
allCargoPackages.push(...packages.cargo);
138186
}
139-
187+
140188
if (config.custom_install) {
141189
if (config.custom_install.env) {
142190
Object.assign(allEnvVars, config.custom_install.env);
@@ -151,6 +199,13 @@ function generateCombinedDockerfile(projectNames, outputDir) {
151199
allCommands.push(...config.custom_install.commands);
152200
}
153201
}
202+
203+
// Collect cache_warm configs
204+
if (config.cache_warm) {
205+
if (config.cache_warm.npm) allCacheWarm.npm.push(...config.cache_warm.npm);
206+
if (config.cache_warm.cargo) allCacheWarm.cargo.push(...config.cache_warm.cargo);
207+
if (config.cache_warm.pip) allCacheWarm.pip.push(...config.cache_warm.pip);
208+
}
154209
}
155210

156211
const uniqueNpmPackages = [...new Set(allNpmPackages)];
@@ -198,13 +253,24 @@ function generateCombinedDockerfile(projectNames, outputDir) {
198253

199254
if (uniqueCargoPackages.length > 0) {
200255
for (const pkg of uniqueCargoPackages) {
201-
const cargoCmd = pkg.includes('@')
256+
const cargoCmd = pkg.includes('@')
202257
? `cargo install ${pkg.split('@')[0]} --version ${pkg.split('@')[1]}`
203258
: `cargo install ${pkg}`;
204259
dockerfileLines.push(`\nRUN ${cargoCmd}\n`);
205260
}
206261
}
207-
262+
263+
// Combined cache warming (deduplicated)
264+
const mergedCacheWarm = {
265+
npm: [...new Set(allCacheWarm.npm)],
266+
cargo: [...new Set(allCacheWarm.cargo)],
267+
pip: [...new Set(allCacheWarm.pip)]
268+
};
269+
const hasCacheWarm = mergedCacheWarm.npm.length > 0 || mergedCacheWarm.cargo.length > 0 || mergedCacheWarm.pip.length > 0;
270+
if (hasCacheWarm) {
271+
dockerfileLines.push(generateCacheWarmCommands(mergedCacheWarm));
272+
}
273+
208274
const projectsDesc = sortedProjectNames.join(', ');
209275
dockerfileLines.push(`\nLABEL description="Combined: ${projectsDesc}"\n`);
210276

intermediate/rust.Dockerfile

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,15 @@ USER root
88
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y \
99
&& chmod -R a+w $RUSTUP_HOME $CARGO_HOME
1010

11+
# Pre-warm cargo cache with commonly used crates
12+
RUN mkdir -p /tmp/cargo-warm && \
13+
printf '[package]\nname = "warm"\nversion = "0.0.0"\nedition = "2021"\n\n[dependencies]\ntokio = { version = "1", features = ["full"] }\nserde = { version = "1", features = ["derive"] }\nserde_json = "1"\nthiserror = "1"\nanyhow = "1"\ntracing = "0.1"\ntracing-subscriber = "0.3"\nasync-trait = "0.1"\nfutures = "0.3"\nreqwest = { version = "0.12", features = ["json"] }\nclap = { version = "4", features = ["derive"] }\n' > /tmp/cargo-warm/Cargo.toml && \
14+
mkdir -p /tmp/cargo-warm/src && \
15+
echo 'fn main() {}' > /tmp/cargo-warm/src/main.rs && \
16+
cd /tmp/cargo-warm && cargo fetch && \
17+
rm -rf /tmp/cargo-warm && \
18+
chmod -R a+w $CARGO_HOME
19+
1120
USER project
1221

1322
LABEL description="Rust intermediate layer"

0 commit comments

Comments
 (0)