feat(tools): unified container dev setup (#65589)

This commit is contained in:
Mrugesh Mohapatra
2026-02-08 12:20:52 +05:30
committed by GitHub
parent b321f075fd
commit 46b607d84a
10 changed files with 337 additions and 148 deletions

31
.devcontainer/README.md Normal file
View File

@@ -0,0 +1,31 @@
# To start developing:
Wait for the container to build and start. You will see "Done. Press any key to close the terminal." in the terminal when it's ready.
Once it's running, you can start the development server:
**Option 1:** Press `Ctrl+Shift+P`, type "Run Task", select "Start Development"
**Option 2:** Open a terminal and run:
```bash
pnpm run develop
```
## Optional setup
For E2E tests:
```bash
npx playwright install chromium
```
For curriculum tests:
```bash
pnpm -F=curriculum install-puppeteer
```
## More information
For detailed setup instructions and contribution guidelines, visit:
https://contribute.freecodecamp.org/how-to-setup-freecodecamp-locally

View File

@@ -1,8 +1,11 @@
{
"name": "freeCodeCampDC",
"name": "freeCodeCamp",
"dockerComposeFile": "docker-compose.yml",
"service": "devcontainer",
"workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",
"workspaceFolder": "/workspaces/freeCodeCamp",
"mounts": [
"source=fcc-node-modules,target=${containerWorkspaceFolder}/node_modules,type=volume"
],
"forwardPorts": [3000, 8000],
"portsAttributes": {
"3000": {
@@ -17,9 +20,64 @@
"otherPortsAttributes": {
"onAutoForward": "silent"
},
"onCreateCommand": "[ ! -f .env ] && cp sample.env .env || true",
"updateContentCommand": "pnpm install && pnpm seed",
"postAttachCommand": {
"instructions": "bash -c 'echo \"\n\n\n Start a new terminal and run \\`pnpm run develop\\` when you are ready.\n\n\n\"'"
"onCreateCommand": "sudo chown node:node node_modules && ([ ! -f .env ] && cp sample.env .env || true)",
"updateContentCommand": "pnpm install --prefer-offline",
"postCreateCommand": "rsync -a --include='*/' --include='.turbo/***' --exclude='*' /home/node/.cache/fcc/ ./ && set -a && . ./.env && set +a && until mongosh --eval 'rs.status().ok' 2>/dev/null; do sleep 1; done && pnpm seed",
"customizations": {
"vscode": {
"extensions": [
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode"
],
"settings": {
"task.allowAutomaticTasks": "on",
"tasks": {
"version": "2.0.0",
"tasks": [
{
"label": "Start API Server",
"type": "shell",
"command": "pnpm run develop:api",
"isBackground": true,
"problemMatcher": [],
"presentation": {
"reveal": "always",
"panel": "dedicated",
"group": "develop"
}
},
{
"label": "Start Client Server",
"type": "shell",
"command": "pnpm run develop:client",
"isBackground": true,
"problemMatcher": [],
"presentation": {
"reveal": "always",
"panel": "dedicated",
"group": "develop"
}
},
{
"label": "Start Development",
"dependsOn": ["Start API Server", "Start Client Server"],
"problemMatcher": []
},
{
"label": "Open README",
"type": "shell",
"command": "code .devcontainer/README.md",
"presentation": {
"reveal": "silent",
"close": true
},
"runOptions": {
"runOn": "folderOpen"
}
}
]
}
}
}
}
}

View File

@@ -1,17 +1,16 @@
services:
devcontainer:
image: ghcr.io/freecodecamp/devcontainer:latest
depends_on:
- db
- setup
image: mcr.microsoft.com/devcontainers/typescript-node:22
volumes:
- ../..:/workspaces:cached
- ..:/workspaces/freeCodeCamp:cached
network_mode: service:db
command: sleep infinity
db:
image: mongo:8.0
container_name: mongodb
command: mongod --replSet rs0
restart: unless-stopped
hostname: mongodb
@@ -21,27 +20,20 @@ services:
test: ['CMD', 'mongosh', '--eval', "db.adminCommand('ping')"]
interval: 2s
retries: 5
start_period: 10s
setup:
image: mongo:8.0
depends_on:
db:
condition: service_healthy
restart: on-failure
# This will try to initiate the replica set, until it succeeds twice (i.e. until the replica set is already initialized)
restart: on-failure:5
command: >
mongosh --host mongodb:27017 --eval '
var cfg = {
rs.initiate({
_id: "rs0",
members: [
{ _id: 0, host: "mongodb:27017" }
]
};
try {
rs.initiate(cfg);
} catch (err) {
if(err.codeName !== "AlreadyInitialized") throw err;
}
members: [{ _id: 0, host: "mongodb:27017" }]
}).ok || rs.status().ok
'
volumes:

63
.github/workflows/devcontainer-ci.yml vendored Normal file
View File

@@ -0,0 +1,63 @@
name: CI - Devcontainer
on:
pull_request:
paths:
- '.devcontainer/**'
- 'docker/devcontainer/**'
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
permissions:
contents: read
packages: read
jobs:
validate:
name: Validate
runs-on: ubuntu-24.04
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Login to GHCR
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Install devcontainer CLI
# renovate: datasource=npm depName=@devcontainers/cli
run: npm install -g @devcontainers/cli@0.83.0
- name: Build devcontainer
run: devcontainer build --workspace-folder .
- name: Start devcontainer
run: devcontainer up --workspace-folder .
- name: Validate required tools
run: |
devcontainer exec --workspace-folder . pnpm --version
devcontainer exec --workspace-folder . rsync --version
devcontainer exec --workspace-folder . mongosh --version
devcontainer exec --workspace-folder . node --version
devcontainer exec --workspace-folder . git --version
- name: Validate MongoDB replica set
run: |
for i in $(seq 1 30); do
if devcontainer exec --workspace-folder . mongosh --eval "rs.status().ok" 2>/dev/null; then
echo "Replica set is ready"
exit 0
fi
echo "Waiting for replica set... (attempt $i/30)"
sleep 2
done
echo "Replica set failed to initialize"
exit 1

View File

@@ -1,4 +1,4 @@
name: CD - Docker - GHCR (Gitpod)
name: CD - Docker - GHCR Images
on:
workflow_dispatch:
@@ -6,55 +6,44 @@ on:
branches:
- main
paths:
- 'docker/gitpod/*'
- 'pnpm-lock.yaml'
- 'docker/devcontainer/**'
- '.github/workflows/docker-ghcr.yml'
permissions:
contents: read
packages: write
jobs:
build-and-push-image:
build-and-push:
name: Build and Push Images
runs-on: ubuntu-24.04
permissions:
contents: read
packages: write
strategy:
fail-fast: false
matrix:
images:
- gitpod
steps:
- name: Checkout code
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
submodules: 'recursive'
- name: Set up QEMU
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3
- name: Log in to the GHCR
- name: Log in to GHCR
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Cache Docker layers
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
- name: Build and push images
uses: docker/bake-action@5be5f02ff8819ecd3092ea6b2e6261c31774f2b4 # v6
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-${{ matrix.images }}-${{ github.sha }}
restore-keys: |
${{ runner.os }}-buildx-${{ matrix.images }}-
- name: Build and push Docker image
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
with:
context: ./docker/${{ matrix.images }}
files: docker/devcontainer/docker-bake.hcl
targets: devcontainer
push: true
tags: |
ghcr.io/freecodecamp/${{ matrix.images }}:${{ github.sha }}
ghcr.io/freecodecamp/${{ matrix.images }}:latest
cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max
- name: Move cache
run: |
rm -rf /tmp/.buildx-cache
mv /tmp/.buildx-cache-new /tmp/.buildx-cache
env:
TAG: ${{ github.sha }}
TAG_LATEST: ${{ github.ref_name == 'main' }}

View File

@@ -1,78 +0,0 @@
image: ghcr.io/freecodecamp/gitpod:latest
ports:
- port: 27017 # mongodb
onOpen: ignore
- port: 8000 # client
onOpen: notify
visibility: public
- port: 9228 # node debug
onOpen: ignore
- port: 3000 # api
onOpen: ignore
visibility: public
- port: 9229 # node debug
onOpen: ignore
- port: 9230 # client node debug
onOpen: ignore
- port: 3200 # challenge editor api
visibility: public
- port: 3300 # challenge editor client
visibility: public
- port: 8025 # Mailpit
visibility: public
onOpen: ignore
- port: 1025 # Mailpit
onOpen: ignore
- port: 9323 # Playwright
visibility: public
onOpen: ignore
tasks:
- before: |
echo '
export COOKIE_DOMAIN=.gitpod.io
export HOME_LOCATION=$(gp url 8000)
export API_LOCATION=$(gp url 3000)
export CHALLENGE_EDITOR_API_LOCATION=$(gp url 3200)
export CHALLENGE_EDITOR_CLIENT_LOCATION=$(gp url 3300)
export CHALLENGE_EDITOR_LEARN_CLIENT_LOCATION=$(gp url 8000)
' >> ~/.bashrc;
exit;
- name: db
# starting mongod in background, so it doesn't block prebuilds
before: >
docker compose -f docker/docker-compose.yml up -d
- name: server
before: export COOKIE_DOMAIN=.gitpod.io && export HOME_LOCATION=$(gp url 8000) && export API_LOCATION=$(gp url 3000)
# init is not executed for prebuilt workspaces and restarts,
# so we should put all the heavy initialization here
init: >
cp sample.env .env &&
pnpm install &&
gp sync-done pnpm-install &&
pnpm run build:curriculum &&
gp ports await 27017
command: >
pnpm run seed &&
mongosh --eval "db.fsyncLock(); db.fsyncUnlock()" &&
gp ports await 27017 &&
cd api &&
pnpm run develop
- name: client
before: export HOME_LOCATION=$(gp url 8000) && export API_LOCATION=$(gp url 3000)
init: >
cd ./client &&
gp sync-await pnpm-install &&
cd ..
command: >
gp ports await 3000 &&
pnpm run develop:client -- -H '0.0.0.0'
openMode: split-right
vscode:
extensions:
- dbaeumer.vscode-eslint
- esbenp.prettier-vscode

View File

@@ -0,0 +1,97 @@
# =============================================================================
# BASE - System dependencies, pnpm, mongosh
# =============================================================================
FROM node:24-bookworm AS base
RUN apt-get update && apt-get install -y --no-install-recommends \
git \
curl \
jq \
gnupg \
sudo \
rsync \
&& rm -rf /var/lib/apt/lists/* \
&& echo "node ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers.d/node
RUN curl -fsSL https://www.mongodb.org/static/pgp/server-8.0.asc | gpg --dearmor -o /usr/share/keyrings/mongodb-archive-keyring.gpg \
&& echo "deb [signed-by=/usr/share/keyrings/mongodb-archive-keyring.gpg] https://repo.mongodb.org/apt/debian bookworm/mongodb-org/8.0 main" > /etc/apt/sources.list.d/mongodb-org-8.0.list \
&& apt-get update \
&& apt-get install -y --no-install-recommends mongodb-mongosh \
&& rm -rf /var/lib/apt/lists/*
# renovate: datasource=npm depName=pnpm
RUN npm install -g pnpm@10.28.1
ENV PLAYWRIGHT_BROWSERS_PATH=/home/node/.cache/ms-playwright
ENV npm_config_store_dir=/home/node/.local/share/pnpm/store
# =============================================================================
# BUILDER - Install dependencies and build to populate caches (disposable)
# =============================================================================
FROM base AS builder
USER node
WORKDIR /home/node/.cache/fcc
# Package manifests only (for pnpm install caching)
COPY --chown=node:node pnpm-lock.yaml pnpm-workspace.yaml package.json turbo.json tsconfig-base.json ./
COPY --chown=node:node api/package.json api/
COPY --chown=node:node client/package.json client/turbo.json client/
COPY --chown=node:node curriculum/package.json curriculum/turbo.json curriculum/
COPY --chown=node:node e2e/package.json e2e/
COPY --chown=node:node tools/challenge-helper-scripts/package.json tools/challenge-helper-scripts/
COPY --chown=node:node tools/challenge-parser/package.json tools/challenge-parser/
COPY --chown=node:node tools/client-plugins/browser-scripts/package.json tools/client-plugins/browser-scripts/
COPY --chown=node:node tools/client-plugins/gatsby-remark-node-identity/package.json tools/client-plugins/gatsby-remark-node-identity/
COPY --chown=node:node tools/client-plugins/gatsby-source-challenges/package.json tools/client-plugins/gatsby-source-challenges/
COPY --chown=node:node tools/daily-challenges/package.json tools/daily-challenges/
COPY --chown=node:node tools/scripts/seed/package.json tools/scripts/seed/
COPY --chown=node:node tools/scripts/seed-exams/package.json tools/scripts/seed-exams/
COPY --chown=node:node packages/challenge-builder/package.json packages/challenge-builder/
COPY --chown=node:node packages/challenge-linter/package.json packages/challenge-linter/
COPY --chown=node:node packages/eslint-config/package.json packages/eslint-config/
COPY --chown=node:node packages/shared/package.json packages/shared/
# Prisma schema needed for api postinstall script (prisma generate)
COPY --chown=node:node api/prisma/ api/prisma/
COPY --chown=node:node api/prisma.config.ts api/
# Install dependencies (populates pnpm store)
ENV PUPPETEER_SKIP_DOWNLOAD=true
RUN pnpm install --frozen-lockfile
# Source files for builds
COPY --chown=node:node packages/ packages/
COPY --chown=node:node tools/ tools/
COPY --chown=node:node curriculum/ curriculum/
# Build shared packages and curriculum (populates Turbo cache)
# Source .env so turbo hashes env vars (e.g. FCC_*) identically to runtime
COPY --chown=node:node sample.env .env
RUN set -a && . ./.env && set +a && \
pnpm turbo build --filter=@freecodecamp/shared && \
pnpm turbo build --filter=@freecodecamp/curriculum
# =============================================================================
# DEVCONTAINER - Clean image with only pre-populated caches
# Used by: GitHub Codespaces, local VS Code devcontainers
# =============================================================================
FROM base AS devcontainer
LABEL org.opencontainers.image.source=https://github.com/freeCodeCamp/freeCodeCamp \
org.opencontainers.image.description="Development container for freeCodeCamp with pre-populated caches" \
org.opencontainers.image.licenses=BSD-3-Clause
USER node
# Copy pre-populated pnpm store (for fast pnpm install --prefer-offline)
COPY --from=builder --chown=node:node /home/node/.local/share/pnpm/store /home/node/.local/share/pnpm/store
# Copy turbo cache only (preserving directory structure for rsync at runtime)
WORKDIR /home/node/.cache/fcc
RUN --mount=type=bind,from=builder,source=/home/node/.cache/fcc,target=/tmp/fcc-build \
rsync -a --include='*/' --include='.turbo/***' --exclude='*' /tmp/fcc-build/ ./
WORKDIR /workspaces/freeCodeCamp

View File

@@ -0,0 +1,50 @@
// Docker Bake configuration for freeCodeCamp devcontainer images
//
// Usage (from repo root):
// docker buildx bake -f docker/devcontainer/docker-bake.hcl # Local build (native platform)
// docker buildx bake -f docker/devcontainer/docker-bake.hcl devcontainer --push # Push multi-arch to GHCR
//
// With custom tag:
// TAG=v1.0.0 docker buildx bake -f docker/devcontainer/docker-bake.hcl devcontainer --push
variable "REGISTRY" {
default = "ghcr.io/freecodecamp"
}
variable "TAG" {
default = "latest"
}
variable "TAG_LATEST" {
default = "true"
}
group "default" {
targets = ["local-devcontainer"]
}
target "_common" {
context = "."
dockerfile = "docker/devcontainer/Dockerfile"
}
// Multi-arch image for pushing to registry (CI and local --push)
target "devcontainer" {
inherits = ["_common"]
target = "devcontainer"
platforms = ["linux/amd64", "linux/arm64"]
cache-from = ["type=gha"]
cache-to = ["type=gha,mode=max"]
tags = concat(
["${REGISTRY}/devcontainer:${TAG}"],
TAG_LATEST == "true" ? ["${REGISTRY}/devcontainer:latest"] : []
)
}
// Native platform only (fast local builds)
target "local-devcontainer" {
inherits = ["_common"]
target = "devcontainer"
output = ["type=docker"]
tags = ["ghcr.io/freecodecamp/devcontainer:latest"]
}

View File

@@ -1,13 +0,0 @@
FROM gitpod/workspace-mongodb:latest
LABEL org.opencontainers.image.source=https://github.com/freecodecamp/freecodecamp \
org.opencontainers.image.description="A Gitpod image for the main freeCodeCamp repository" \
org.opencontainers.image.licenses=BSD-3-Clause
# from https://www.gitpod.io/docs/introduction/languages/javascript#node-versions
RUN bash -c 'VERSION="24" \
&& source $HOME/.nvm/nvm.sh && nvm install $VERSION \
&& nvm use $VERSION && nvm alias default $VERSION \
&& npm i -g pnpm@10 \
&& echo "nvm use default &>/dev/null" >> ~/.bashrc.d/51-nvm-fix \
&& pnpm dlx playwright@1.47.1 install --with-deps chromium'