Skip to content

Docker - #364

Open
mirmirmirr wants to merge 46 commits into
v0.5.0from
docker
Open

Docker#364
mirmirmirr wants to merge 46 commits into
v0.5.0from
docker

Conversation

@mirmirmirr

@mirmirmirr mirmirmirr commented Jul 4, 2026

Copy link
Copy Markdown
Member

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 .gitignore

Docker 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 help for 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.md with a proper Docker setup section (services overview, quick start, trimmed Make command table).

Added ./docs/docker.md with a deeper explanation of the architecture, image build strategy, Compose file, CI workflows, and a "Known Gaps" section for follow-up work.

@mirmirmirr
mirmirmirr marked this pull request as ready for review July 25, 2026 15:03
@mirmirmirr
mirmirmirr requested a review from Copilot July 25, 2026 15:07
@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

The 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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 1c91c59 and 6cdd712.

⛔ Files ignored due to path filters (1)
  • frontend/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (20)
  • .github/workflows/clean-up.yml
  • .github/workflows/merge-protect.yml
  • .github/workflows/publish-images.yml
  • Makefile
  • README.md
  • backend/.dockerignore
  • backend/Dockerfile
  • backend/Dockerfile .dev
  • backend/api/settings.py
  • backend/start.sh
  • docker-compose.yml
  • docs/docker.md
  • frontend/.dockerignore
  • frontend/Dockerfile
  • frontend/Dockerfile.dev
  • frontend/next.config.ts
  • frontend/package.json
  • frontend/src/lib/utils/api/server-fetch.ts
  • frontend/start.sh
  • scripts/print-ip.js

Comment thread .github/workflows/clean-up.yml
Comment thread .github/workflows/clean-up.yml
Comment thread .github/workflows/merge-protect.yml Outdated
Comment thread .github/workflows/publish-images.yml Outdated
Comment thread .github/workflows/publish-images.yml Outdated
Comment thread frontend/Dockerfile.dev
Comment on lines +3 to +14
FROM node:22-alpine

WORKDIR /app

COPY package.json package-lock.json* ./
RUN npm ci

COPY . .

EXPOSE 3000

CMD ["npm", "run", "dev"]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 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.

Suggested change
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

Comment on lines +35 to +36
const baseUrl =
process.env.INTERNAL_API_URL || process.env.NEXT_PUBLIC_API_URL;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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' || true

Repository: 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 }));
  }
}
JS

Repository: 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.

Comment thread Makefile
@@ -0,0 +1,95 @@

.PHONY: help up down restart logs-api logs-web shell-api shell-web migrate makemigrations

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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 || true

Repository: 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 || true

Repository: 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:


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.

Suggested change
.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

Comment thread Makefile
Comment on lines +9 to +12
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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

make -n help

Repository: 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'}")
PY

Repository: 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.

Comment thread Makefile
Comment on lines +57 to +59
# Restarts the containers without rebuilding them. Use this for things like env changes.
restart:
docker compose restart

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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)$' || true

Repository: 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:


🏁 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 || true

Repository: 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.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 .dockerignore files to keep build contexts small.
  • Adds a Makefile and 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.

Comment thread scripts/print-ip.js
@@ -0,0 +1,9 @@
import os from "os";
Comment thread Makefile
Comment on lines +4 to +13
# 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
Comment thread Makefile
Comment thread Makefile
Comment on lines +86 to +87
shell-api:
docker compose exec -it backend /bin/bash
Comment thread docker-compose.yml
Comment on lines +11 to +14
build:
context: ./backend
dockerfile: Dockerfile.dev
command: ["sh", "./start.sh"]
Comment thread backend/Dockerfile
Comment on lines +18 to +20
# Create a non-root user and switch to it
RUN addgroup --system appgroup && adduser --system --group user
USER user
Comment thread .github/workflows/merge-protect.yml
Comment on lines +73 to +75
// Inside serverGet and serverPost:
const baseUrl =
process.env.INTERNAL_API_URL || process.env.NEXT_PUBLIC_API_URL;
Comment thread docs/docker.md
Comment on lines +23 to +27
- name: Check package version
if: github.base_ref == 'main'
run: |
# Extract the version from package.json
PACKAGE_VERSION=$(jq -r '.version' frontend/package.json)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants