Spaces:
Running
build: BuildKit cache mounts + .dockerignore + cursor rule
Browse filesDockerfile opts into BuildKit (# syntax=docker/dockerfile:1.7) and mounts persistent caches at /root/.npm and /home/user/.cache/pip so npm and pip downloads survive across rebuilds. First build is unchanged; subsequent rebuilds skip the network for unchanged dependencies.
.dockerignore narrows the build context (drops .git, .cursor, node_modules, build outputs, Python/Node caches, .env files, backend/tests, etc.) so the daemon does not ship hundreds of MB of local-dev cruft to the builder on every build.
.cursor/rules/docker-build.mdc captures the conventions (syntax directive, cache mount targets, no --no-cache-dir, build context narrowing) so future agents reuse the same pattern instead of regressing.
Co-authored-by: Cursor <cursoragent@cursor.com>
- .cursor/rules/docker-build.mdc +87 -0
- .dockerignore +61 -0
- Dockerfile +23 -2
|
@@ -0,0 +1,87 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
description: Docker build cache conventions for CCAI-Vibe-Demo
|
| 3 |
+
globs: Dockerfile,docker-compose*.yml,.dockerignore,backend/requirements.txt,frontend/package*.json
|
| 4 |
+
alwaysApply: false
|
| 5 |
+
---
|
| 6 |
+
|
| 7 |
+
# Docker build cache conventions
|
| 8 |
+
|
| 9 |
+
This project uses **BuildKit cache mounts** so `npm ci` and `pip install`
|
| 10 |
+
skip the network when only source files change. Don't replace them
|
| 11 |
+
with plain `RUN` commands - that reintroduces full re-downloads on every
|
| 12 |
+
dependency change.
|
| 13 |
+
|
| 14 |
+
## Required Dockerfile pattern
|
| 15 |
+
|
| 16 |
+
The Dockerfile MUST start with the BuildKit syntax directive:
|
| 17 |
+
|
| 18 |
+
```dockerfile
|
| 19 |
+
# syntax=docker/dockerfile:1.7
|
| 20 |
+
```
|
| 21 |
+
|
| 22 |
+
### Frontend stage (root user, node:22-alpine)
|
| 23 |
+
|
| 24 |
+
```dockerfile
|
| 25 |
+
COPY frontend/package.json frontend/package-lock.json ./
|
| 26 |
+
RUN --mount=type=cache,target=/root/.npm \
|
| 27 |
+
npm ci
|
| 28 |
+
```
|
| 29 |
+
|
| 30 |
+
### Backend stage (USER user, uid 1000, python:3.12-slim)
|
| 31 |
+
|
| 32 |
+
```dockerfile
|
| 33 |
+
COPY --chown=user backend/requirements.txt ./
|
| 34 |
+
RUN --mount=type=cache,target=/home/user/.cache/pip,uid=1000,gid=1000 \
|
| 35 |
+
pip install --user -r requirements.txt
|
| 36 |
+
```
|
| 37 |
+
|
| 38 |
+
The `uid=1000,gid=1000` flags are mandatory because the cache target
|
| 39 |
+
sits inside the unprivileged user's home dir. Without them the cache
|
| 40 |
+
is created root-owned and pip cannot write to it.
|
| 41 |
+
|
| 42 |
+
## Don'ts
|
| 43 |
+
|
| 44 |
+
- **Don't** pass `--no-cache-dir` to `pip install`. It defeats the
|
| 45 |
+
cache mount. The mount lives outside the image, so wheels never
|
| 46 |
+
bloat the final layer either way.
|
| 47 |
+
- **Don't** `COPY .` blindly. Layers must be copy-deps-first,
|
| 48 |
+
copy-source-second so a source edit doesn't bust the deps layer.
|
| 49 |
+
- **Don't** add new files outside `backend/`, `frontend/` or the
|
| 50 |
+
lockfiles to a `COPY` line without first checking `.dockerignore`.
|
| 51 |
+
|
| 52 |
+
## Build context (`.dockerignore`)
|
| 53 |
+
|
| 54 |
+
The ignore list excludes `.git/`, `node_modules/`, `frontend/build/`,
|
| 55 |
+
`agent-transcripts/`, `__pycache__`, secrets (`.env*`),
|
| 56 |
+
`docker-compose.override.yml`, `backend/tests/`, and editor metadata.
|
| 57 |
+
If you need a previously-ignored path inside the image, add a narrow
|
| 58 |
+
`!path/to/keep` exception rather than removing the broad ignore.
|
| 59 |
+
|
| 60 |
+
## Verifying the cache works
|
| 61 |
+
|
| 62 |
+
After a Dockerfile or deps-file change, run two builds back to back:
|
| 63 |
+
|
| 64 |
+
```powershell
|
| 65 |
+
docker compose build
|
| 66 |
+
docker compose build # second time
|
| 67 |
+
```
|
| 68 |
+
|
| 69 |
+
The second build's `npm ci` / `pip install` step should print a
|
| 70 |
+
`CACHED` line (when nothing in the deps file changed) or a
|
| 71 |
+
near-instant `=> [internal] load build context` finish (when only
|
| 72 |
+
source files changed). If it re-downloads, the cache mount is
|
| 73 |
+
mis-configured.
|
| 74 |
+
|
| 75 |
+
If a build ever errors with `the --mount option requires BuildKit`,
|
| 76 |
+
opt in explicitly:
|
| 77 |
+
|
| 78 |
+
```powershell
|
| 79 |
+
$env:DOCKER_BUILDKIT=1; docker compose build
|
| 80 |
+
```
|
| 81 |
+
|
| 82 |
+
## HuggingFace Spaces compatibility
|
| 83 |
+
|
| 84 |
+
HuggingFace's Docker Space builder honors the `# syntax=` directive
|
| 85 |
+
and BuildKit cache mounts. Older builders silently treat the mounts
|
| 86 |
+
as no-ops, so this Dockerfile remains forward-compatible with any
|
| 87 |
+
plain-Docker environment.
|
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Build context exclusions for `docker build` / `docker compose build`.
|
| 2 |
+
# Trimming the context speeds the daemon transfer and keeps cache hashes
|
| 3 |
+
# stable: changes to ignored paths do NOT invalidate Dockerfile layers.
|
| 4 |
+
|
| 5 |
+
# ── Git / VCS metadata ───────────────────────────────────────────────
|
| 6 |
+
.git
|
| 7 |
+
.gitignore
|
| 8 |
+
.gitattributes
|
| 9 |
+
.github
|
| 10 |
+
|
| 11 |
+
# ── Cursor / agent transcripts (local IDE state, never needed in image)
|
| 12 |
+
.cursor
|
| 13 |
+
agent-transcripts
|
| 14 |
+
.commit-msg.txt
|
| 15 |
+
|
| 16 |
+
# ── Editor / OS junk ─────────────────────────────────────────────────
|
| 17 |
+
.vscode
|
| 18 |
+
.idea
|
| 19 |
+
.DS_Store
|
| 20 |
+
Thumbs.db
|
| 21 |
+
*.swp
|
| 22 |
+
|
| 23 |
+
# ── Secrets / env (image gets env at runtime via docker-compose)
|
| 24 |
+
.env
|
| 25 |
+
.env.*
|
| 26 |
+
*.env
|
| 27 |
+
!**/.env.example
|
| 28 |
+
|
| 29 |
+
# ── Python local caches ──────────────────────────────────────────────
|
| 30 |
+
**/__pycache__
|
| 31 |
+
*.pyc
|
| 32 |
+
*.pyo
|
| 33 |
+
*.pyd
|
| 34 |
+
*.egg-info
|
| 35 |
+
.pytest_cache
|
| 36 |
+
.mypy_cache
|
| 37 |
+
.ruff_cache
|
| 38 |
+
.venv
|
| 39 |
+
venv
|
| 40 |
+
|
| 41 |
+
# ── Node local caches & build outputs ────────────────────────────────
|
| 42 |
+
# Lockfiles ARE included on purpose so `npm ci` is reproducible.
|
| 43 |
+
**/node_modules
|
| 44 |
+
frontend/build
|
| 45 |
+
frontend/.next
|
| 46 |
+
frontend/.cache
|
| 47 |
+
**/.cache
|
| 48 |
+
npm-debug.log*
|
| 49 |
+
yarn-debug.log*
|
| 50 |
+
yarn-error.log*
|
| 51 |
+
|
| 52 |
+
# ── Local-only docker compose overrides ──────────────────────────────
|
| 53 |
+
# The base docker-compose.yml is needed for `docker compose build` from
|
| 54 |
+
# the build dir, but the local override (which references absolute host
|
| 55 |
+
# paths to a secrets file outside this repo) shouldn't end up in the
|
| 56 |
+
# image build context.
|
| 57 |
+
docker-compose.override.yml
|
| 58 |
+
|
| 59 |
+
# ── Tests aren't shipped in the runtime image ────────────────────────
|
| 60 |
+
# (Re-enable if you want to run pytest inside the deployed container.)
|
| 61 |
+
backend/tests
|
|
@@ -1,11 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
FROM node:22-alpine AS frontend-build
|
| 2 |
WORKDIR /app/frontend
|
|
|
|
|
|
|
|
|
|
| 3 |
COPY frontend/package.json frontend/package-lock.json ./
|
| 4 |
-
RUN npm
|
|
|
|
|
|
|
| 5 |
COPY frontend/ ./
|
| 6 |
ENV REACT_APP_API_URL=
|
| 7 |
RUN npm run build
|
| 8 |
|
|
|
|
| 9 |
FROM python:3.12-slim
|
| 10 |
|
| 11 |
RUN useradd -m -u 1000 user
|
|
@@ -16,8 +34,11 @@ ENV HOME=/home/user \
|
|
| 16 |
|
| 17 |
WORKDIR $HOME/app
|
| 18 |
|
|
|
|
|
|
|
| 19 |
COPY --chown=user backend/requirements.txt ./
|
| 20 |
-
RUN
|
|
|
|
| 21 |
|
| 22 |
COPY --chown=user backend/ ./
|
| 23 |
COPY --chown=user --from=frontend-build /app/frontend/build ./static
|
|
|
|
| 1 |
+
# syntax=docker/dockerfile:1.7
|
| 2 |
+
#
|
| 3 |
+
# BuildKit cache mounts speed up rebuilds by persisting npm and pip
|
| 4 |
+
# package caches across builds (outside the image - they don't bloat
|
| 5 |
+
# the final layer). The first `docker compose build` is unchanged;
|
| 6 |
+
# subsequent rebuilds skip the network for any unchanged dependency.
|
| 7 |
+
#
|
| 8 |
+
# The `# syntax=` directive auto-opts into BuildKit on Docker Desktop
|
| 9 |
+
# 20.10+; HuggingFace Spaces also supports this directive. Older
|
| 10 |
+
# builders silently treat the cache mounts as no-ops, so this file
|
| 11 |
+
# remains forward-compatible.
|
| 12 |
+
|
| 13 |
FROM node:22-alpine AS frontend-build
|
| 14 |
WORKDIR /app/frontend
|
| 15 |
+
|
| 16 |
+
# Copy lockfile + manifest first so the layer hash only changes when
|
| 17 |
+
# deps actually change, not on every source edit.
|
| 18 |
COPY frontend/package.json frontend/package-lock.json ./
|
| 19 |
+
RUN --mount=type=cache,target=/root/.npm \
|
| 20 |
+
npm ci
|
| 21 |
+
|
| 22 |
COPY frontend/ ./
|
| 23 |
ENV REACT_APP_API_URL=
|
| 24 |
RUN npm run build
|
| 25 |
|
| 26 |
+
|
| 27 |
FROM python:3.12-slim
|
| 28 |
|
| 29 |
RUN useradd -m -u 1000 user
|
|
|
|
| 34 |
|
| 35 |
WORKDIR $HOME/app
|
| 36 |
|
| 37 |
+
# Lockfile-equivalent first; cache mount targets the unprivileged
|
| 38 |
+
# user's pip cache (uid/gid 1000 matches the `user` account).
|
| 39 |
COPY --chown=user backend/requirements.txt ./
|
| 40 |
+
RUN --mount=type=cache,target=/home/user/.cache/pip,uid=1000,gid=1000 \
|
| 41 |
+
pip install --user -r requirements.txt
|
| 42 |
|
| 43 |
COPY --chown=user backend/ ./
|
| 44 |
COPY --chown=user --from=frontend-build /app/frontend/build ./static
|