From 74d33a57d264c8f29a21127aa68f61b991edfb50 Mon Sep 17 00:00:00 2001 From: jo-cube <56916509+jo-cube@users.noreply.github.com> Date: Tue, 22 Sep 2026 22:55:54 +0530 Subject: [PATCH 1/6] feat(launch): align image selection and runtime commands --- compose.yaml | 2 +- justfile | 66 +++++++++++++++++++++-------------- templates/enterprise/justfile | 59 +++++++++++++++++++++---------- 3 files changed, 80 insertions(+), 47 deletions(-) diff --git a/compose.yaml b/compose.yaml index 55afd4b..f20f13a 100644 --- a/compose.yaml +++ b/compose.yaml @@ -5,7 +5,7 @@ services: workspace: - image: ${REGISTRY:-ghcr.io/jo-cube}/workspace:${FLAVOR:-code} + image: ${REGISTRY:-ghcr.io/jo-cube}/workspace:${FLAVOR:-code}-${TAG:-latest} ports: - "${WORKSPACE_BIND_ADDRESS:-127.0.0.1}:${WORKSPACE_PORT:-8080}:8080" volumes: diff --git a/justfile b/justfile index fb3f85b..e83e8fd 100644 --- a/justfile +++ b/justfile @@ -1,35 +1,39 @@ -# justfile +# Load only this project's optional configuration. +set dotenv-command := "if [ -f .env ]; then cat .env; fi" +set positional-arguments +set shell := ["sh", "-eu", "-c"] -registry := env("REGISTRY", "ghcr.io/jo-cube") -tag := env("TAG", "latest") -flavor := env("FLAVOR", "code") +export REGISTRY := if env("REGISTRY", "") == "" { "ghcr.io/jo-cube" } else { env("REGISTRY") } +export TAG := if env("TAG", "") == "" { "latest" } else { env("TAG") } +flavor := if env("FLAVOR", "") == "" { "code" } else { env("FLAVOR") } # List available commands default: @just --list # Build a specific flavor (and its dependencies) -build target=flavor: - docker buildx bake {{ target }} +build target=flavor: (_validate-flavor target) + docker buildx bake "$1" # Build all supported images build-all: docker buildx bake all -# Rebuild and start a specific flavor -up target=flavor: - just build {{ target }} - mkdir -p workspace runtime-config - chmod 0777 workspace - REGISTRY={{ registry }} FLAVOR={{ target }} docker compose up -d --no-build - -# Start a flavor, building only when the local runtime image is missing -start target=flavor: - @image="{{ registry }}/workspace:{{ target }}"; \ - docker image inspect "$image" >/dev/null 2>&1 || just build {{ target }}; \ +# Rebuild and wait for a healthy workspace +up target=flavor: (build target) + just start "$1" + +# Start a flavor, building only when the selected image is missing +start target=flavor: (_validate-flavor target) + @image="${REGISTRY}/workspace:${1}-${TAG}"; \ + if ! docker image inspect "$image" >/dev/null 2>&1; then just build "$1"; fi; \ mkdir -p workspace runtime-config; \ chmod 0777 workspace; \ - REGISTRY={{ registry }} FLAVOR={{ target }} docker compose up -d --no-build + FLAVOR="$1" docker compose up --wait --wait-timeout 120 --no-build + +# Pull a published flavor at TAG without building locally +pull target=flavor: (_validate-flavor target) + FLAVOR="$1" docker compose pull workspace # Stop the running workspace down: @@ -37,27 +41,36 @@ down: # Open a dev shell in the running workspace shell: - docker compose exec --user dev --env HOME=/home/dev --env USER=dev workspace zsh + docker compose exec --user dev --env HOME=/home/dev --env USER=dev --workdir /workspace workspace zsh -l + +# Run a command as dev; preserve arguments, stdin, and exit status +exec +command: + @docker compose exec -T --user dev --env HOME=/home/dev --env USER=dev --workdir /workspace workspace "$@" # Follow workspace logs logs: docker compose logs -f -# Show running container status +# Show container state, including Docker health status status: docker compose ps # Run health checks inside the container doctor: - docker compose exec workspace bash /scripts/doctor.sh + docker compose exec -T workspace bash /scripts/doctor.sh + +# Check the proxy and every enabled browser service +health: + @docker compose exec -T workspace /scripts/healthcheck.sh + @echo "workspace healthy" # Push a specific flavor to registry -push target=flavor: - REGISTRY={{ registry }} TAG={{ tag }} docker buildx bake {{ target }} --push +push target=flavor: (_validate-flavor target) + docker buildx bake "$1" --push # Push all images to registry push-all: - REGISTRY={{ registry }} TAG={{ tag }} docker buildx bake all --push + docker buildx bake all --push # Reset runtime data, keep build cache reset: @@ -75,6 +88,5 @@ clean-build-cache: # Remove runtime data and Docker build cache clean: reset clean-build-cache -# Quick Caddy liveness check from host -health: - @curl --noproxy '*' -sf http://localhost:${WORKSPACE_PORT:-8080}/health >/dev/null && echo "workspace healthy" +_validate-flavor target: + @case "$1" in code|platform|full) ;; *) echo "Unsupported flavor: $1 (expected code, platform, or full)" >&2; exit 1 ;; esac diff --git a/templates/enterprise/justfile b/templates/enterprise/justfile index 8fdbc82..353554f 100644 --- a/templates/enterprise/justfile +++ b/templates/enterprise/justfile @@ -1,8 +1,11 @@ -# templates/enterprise/justfile +# Load only this project's optional configuration. +set dotenv-command := "if [ -f .env ]; then cat .env; fi" +set positional-arguments +set shell := ["sh", "-eu", "-c"] -base_image := env("BASE_IMAGE", "ghcr.io/jo-cube/workspace:platform") -registry := env("REGISTRY", "registry.internal.example.com/workspace") -tag := env("TAG", "latest") +export BASE_IMAGE := if env("BASE_IMAGE", "") == "" { "ghcr.io/jo-cube/workspace:platform-latest" } else { env("BASE_IMAGE") } +export REGISTRY := if env("REGISTRY", "") == "" { "registry.internal.example.com/workspace" } else { env("REGISTRY") } +export TAG := if env("TAG", "") == "" { "latest" } else { env("TAG") } # List available commands default: @@ -11,22 +14,23 @@ default: # Build the enterprise overlay image build: set -a; [ ! -f config/proxy.env ] || . ./config/proxy.env; set +a; \ - REGISTRY={{ registry }} TAG={{ tag }} BASE_IMAGE={{ base_image }} docker buildx bake enterprise + docker buildx bake enterprise -# Rebuild and start the workspace -up: - just build - mkdir -p workspace runtime-config secrets - chmod 0777 workspace - REGISTRY={{ registry }} TAG={{ tag }} docker compose up -d --no-build +# Rebuild and wait for a healthy workspace +up: build + just start -# Start the workspace, building only when the local image is missing +# Start the workspace, building only when the selected image is missing start: - @image="{{ registry }}:{{ tag }}"; \ - docker image inspect "$image" >/dev/null 2>&1 || just build; \ + @image="${REGISTRY}:${TAG}"; \ + if ! docker image inspect "$image" >/dev/null 2>&1; then just build; fi; \ mkdir -p workspace runtime-config secrets; \ chmod 0777 workspace; \ - REGISTRY={{ registry }} TAG={{ tag }} docker compose up -d --no-build + docker compose up --wait --wait-timeout 120 --no-build + +# Pull the published enterprise image at TAG without building locally +pull: + docker compose pull workspace # Stop the workspace down: @@ -34,17 +38,34 @@ down: # Open a dev shell in the workspace shell: - docker compose exec --user dev --env HOME=/home/dev --env USER=dev workspace zsh + docker compose exec --user dev --env HOME=/home/dev --env USER=dev --workdir /workspace workspace zsh -l + +# Run a command as dev; preserve arguments, stdin, and exit status +exec +command: + @docker compose exec -T --user dev --env HOME=/home/dev --env USER=dev --workdir /workspace workspace "$@" # Follow logs logs: docker compose logs -f +# Show container state, including Docker health status +status: + docker compose ps + +# Run health checks inside the container +doctor: + docker compose exec -T workspace bash /scripts/doctor.sh + +# Check the proxy and every enabled browser service +health: + @docker compose exec -T workspace /scripts/healthcheck.sh + @echo "workspace healthy" + # Push to enterprise registry push: set -a; [ ! -f config/proxy.env ] || . ./config/proxy.env; set +a; \ - REGISTRY={{ registry }} TAG={{ tag }} BASE_IMAGE={{ base_image }} docker buildx bake enterprise --push + docker buildx bake enterprise --push -# Remove volumes -clean: +# Reset runtime data, keep workspace files and build cache +reset: docker compose down -v From 5ad362203b81ee594d30182bcf9235345241ae17 Mon Sep 17 00:00:00 2001 From: jo-cube <56916509+jo-cube@users.noreply.github.com> Date: Tue, 22 Sep 2026 22:55:57 +0530 Subject: [PATCH 2/6] fix(build): align tagged images and reduce build overhead --- .dockerignore | 1 + .github/workflows/release-images.yml | 5 +++-- docker-bake.hcl | 6 +++--- docker/full.Dockerfile | 3 ++- docker/runtime.Dockerfile | 2 +- templates/enterprise/Dockerfile | 2 +- templates/enterprise/docker-bake.hcl | 2 +- 7 files changed, 12 insertions(+), 9 deletions(-) diff --git a/.dockerignore b/.dockerignore index a12d528..a092cd5 100644 --- a/.dockerignore +++ b/.dockerignore @@ -2,6 +2,7 @@ .github *.md docs/ +tests/ templates/ workspace/ runtime-config/ diff --git a/.github/workflows/release-images.yml b/.github/workflows/release-images.yml index 25bd373..1a5ef38 100644 --- a/.github/workflows/release-images.yml +++ b/.github/workflows/release-images.yml @@ -120,6 +120,7 @@ jobs: - name: Build release image for scanning env: REGISTRY: workspace-test + TAG: latest CACHE_FROM: "true" CACHE_REGISTRY: ghcr.io/${{ github.repository_owner }} CACHE_IMAGE: workspace-cache @@ -131,7 +132,7 @@ jobs: docker run --rm \ --entrypoint /bin/sh \ --volume /var/run/docker.sock:/var/run/docker.sock \ - workspace-test/workspace:full -c ' + workspace-test/workspace:full-latest -c ' set -eu trivy image --download-db-only --no-progress trivy image --download-java-db-only --no-progress @@ -143,7 +144,7 @@ jobs: --ignore-unfixed \ --scanners vuln \ --severity CRITICAL \ - workspace-test/workspace:full + workspace-test/workspace:full-latest ' - name: Publish code, platform, and full diff --git a/docker-bake.hcl b/docker-bake.hcl index 67ebc5c..d825be6 100644 --- a/docker-bake.hcl +++ b/docker-bake.hcl @@ -51,7 +51,7 @@ target "code-core" { target "code" { dockerfile = "docker/runtime.Dockerfile" context = "." - tags = ["${REGISTRY}/workspace:code-${TAG}", "${REGISTRY}/workspace:code"] + tags = ["${REGISTRY}/workspace:code-${TAG}"] args = { BASE_IMAGE = "${REGISTRY}/workspace:code-core" } contexts = { "${REGISTRY}/workspace:code-core" = "target:code-core" } output = PUBLISH == "true" ? [] : ["type=docker"] @@ -78,7 +78,7 @@ target "platform-core" { target "platform" { dockerfile = "docker/runtime.Dockerfile" context = "." - tags = ["${REGISTRY}/workspace:platform-${TAG}", "${REGISTRY}/workspace:platform"] + tags = ["${REGISTRY}/workspace:platform-${TAG}"] args = { BASE_IMAGE = "${REGISTRY}/workspace:platform-core" } contexts = { "${REGISTRY}/workspace:platform-core" = "target:platform-core" } output = PUBLISH == "true" ? [] : ["type=docker"] @@ -96,7 +96,7 @@ target "full-core" { target "full" { dockerfile = "docker/runtime.Dockerfile" context = "." - tags = ["${REGISTRY}/workspace:full-${TAG}", "${REGISTRY}/workspace:full", "${REGISTRY}/workspace:latest"] + tags = ["${REGISTRY}/workspace:full-${TAG}"] args = { BASE_IMAGE = "${REGISTRY}/workspace:full-core" } contexts = { "${REGISTRY}/workspace:full-core" = "target:full-core" } output = PUBLISH == "true" ? [] : ["type=docker"] diff --git a/docker/full.Dockerfile b/docker/full.Dockerfile index dcfe290..5b29121 100644 --- a/docker/full.Dockerfile +++ b/docker/full.Dockerfile @@ -27,6 +27,7 @@ esac >> /etc/arch-env EOF # JupyterLab +# Build the Rust kernel serially to keep peak compiler memory manageable. USER dev RUN --mount=type=cache,target=/cache/uv,sharing=locked,uid=1000,gid=1000 \ --mount=type=cache,target=/opt/rust/cargo/registry,sharing=locked,uid=1000,gid=1000 \ @@ -36,7 +37,7 @@ RUN --mount=type=cache,target=/cache/uv,sharing=locked,uid=1000,gid=1000 \ && /opt/uv-tools/jupyterlab/bin/python -m bash_kernel.install --sys-prefix \ && /opt/uv-tools/jupyterlab/bin/python -c 'import json,pathlib,sys; p=pathlib.Path(sys.prefix)/"share/jupyter/kernels/kotlin/kernel.json"; data=json.loads(p.read_text()); data["argv"][0]=sys.executable; data["metadata"]["jar_path_detect_command"][0]=sys.executable; p.write_text(json.dumps(data, indent=2)+"\n")' \ && rustup component add rust-src \ - && cargo install --locked evcxr_jupyter \ + && cargo install --locked --jobs 1 evcxr_jupyter \ && JUPYTER_PATH=/opt/uv-tools/jupyterlab/share/jupyter evcxr_jupyter --install USER root diff --git a/docker/runtime.Dockerfile b/docker/runtime.Dockerfile index 12dbdb4..50f046c 100644 --- a/docker/runtime.Dockerfile +++ b/docker/runtime.Dockerfile @@ -18,7 +18,7 @@ RUN chmod +x /etc/s6-overlay/s6-rc.d/caddy/run \ COPY config/cont-init.d/ /etc/cont-init.d/ RUN chmod +x /etc/cont-init.d/* -COPY scripts/ /scripts/ +COPY scripts/doctor.sh scripts/healthcheck.sh /scripts/ RUN chmod +x /scripts/*.sh COPY config/Caddyfile /etc/caddy/Caddyfile diff --git a/templates/enterprise/Dockerfile b/templates/enterprise/Dockerfile index 5aa514e..1ab5a7f 100644 --- a/templates/enterprise/Dockerfile +++ b/templates/enterprise/Dockerfile @@ -2,7 +2,7 @@ # Enterprise workspace overlay # Builds FROM the generic workspace image and adds enterprise-specific config. -ARG BASE_IMAGE=ghcr.io/jo-cube/workspace:platform +ARG BASE_IMAGE=ghcr.io/jo-cube/workspace:platform-latest FROM ${BASE_IMAGE} # Enterprise CA certificates diff --git a/templates/enterprise/docker-bake.hcl b/templates/enterprise/docker-bake.hcl index 4ff1ae0..395ad6a 100644 --- a/templates/enterprise/docker-bake.hcl +++ b/templates/enterprise/docker-bake.hcl @@ -7,7 +7,7 @@ variable "TAG" { } variable "BASE_IMAGE" { - default = "ghcr.io/jo-cube/workspace:platform" + default = "ghcr.io/jo-cube/workspace:platform-latest" } variable "HTTP_PROXY" { From 7186fc6f8109767deda4bc32503e682ec4d87190 Mon Sep 17 00:00:00 2001 From: jo-cube <56916509+jo-cube@users.noreply.github.com> Date: Tue, 22 Sep 2026 22:55:59 +0530 Subject: [PATCH 3/6] fix(health): simplify readiness and health checks --- config/Caddyfile | 5 ----- config/index.html | 1 - scripts/doctor.sh | 1 - scripts/smoke-test.sh | 11 +++-------- 4 files changed, 3 insertions(+), 15 deletions(-) diff --git a/config/Caddyfile b/config/Caddyfile index c54b9f7..0ea9f6d 100644 --- a/config/Caddyfile +++ b/config/Caddyfile @@ -8,11 +8,6 @@ respond "OK" } - handle /status { - header Content-Type application/json - respond `{"status":"running"}` - } - @lab path /lab /lab/* handle @lab { reverse_proxy 127.0.0.1:8888 diff --git a/config/index.html b/config/index.html index 8570542..274c231 100644 --- a/config/index.html +++ b/config/index.html @@ -100,7 +100,6 @@