Docker - #364
Conversation
WalkthroughThe change adds Docker and Compose support for local frontend, backend, and Redis development, including image definitions, startup scripts, configurable service URLs, persistent volumes, and Makefile helpers. It adds multi-platform GHCR publishing, weekly image retention cleanup, and pull request version validation workflows. Frontend version metadata is updated, network URL discovery is added, and README and Docker documentation describe setup, commands, architecture, and current Compose limitations. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 19
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: f57daeaf-bbb1-4740-8638-945ebab0d00a
⛔ Files ignored due to path filters (1)
frontend/package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (20)
.github/workflows/clean-up.yml.github/workflows/merge-protect.yml.github/workflows/publish-images.ymlMakefileREADME.mdbackend/.dockerignorebackend/Dockerfilebackend/Dockerfile .devbackend/api/settings.pybackend/start.shdocker-compose.ymldocs/docker.mdfrontend/.dockerignorefrontend/Dockerfilefrontend/Dockerfile.devfrontend/next.config.tsfrontend/package.jsonfrontend/src/lib/utils/api/server-fetch.tsfrontend/start.shscripts/print-ip.js
| FROM node:22-alpine | ||
|
|
||
| WORKDIR /app | ||
|
|
||
| COPY package.json package-lock.json* ./ | ||
| RUN npm ci | ||
|
|
||
| COPY . . | ||
|
|
||
| EXPOSE 3000 | ||
|
|
||
| CMD ["npm", "run", "dev"] |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Run the development image as a non-root user.
The image never switches from root, so Compose runs npm install and Next.js with root access over the bind-mounted source tree. Create/use a non-root user and explicitly grant ownership of /app and its writable volumes before USER.
Proposed fix
RUN npm ci
-COPY . .
+COPY --chown=node:node . .
+RUN chown -R node:node /app
+USER node
EXPOSE 3000📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| FROM node:22-alpine | |
| WORKDIR /app | |
| COPY package.json package-lock.json* ./ | |
| RUN npm ci | |
| COPY . . | |
| EXPOSE 3000 | |
| CMD ["npm", "run", "dev"] | |
| FROM node:22-alpine | |
| WORKDIR /app | |
| COPY package.json package-lock.json* ./ | |
| RUN npm ci | |
| COPY --chown=node:node . . | |
| RUN chown -R node:node /app | |
| USER node | |
| EXPOSE 3000 | |
| CMD ["npm", "run", "dev"] |
🧰 Tools
🪛 Checkov (3.3.8)
[low] 1-14: Ensure that HEALTHCHECK instructions have been added to container images
(CKV_DOCKER_2)
[low] 1-14: Ensure that a user for the container has been created
(CKV_DOCKER_3)
Source: Linters/SAST tools
| const baseUrl = | ||
| process.env.INTERNAL_API_URL || process.env.NEXT_PUBLIC_API_URL; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== locate file =="
fd -a 'server-fetch\.ts$|next\.config\.(ts|js|mjs)$' . || true
echo "== inspect server-fetch =="
if [ -f frontend/src/lib/utils/api/server-fetch.ts ]; then
nl -ba frontend/src/lib/utils/api/server-fetch.ts | sed -n '1,110p'
fi
echo "== inspect next config API fallbacks =="
for f in frontend/next.config.ts frontend/next.config.js frontend/next.config.mjs; do
[ -f "$f" ] && { echo "--- $f"; nl -ba "$f" | sed -n '1,160p'; }
done
echo "== search NEXT_PUBLIC_API_URL / INTERNAL_API_URL =="
rg -n "INTERNAL_API_URL|NEXT_PUBLIC_API_URL" . --glob '!node_modules' --glob '!dist' --glob '!build' || trueRepository: plan-cake/plancake
Length of output: 346
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== inspect server-fetch =="
if [ -f frontend/src/lib/utils/api/server-fetch.ts ]; then
awk '{printf "%6d\t%s\n", NR, $0}' frontend/src/lib/utils/api/server-fetch.ts | sed -n '1,120p'
fi
echo "== inspect next config API fallbacks =="
for f in frontend/next.config.ts frontend/next.config.js frontend/next.config.mjs; do
[ -f "$f" ] && { echo "--- $f"; awk '{printf "%6d\t%s\n", NR, $0}' "$f" | sed -n '1,180p'; }
done
echo "== search API env usages =="
rg -n "INTERNAL_API_URL|NEXT_PUBLIC_API_URL" . --glob '!node_modules' --glob '!dist' --glob '!build' || true
echo "== probe baseUrl interpolation behavior =="
node - <<'JS'
function makeUrl(baseUrl, endpoint) {
return new URL(endpoint, baseUrl);
}
for (const e of ['undefined value', null, '']) {
const baseUrl = e === 'undefined value' ? undefined : e;
try {
const url = makeUrl(baseUrl, '/api/users');
console.log(JSON.stringify({ baseUrl, url: String(url) }));
} catch (err) {
console.log(JSON.stringify({ baseUrl, error: err.name + ': ' + err.message }));
}
}
JSRepository: plan-cake/plancake
Length of output: 5723
Validate the API base URL before constructing requests.
When both INTERNAL_API_URL and NEXT_PUBLIC_API_URL are unset, baseUrl becomes undefined, and the interpolated URL (undefined/...) throws before authentication/cookie handling runs. Add a small host-wide dev fallback such as http://localhost:8000, or throw a clear configuration error before calling fetchJson() for both helpers.
| @@ -0,0 +1,95 @@ | |||
|
|
|||
| .PHONY: help up down restart logs-api logs-web shell-api shell-web migrate makemigrations | |||
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Makefile relevant lines =="
if [ -f Makefile ]; then
nl -ba Makefile | sed -n '1,110p'
else
echo "Makefile not found"
fi
echo
echo "== Makefile phony and target declarations =="
python3 - <<'PY'
from pathlib import Path
p = Path("Makefile")
if not p.exists():
raise SystemExit
lines = p.read_text().splitlines()
for i, line in enumerate(lines, 1):
if line.startswith(".PHONY"):
print(f"{i}: {line}")
for i, line in enumerate(lines, 1):
if line.startswith("build:") or line.startswith("url:") or line.startswith("help:") or line.startswith("up:"):
print(f"{i}: {line}")
PY
echo
echo "== Check if make is available =="
command -v make || trueRepository: plan-cake/plancake
Length of output: 226
🏁 Script executed:
#!/bin/bash
set -u
echo "== Makefile existence and size =="
if [ -f Makefile ]; then
wc -l Makefile
else
echo "Makefile not found"
fi
echo
echo "== Makefile relevant lines =="
if [ -f Makefile ]; then
cat -n Makefile | sed -n '1,110p'
fi
echo
echo "== Makefile phony and target declarations =="
if [ -f Makefile ]; then
awk 'NR==FNR{for(i=1;i<=NF;i++) if($i=="\\") $(i+1)=$0} {print NR": "$0}' Makefile | \
grep -E '^\s*[0-9]+: (\.PHONY|build:|url:|\s*build\s*:|\s*url\s*:)+'
awk '{line=$0} NR==FNR{if(/\\[[:space:]]*$/){getline; line=line$0} else if(line~/^(\.PHONY|build:|url:|\s*build\s*:|\s*url\s*:)/ || $0~ /^(\.PHONY|build:|url:|\s*build\s*:|\s*url\s*:)/) print FNR": "$0}' Makefile Makefile
fi
echo
echo "== Check if make is available =="
command -v make || true
echo
echo "== Deterministic Makefile behavior probe with system make, if available =="
tmp="$(mktemp -d)"
trap 'rm -rf "$tmp"' EXIT
cat > "$tmp/Makefile" <<'EOF'
.PHONY: phony-only one-two
one-two: app
`@echo` TARGET_RAN
phony-only:
`@echo` PHONY_ONLY_RAN
EOF
touch "$tmp/app"
{
echo "== with app present, target not marked .PHONY =="
(cd "$tmp" && make --no-print-directory one-two || true)
echo
echo "== with app absent =="
rm "$tmp/app"
(cd "$tmp" && make --no-print-directory one-two || true)
} 2>&1 || trueRepository: plan-cake/plancake
Length of output: 4892
🌐 Web query:
GNU make .PHONY target exists file directory behavior make target skipped
💡 Result:
In GNU make, the.PHONY special target is used to explicitly declare that a target does not correspond to an actual file on the filesystem [1][2][3]. When a target is marked as.PHONY, make ignores the existence or modification time of any file with the same name and executes the associated recipe unconditionally every time that target is requested [4][1][5][3]. Key behaviors include: 1. Unconditional Execution: If you do not declare a target as.PHONY and a file with that same name happens to exist in the directory, make will treat it as a real file target [1][2]. If that file is up to date relative to its prerequisites, make will consider it "up to date" and skip the recipe [1][2][3]. Declaring it.PHONY forces execution regardless of the file's presence or age [4][2][3]. 2. Performance Optimization: Because make knows that a.PHONY target does not represent an actual file, it skips the implicit rule search for that target [2][6][3]. This provides a minor performance benefit by avoiding unnecessary filesystem lookups [1][2][3]. 3. Proper Usage: You should use.PHONY for targets that perform actions rather than building files (e.g., clean, all, install) [1][2]. Conversely, you should avoid using.PHONY for real file targets (like object files or executables), as doing so will cause them to be rebuilt unnecessarily every time you run make [7]. 4. Interaction with Directories: If a directory exists with the same name as your target, it is still treated as a real file/directory by make [8]. If you want a rule to build a directory (e.g., creating it with mkdir), do not mark it as.PHONY; instead, consider using order-only prerequisites if you only want the directory to exist before other files are created [8].
Citations:
- 1: https://stackoverflow.com/questions/2145590/what-is-the-purpose-of-phony-in-a-makefile
- 2: https://www.tack.ch/gnu/make/make_34.html
- 3: https://www.manpagez.com/info/make/make-4.4.1/Phony_Targets.php
- 4: https://stackoverflow.com/questions/13852535/makefile-dependencies-dont-work-for-phony-target
- 5: https://web.mit.edu/gnu/doc/html/make_4.html
- 6: http://ftp.twaren.net/Unix/GNU/old-gnu/Manuals/make-3.80/html_node/make_37.html
- 7: https://stackoverflow.com/questions/76701908/makefile-not-detecting-that-target-executable-has-already-been-built-gnu-make
- 8: https://stackoverflow.com/questions/4248300/in-a-makefile-is-a-directory-name-a-phony-target-or-real-target
Declare build and url as phony targets.
Both targets define Docker/printing actions rather than files, but they are only referenced from .PHONY for real files. If build or url exists, make build / make url can be skipped instead of running their recipes.
Proposed fix
-.PHONY: help up down restart logs-api logs-web shell-api shell-web migrate makemigrations
+.PHONY: help up build down restart logs-api logs-web shell-api shell-web migrate makemigrations url📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| .PHONY: help up down restart logs-api logs-web shell-api shell-web migrate makemigrations | |
| .PHONY: help up build down restart logs-api logs-web shell-api shell-web migrate makemigrations url |
| ifneq ($(CURRENT_BRANCH),main) | ||
| # Get the version from the frontend package.json for identifying the Docker image. | ||
| PKG_VERSION := $(shell node -p "require('./frontend/package.json').version") | ||
| IMAGE_TAG := v$(PKG_VERSION) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
make -n helpRepository: plan-cake/plancake
Length of output: 198
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf 'Makefile lines 1-40:\n'
sed -n '1,45p' Makefile | cat -n
printf '\nMakefile indentation around conditional:\n'
python3 - <<'PY'
from pathlib import Path
p = Path("Makefile")
text = p.read_text(errors="replace").splitlines()
for i, line in enumerate(text[max(0,6):min(len(text),15)], start=max(0,6)+1):
print(f"{i}: {line!r} chars before content: {len(line)-len(line.lstrip(chr(9)))} {'tab'*(len(line)-len(line.lstrip(chr(9)))>0) else 'spaces'}")
PYRepository: plan-cake/plancake
Length of output: 2834
Remove the leading tabs from the conditional Makefile assignments.
PKG_VERSION := ... and IMAGE_TAG := ... must not be indented with tabs at the top level; tab-indented lines are parsed as recipes, which makes make fail before targets can run.
| # Restarts the containers without rebuilding them. Use this for things like env changes. | ||
| restart: | ||
| docker compose restart |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Makefile around restart target =="
nl -ba Makefile | sed -n '45,68p'
echo
echo "== Search for restart target / docker compose usage =="
rg -n "restart:|docker compose|FORCE|env changes" Makefile .github -S || true
echo
echo "== Local repository version files if any =="
git ls-files | rg '(^|/)(Makefile|compose.*\.ya?ml|\.env|docker-compose\.ya?ml)$' || trueRepository: plan-cake/plancake
Length of output: 233
🌐 Web query:
Docker Compose restart up --force-recreate environment variables .env documentation
💡 Result:
When using the docker compose up command, the --force-recreate flag instructs Docker Compose to stop and recreate all containers, even if their configuration or image has not changed [1][2]. This is particularly useful when you need to ensure that environment variable changes or other configuration updates are applied to the containers [3]. Regarding environment variables and the.env file: 1. Behavior with --force-recreate: By default, Docker Compose compares the current configuration (including environment variables) of running containers with the desired configuration in your compose.yaml [1]. If it detects a difference, it automatically recreates them [1][3]. However, using --force-recreate guarantees that the containers are replaced regardless of whether Compose detects a change, ensuring that the environment defined by your current.env file or other environment settings is applied [1][2]. 2..env File Precedence: Docker Compose automatically loads a default.env file from the project directory [4][5]. If you need to use a different file, you can specify it using the --env-file option [4][6]. 3. Precedence Rules: Environment variables are resolved based on a specific order of precedence [5][7]. Generally, variables defined in the environment attribute of your compose.yaml file take precedence over those loaded from an env_file [5][8]. If you are changing environment variables and the container does not seem to update, ensure you are running the command in the same directory as your compose.yaml or specify the path to your configuration file correctly [4]. If the container persists old state (e.g., due to anonymous volumes), you may also need to consider flags like --renew-anon-volumes to fully refresh the container's state [9].
Citations:
- 1: https://docs.docker.com/reference/cli/docker/compose/up/
- 2: https://github.com/docker/compose/blob/28120148f4e1380096407626f846eda8f6970f36/docs/reference/up.md
- 3: https://stackoverflow.com/questions/53171334/docker-compose-up-not-recreate-container
- 4: https://docs.docker.com/compose/how-tos/environment-variables/variable-interpolation/
- 5: https://docs.docker.com/compose/how-tos/environment-variables/envvars-precedence/
- 6: https://docs.docker.com/compose/how-tos/environment-variables/set-environment-variables/
- 7: https://docs.docker.com/compose/how-tos/environment-variables/envvars/
- 8: https://docs.docker.com/reference/compose-file/services
- 9: [BUG] --force-recreate does not use the newly built image docker/compose#11945
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Files matching relevant names =="
git ls-files 2>/dev/null || true \
| grep -E '(^|/)(Makefile|compose.*\.ya?ml|docker-compose\.ya?ml|\.env(_[^/]+)?)$' || true
echo
echo "== Makefile around restart target =="
awk 'NR>=45 && NR<=70 { printf "%6d\t%s\n", NR, $0 }' Makefile 2>/dev/null || true
echo
echo "== Search restart / docker compose / env changes =="
grep -n -- "restart:|docker compose|env changes" Makefile Makefile.lock 2>/dev/null | head -50 || trueRepository: plan-cake/plancake
Length of output: 16153
Recreate containers when applying environment changes.
docker compose restart only restarts existing containers and does not apply changed .env values or updated Compose configuration, contradicting the target comment. Use docker compose up -d --force-recreate instead.
There was a problem hiding this comment.
Pull request overview
This PR introduces a Docker-based local development ecosystem (frontend, backend, Redis) plus supporting developer tooling (Make targets), CI workflows for publishing/retaining images, and documentation updates to standardize setup and improve reproducibility across contributors.
Changes:
- Adds Docker Compose stack and Dockerfiles for frontend/backend, plus
.dockerignorefiles to keep build contexts small. - Adds a
Makefileand helper scripts to simplify common Docker workflows (up/build/down/logs/shell/migrations). - Adds GitHub Actions workflows for image publishing, merge protection, and registry cleanup; updates docs/README for the new workflow.
Reviewed changes
Copilot reviewed 20 out of 21 changed files in this pull request and generated 11 comments.
Show a summary per file
| File | Description |
|---|---|
| scripts/print-ip.js | Adds a helper script to print network-accessible URLs for local dev. |
| README.md | Expands root documentation with Docker + Make-based quick start and project overview. |
| Makefile | Introduces Make targets wrapping Docker Compose workflows and URL printing. |
| frontend/start.sh | Adds a container entrypoint script for dev startup. |
| frontend/src/lib/utils/api/server-fetch.ts | Updates server-side API base URL selection to support Docker internal networking. |
| frontend/package.json | Bumps frontend version (used by release/branch/versioning workflows). |
| frontend/package-lock.json | Updates lockfile version fields to match the new package.json version. |
| frontend/next.config.ts | Adjusts dev rewrite destination to use INTERNAL_API_URL when present. |
| frontend/Dockerfile.dev | Adds a frontend development image build definition. |
| frontend/Dockerfile | Adds a multi-stage frontend production image using Next.js standalone output. |
| frontend/.dockerignore | Adds frontend Docker build context exclusions. |
| docs/docker.md | Adds detailed documentation of the Docker architecture, workflows, and known gaps. |
| docker-compose.yml | Defines the Compose stack (frontend/backend/redis), volumes, and Docker-network env wiring. |
| backend/start.sh | Adds a backend dev startup script (deps/migrations/uvicorn reload). |
| backend/Dockerfile .dev | Adds a backend dev Dockerfile (note: filename currently includes a space). |
| backend/Dockerfile | Adds a backend production Dockerfile (non-root user). |
| backend/api/settings.py | Makes Redis URLs configurable via environment variables for Docker compatibility. |
| backend/.dockerignore | Adds backend Docker build context exclusions. |
| .github/workflows/publish-images.yml | Adds CI workflow to build/push images to GHCR. |
| .github/workflows/merge-protect.yml | Adds merge gating + package-version-vs-branch-name enforcement for releases. |
| .github/workflows/clean-up.yml | Adds scheduled pruning of old container images in GHCR. |
Files not reviewed (1)
- frontend/package-lock.json: Generated file
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| @@ -0,0 +1,9 @@ | |||
| import os from "os"; | |||
| # Determine the Docker image tag based on the current Git branch. If the branch is not | ||
| # "main", use the version from the frontend package.json to identify the Docker image. | ||
| # Otherwise, default to latest. | ||
| CURRENT_BRANCH := $(shell git rev-parse --abbrev-ref HEAD) | ||
| IMAGE_TAG := latest | ||
| ifneq ($(CURRENT_BRANCH),main) | ||
| # Get the version from the frontend package.json for identifying the Docker image. | ||
| PKG_VERSION := $(shell node -p "require('./frontend/package.json').version") | ||
| IMAGE_TAG := v$(PKG_VERSION) | ||
| endif |
| shell-api: | ||
| docker compose exec -it backend /bin/bash |
| build: | ||
| context: ./backend | ||
| dockerfile: Dockerfile.dev | ||
| command: ["sh", "./start.sh"] |
| # Create a non-root user and switch to it | ||
| RUN addgroup --system appgroup && adduser --system --group user | ||
| USER user |
| // Inside serverGet and serverPost: | ||
| const baseUrl = | ||
| process.env.INTERNAL_API_URL || process.env.NEXT_PUBLIC_API_URL; |
| - name: Check package version | ||
| if: github.base_ref == 'main' | ||
| run: | | ||
| # Extract the version from package.json | ||
| PACKAGE_VERSION=$(jq -r '.version' frontend/package.json) |
In light of live updates and for other future development changes, docker might be better for maintaining easy reproducibility of our code. This PR set up a docker ecosystem for the project (frontend, backend, Redis), plus the Make commands, CI workflows, and docs needed to support it.
Docker Images
./backend/Dockerfile- production image (non-root user, Uvicorn)../frontend/Dockerfile- multi-stage production build using Next.js standalone output../backend/Dockerfile.dev- dev image, used by docker-compose./frontend/Dockerfile.dev- dev image (npm run dev), used by docker-compose.dockerignore- like.gitignoreDocker Compose
docker-compose.yml defines frontend, backend, and redis services, wired together over the Docker network, with bind mounts for hot-reloading and named volumes (redis_data, node_modules, next_cache) to avoid reinstalling/rebuilding on every restart.
Make Commands
Added a Makefile wrapping common workflows: up, build, down, restart, log streaming, container shells, and running/creating Django migrations. Run
make helpfor the full list. This was meant to make the developer experience a little smoother. Ideally, all you would need to remember are these make commands.CI/CD
publish-images.yml- builds and pushes both images to GHCR on pushes to main/version branches, multi-platform (amd64/arm64) with GHA build caching.clean-up.yml-weekly scheduled job to prune old images from the registry, keeping the most recent few.merge-protect.yml- gates PRs into main to version branches only, and checks that the branch name matches frontend/package.json's version.Docs
Updated the root
README.mdwith a proper Docker setup section (services overview, quick start, trimmed Make command table).Added
./docs/docker.mdwith a deeper explanation of the architecture, image build strategy, Compose file, CI workflows, and a "Known Gaps" section for follow-up work.