-
Notifications
You must be signed in to change notification settings - Fork 12
Add Docker support with multi-stage Dockerfile and compose config #111
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| name: Docker Image CI | ||
|
|
||
| on: | ||
| push: | ||
| branches: [ "main" ] | ||
| pull_request: | ||
| branches: [ "main" ] | ||
|
|
||
| jobs: | ||
| build-and-push: | ||
| runs-on: ubuntu-latest | ||
|
|
||
| steps: | ||
| - name: Checkout repository | ||
| uses: actions/checkout@v4 | ||
| with: | ||
| persist-credentials: false | ||
|
|
||
| - name: Log in to Docker Hub | ||
| if: github.event_name == 'push' | ||
| uses: docker/login-action@v3 | ||
| with: | ||
| username: ${{ secrets.DOCKER_USERNAME }} | ||
| password: ${{ secrets.DOCKER_PASSWORD }} | ||
|
|
||
| - name: Build and push Docker image | ||
| uses: docker/build-push-action@v6 | ||
| with: | ||
| context: . | ||
| file: ./Dockerfile | ||
| target: builder | ||
| push: ${{ github.event_name == 'push' }} | ||
| tags: callofcode07/callofcode:latest | ||
| # change this if using this for production | ||
| build-args: | | ||
| API_BASE_URL=http://coc-api:3000 | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,207 @@ | ||
| # Docker — Local Development Guide | ||
|
|
||
| This document explains how to spin up the full **Call of Code** local development environment using Docker Compose. | ||
|
|
||
| The stack consists of two services running on a shared Docker network (`coc-local`): | ||
|
|
||
| | Service | Image / Source | Port | | ||
| | ----------- | --------------------------------------- | ------ | | ||
| | `coc-api` | `callofcode07/coc-api:latest` (Docker Hub) | 3000 | | ||
| | `frontend` | Built locally from this repo (`Dockerfile`) | 3001 | | ||
|
|
||
| --- | ||
|
|
||
| ## Prerequisites | ||
|
|
||
| - [Docker Desktop](https://www.docker.com/products/docker-desktop/) ≥ 24 **or** Docker Engine + Docker Compose plugin ≥ 2.22 | ||
| - Git | ||
|
|
||
| --- | ||
|
|
||
| ## Quick Start | ||
|
|
||
| ### 1. Configure environment variables | ||
|
|
||
| The Compose setup reads env files from the `docker/` directory. Copy the example files and fill in your values: | ||
|
|
||
| ```bash | ||
| # COC API service | ||
| cp docker/.env.local.coc-api.example docker/.env.local.coc-api | ||
|
|
||
| # Frontend service | ||
| cp docker/.env.local.frontend.example docker/.env.local.frontend | ||
| ``` | ||
|
|
||
| > **Never commit** the real `docker/.env.local.*` files — they are already listed in `.gitignore`. | ||
|
|
||
| ### 2. Start the services | ||
|
|
||
| ```bash | ||
| docker compose up --build | ||
| ``` | ||
|
|
||
| | Flag | Effect | | ||
| | ----------- | ------------------------------------------------- | | ||
| | `--build` | (Re)build the frontend image before starting | | ||
| | `--watch` | Enable hot-reload — see [Hot Reload](#hot-reload) | | ||
| | `-d` | Run in the background (detached mode) | | ||
|
|
||
| The frontend will be available at **http://localhost:3001** once the `coc-api` health check passes. | ||
|
|
||
| --- | ||
|
|
||
| ## Environment Variables | ||
|
|
||
| ### `docker/.env.local.coc-api` | ||
|
|
||
| Consumed by the `coc-api` container. Credentials for the Supabase / Postgres backend. | ||
|
|
||
| | Variable | Description | | ||
| | ------------------------- | --------------------------------------------------------- | | ||
| | `DATABASE_URL` | Postgres pooler connection string (used at runtime) | | ||
| | `DIRECT_URL` | Postgres direct connection string (used for migrations) | | ||
| | `SUPABASE_URL` | Your Supabase project URL | | ||
| | `SUPABASE_SERVICE_ROLE_KEY` | Supabase service-role JWT (keep this secret!) | | ||
| | `NODE_ENV` | Set to `development` for local use | | ||
|
|
||
| Example: | ||
| ```dotenv | ||
| DATABASE_URL=postgresql://postgres.<ref>:<password>@aws-0-ap-south-1.pooler.supabase.com:5432/postgres | ||
| DIRECT_URL=postgresql://postgres.<ref>:<password>@aws-0-ap-south-1.pooler.supabase.com:5432/postgres | ||
| SUPABASE_URL=https://<ref>.supabase.co | ||
| SUPABASE_SERVICE_ROLE_KEY=<your-service-role-key> | ||
| NODE_ENV=development | ||
| ``` | ||
|
|
||
| --- | ||
|
|
||
| ### `docker/.env.local.frontend` | ||
|
|
||
| Consumed by the `frontend` container at runtime. | ||
|
|
||
| | Variable | Description | | ||
| | -------------- | --------------------------------------------------- | | ||
| | `API_BASE_URL` | URL the frontend uses to reach the API. Within the Docker network this is `http://coc-api:3000` | | ||
| | `GITHUB_TOKEN` | GitHub personal access token (optional, for contribution graphs) | | ||
|
|
||
| Example: | ||
| ```dotenv | ||
| API_BASE_URL=http://coc-api:3000 | ||
| GITHUB_TOKEN=ghp_xxxxxxxxxxxxxxxxxxxx | ||
| ``` | ||
|
|
||
| --- | ||
|
|
||
| ## Hot Reload | ||
|
|
||
| The Compose file uses Docker's `develop.watch` feature to sync source files into the running container **without a full rebuild**. | ||
|
|
||
| Start with watch mode enabled: | ||
|
|
||
| ```bash | ||
| docker compose up --watch | ||
| ``` | ||
|
|
||
| Synced paths: | ||
|
|
||
| | Local path | Container path | Action | | ||
| | -------------- | ----------------- | ------- | | ||
| | `./app` | `/app/app` | `sync` | | ||
| | `./components` | `/app/components` | `sync` | | ||
| | `./lib` | `/app/lib` | `sync` | | ||
| | `./public` | `/app/public` | `sync` | | ||
| | `package.json` / `package-lock.json` | — | `rebuild` (triggers a full image rebuild) | | ||
|
|
||
| > **Note:** `sync` changes are reflected instantly. Dependency changes (`package.json`) trigger a full rebuild automatically. | ||
|
|
||
| --- | ||
|
|
||
| ## Dockerfile Stages | ||
|
|
||
| The multi-stage `Dockerfile` has three stages: | ||
|
|
||
| | Stage | Base Image | Purpose | | ||
| | --------- | ----------------- | ------------------------------------------------ | | ||
| | `deps` | `node:20-alpine` | Install `node_modules` with `npm ci` | | ||
| | `builder` | `node:20-alpine` | Copy deps + source, run `npm run build`. **Used by Compose in dev** (keeps dev deps intact). | | ||
| | `runner` | `node:20-alpine` | Lean production image — only production artefacts | | ||
|
|
||
| The Compose file targets the `builder` stage so that dev dependencies (like TypeScript types) remain available inside the container. | ||
|
|
||
| --- | ||
|
|
||
| ## Useful Commands | ||
|
|
||
| ```bash | ||
| # Start all services (foreground) | ||
| docker compose up --build | ||
|
|
||
| # Start with hot-reload | ||
| docker compose up --build --watch | ||
|
|
||
| # Start in background | ||
| docker compose up -d --build | ||
|
|
||
| # View logs for a specific service | ||
| docker compose logs -f frontend | ||
| docker compose logs -f coc-api | ||
|
|
||
| # Stop all services | ||
| docker compose down | ||
|
|
||
| # Stop and remove volumes | ||
| docker compose down -v | ||
|
|
||
| # Rebuild only the frontend image | ||
| docker compose build frontend | ||
|
|
||
| # Open a shell inside the frontend container | ||
| docker compose exec frontend sh | ||
|
|
||
| # Check service health | ||
| docker compose ps | ||
| ``` | ||
|
|
||
| --- | ||
|
|
||
| ## Service Health Check | ||
|
|
||
| The `coc-api` container exposes a health endpoint at `GET /health`. Docker polls it every **15 seconds** (3 retries, 5 s timeout, 15 s start period). The `frontend` service will not start until `coc-api` is reported **healthy**. | ||
|
|
||
| ```yaml | ||
| healthcheck: | ||
| test: ["CMD", "wget", "-qO-", "http://localhost:3000/health"] | ||
| interval: 15s | ||
| timeout: 5s | ||
| retries: 3 | ||
| start_period: 15s | ||
| ``` | ||
|
|
||
| --- | ||
|
|
||
| ## Troubleshooting | ||
|
|
||
| ### Frontend can't reach the API | ||
|
|
||
| - Confirm `docker/.env.local.frontend` has `API_BASE_URL=http://coc-api:3000`. | ||
| - Check that `coc-api` is healthy: `docker compose ps`. | ||
| - Inspect API logs: `docker compose logs coc-api`. | ||
|
|
||
| ### Port already in use | ||
|
|
||
| Change the host-side port mapping in `docker-compose.yml`: | ||
| ```yaml | ||
| ports: | ||
| - "3002:3001" # map host 3002 → container 3001 | ||
| ``` | ||
|
|
||
| ### Hot-reload not working | ||
|
|
||
| Ensure you started with `--watch`: `docker compose up --watch`. The feature requires Docker Compose ≥ 2.22. | ||
|
|
||
| ### Pulling a fresh copy of the API image | ||
|
|
||
| ```bash | ||
| docker compose pull coc-api | ||
| docker compose up --build | ||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| # Stage 1 – deps: install node_modules with npm ci for reproducibility | ||
| FROM node:20-alpine AS deps | ||
| WORKDIR /app | ||
|
|
||
| COPY package.json package-lock.json ./ | ||
| RUN npm ci | ||
|
|
||
|
|
||
| # Stage 2 – builder: compile the Next.js application | ||
| FROM node:20-alpine AS builder | ||
| WORKDIR /app | ||
|
|
||
| COPY --from=deps /app/node_modules ./node_modules | ||
| COPY . . | ||
|
|
||
| # Build-time env vars (non-secret, baked into the bundle) | ||
| ARG API_BASE_URL | ||
| ENV API_BASE_URL=$API_BASE_URL | ||
|
|
||
| RUN npm run build | ||
|
|
||
| # Stage 3 – runner: lean production image | ||
| FROM node:20-alpine AS runner | ||
| WORKDIR /app | ||
|
|
||
| ENV NODE_ENV=production | ||
|
|
||
| # Create a non-root group and user to run the application securely | ||
| RUN addgroup --system --gid 1001 nodejs && \ | ||
| adduser --system --uid 1001 --ingroup nodejs nextjs | ||
|
|
||
| # Next.js standalone output (enable in next.config if needed) | ||
| # COPY --from=builder /app/.next/standalone ./ | ||
| # COPY --from=builder /app/.next/static ./.next/static | ||
| # COPY --from=builder /app/public ./public | ||
|
|
||
| # Standard (non-standalone) output | ||
| # node_modules sourced from deps stage; .next artefacts from builder | ||
| COPY --from=builder /app/public ./public | ||
| COPY --from=builder /app/.next ./.next | ||
| COPY --from=deps /app/node_modules ./node_modules | ||
| COPY --from=builder /app/package.json ./package.json | ||
|
|
||
| # Transfer ownership of the working directory to the non-root user | ||
| RUN chown -R nextjs:nodejs /app | ||
|
|
||
| # Drop privileges — never run production containers as root | ||
| USER nextjs | ||
|
|
||
| EXPOSE 3001 | ||
|
|
||
| CMD ["npm", "run", "start"] | ||
|
coderabbitai[bot] marked this conversation as resolved.
Comment on lines
+22
to
+52
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win Runtime environment variable API_BASE_URL is not available in the runner stage. The builder stage sets The upstream code ( Recommendation: Either:
🔧 Proposed fix: Accept API_BASE_URL at runtime # Stage 3 – runner: lean production image
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
+ENV API_BASE_URL="" # Set at runtime via -e or .env
# Next.js standalone output (enable in next.config if needed)🤖 Prompt for AI Agents |
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟡 Minor
🧩 Analysis chain
🏁 Script executed:
# First, let's examine the docker-image.yml workflow file to see the full context cat -n .github/workflows/docker-image.ymlRepository: call-0f-code/call-of-code
Length of output: 1155
🏁 Script executed:
# Now examine docker-compose.yml to see what images it references cat -n docker-compose.ymlRepository: call-0f-code/call-of-code
Length of output: 2145
🏁 Script executed:
Repository: call-0f-code/call-of-code
Length of output: 191
🏁 Script executed:
Repository: call-0f-code/call-of-code
Length of output: 163
🏁 Script executed:
Repository: call-0f-code/call-of-code
Length of output: 1481
🏁 Script executed:
Repository: call-0f-code/call-of-code
Length of output: 187
🏁 Script executed:
# Check DOCKER.md for documentation about the image naming and architecture cat -n DOCKER.mdRepository: call-0f-code/call-of-code
Length of output: 7972
🏁 Script executed:
Repository: call-0f-code/call-of-code
Length of output: 1655
Published image name doesn't match what the dev stack uses; align or document the separation.
The workflow publishes to
callofcode07/callofcode:latest, but the dev stack indocker-compose.ymlpullscallofcode07/coc-api:latestfor the API and builds the frontend locally ascoc-frontend:dev. The publishedcallofcode07/callofcodeimage is unused in the local development workflow, making this an orphaned build artifact. PerDOCKER.md, the stack has no reference to this image.Additionally, publishing only
:latestleaves no immutable tag for rollbacks or debugging past deployments.Consider either:
🔧 Suggested tagging
📝 Committable suggestion
🤖 Prompt for AI Agents