From 0e287b3b1a5b80afd5bc9f87c4b18915612ee7ea Mon Sep 17 00:00:00 2001 From: Hanwen Zh <70364944+homework36@users.noreply.github.com> Date: Thu, 16 Jul 2026 12:28:33 -0400 Subject: [PATCH 1/9] replace docker hub image path with ghcr --- build.yml | 14 +++++++------- docker-compose.yml | 16 ++++++++-------- rodan-client/local-dev/arm-compose.yml | 12 ++++++------ 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/build.yml b/build.yml index 8e8cdb6c0..9a71a38fb 100644 --- a/build.yml +++ b/build.yml @@ -7,13 +7,13 @@ services: dockerfile: Dockerfile args: VERSION: nightly - image: "ddmal/nginx:nightly" + image: "ghcr.io/ddmal/nginx:nightly" iipsrv: build: context: ./iipsrv dockerfile: Dockerfile - image: "ddmal/iipsrv:nightly" + image: "ghcr.io/ddmal/iipsrv:nightly" rodan: build: @@ -22,7 +22,7 @@ services: args: BRANCHES: develop VERSION: nightly - image: "ddmal/rodan-main:nightly" + image: "ghcr.io/ddmal/rodan-main:nightly" py3-celery: build: @@ -30,7 +30,7 @@ services: dockerfile: ./python3-celery/Dockerfile args: BRANCHES: develop - image: "ddmal/rodan-python3-celery:nightly" + image: "ghcr.io/ddmal/rodan-python3-celery:nightly" gpu-celery: build: @@ -38,16 +38,16 @@ services: dockerfile: ./gpu-celery/Dockerfile args: BRANCHES: develop - image: "ddmal/rodan-gpu-celery:nightly" + image: "ghcr.io/ddmal/rodan-gpu-celery:nightly" postgres: build: context: . dockerfile: ./postgres/Dockerfile - image: "ddmal/postgres-plpython:nightly" + image: "ghcr.io/ddmal/postgres-plpython:nightly" rodan-client: build: context: ./rodan-client dockerfile: Dockerfile - image: "ddmal/rodan-client:nightly" + image: "ghcr.io/ddmal/rodan-client:nightly" diff --git a/docker-compose.yml b/docker-compose.yml index 343879a76..76c4fa2ee 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,7 @@ version: "3.4" services: nginx: - image: "ddmal/nginx:${DOCKER_TAG}" + image: "ghcr.io/ddmal/nginx:${DOCKER_TAG}" command: /run/start depends_on: - celery @@ -20,7 +20,7 @@ services: - "resources:/rodan/data" rodan-main: - image: "ddmal/rodan-main:${DOCKER_TAG}" + image: "ghcr.io/ddmal/rodan-main:${DOCKER_TAG}" healthcheck: test: [ @@ -48,18 +48,18 @@ services: - "./rodan-main/code:/code/Rodan" rodan-client: - image: "ddmal/rodan-client:${DOCKER_TAG}" + image: "ghcr.io/ddmal/rodan-client:${DOCKER_TAG}" volumes: - "./rodan-client/code:/code" - "./rodan-client/config/configuration.json:/client/configuration.json" iipsrv: - image: "ddmal/iipsrv:${DOCKER_TAG}" + image: "ghcr.io/ddmal/iipsrv:${DOCKER_TAG}" volumes: - "resources:/rodan/data" celery: - image: "ddmal/rodan-main:${DOCKER_TAG}" + image: "ghcr.io/ddmal/rodan-main:${DOCKER_TAG}" command: bash -c "tail -f /dev/null" environment: TZ: America/Toronto @@ -95,7 +95,7 @@ services: - "./rodan-main/code:/code/Rodan" py3-celery: - image: "ddmal/rodan-python3-celery:${DOCKER_TAG}" + image: "ghcr.io/ddmal/rodan-python3-celery:${DOCKER_TAG}" command: bash -c "tail -f /dev/null" environment: TZ: America/Toronto @@ -114,7 +114,7 @@ services: - "./rodan-main/code:/code/Rodan" gpu-celery: - image: "ddmal/rodan-gpu-celery:${DOCKER_TAG}" + image: "ghcr.io/ddmal/rodan-gpu-celery:${DOCKER_TAG}" command: bash -c "tail -f /dev/null" environment: TZ: America/Toronto @@ -143,7 +143,7 @@ services: - postgres postgres: - image: "ddmal/postgres-plpython:${DOCKER_TAG}" + image: "ghcr.io/ddmal/postgres-plpython:${DOCKER_TAG}" healthcheck: test: [ "CMD-SHELL", "pg_isready", "-U", "postgres" ] interval: 10s diff --git a/rodan-client/local-dev/arm-compose.yml b/rodan-client/local-dev/arm-compose.yml index e54f72f7f..c4f5e7491 100644 --- a/rodan-client/local-dev/arm-compose.yml +++ b/rodan-client/local-dev/arm-compose.yml @@ -13,7 +13,7 @@ services: - "resources:/rodan/data" rodan-main: - image: "ddmal/rodan-main:nightly" + image: "ghcr.io/ddmal/rodan-main:nightly" healthcheck: test: ["CMD-SHELL", "/usr/bin/curl -H 'User-Agent: docker-healthcheck' http://localhost:8000/api/?format=json || exit 1"] interval: "10s" @@ -37,13 +37,13 @@ services: iipsrv: - image: "ddmal/iipsrv:nightly" + image: "ghcr.io/ddmal/iipsrv:nightly" volumes: - "resources:/rodan/data" celery: - image: "ddmal/rodan-main:nightly" + image: "ghcr.io/ddmal/rodan-main:nightly" command: bash -c "tail -f /dev/null" environment: CELERY_JOB_QUEUE: celery @@ -65,7 +65,7 @@ services: - "./rodan-main/code:/code/Rodan" py3-celery: - image: "ddmal/rodan-python3-celery:nightly" + image: "ghcr.io/ddmal/rodan-python3-celery:nightly" command: bash -c "tail -f /dev/null" environment: CELERY_JOB_QUEUE: Python3 @@ -93,7 +93,7 @@ services: - postgres postgres: - image: "ddmal/postgres-plpython:nightly" + image: "ghcr.io/ddmal/postgres-plpython:nightly" healthcheck: test: ["CMD-SHELL", "pg_isready", "-U", "postgres"] interval: 10s @@ -115,7 +115,7 @@ services: - ./scripts/local.env rodan-client: - image: "ddmal/rodan-client:nightly" + image: "ghcr.io/ddmal/rodan-client:nightly" command: bash -c "tail -f /dev/null" ports: - "8080:9002" From 30553ccf79a3db0d6610f58652ac57672c96f850 Mon Sep 17 00:00:00 2001 From: amirsadraabdollahi Date: Sat, 18 Jul 2026 18:17:09 -0400 Subject: [PATCH 2/9] Fix: resolve the gpu-celery pod to use all MIG --- k8s/33-gpu-celery.yaml | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/k8s/33-gpu-celery.yaml b/k8s/33-gpu-celery.yaml index 1d9131280..16fc4cee4 100644 --- a/k8s/33-gpu-celery.yaml +++ b/k8s/33-gpu-celery.yaml @@ -25,8 +25,12 @@ spec: # inject, so cuInit -> CUDA_ERROR_NO_DEVICE. Instead we pin to a specific node and let # the NVIDIA runtime inject the MIG instance directly via NVIDIA_VISIBLE_DEVICES (which # DOES inject cap4). This means NOT requesting nvidia.com/gpu (that would make the plugin - # overwrite NVIDIA_VISIBLE_DEVICES with the parent). The MIG UUID is per-node, hence the - # hostname pin. Long-term fix: a non-MIG (time-sliced) vGPU profile removes all of this. + # overwrite NVIDIA_VISIBLE_DEVICES with the parent). We select the MIG via "all" rather + # than a specific MIG UUID: the node has exactly one 1g.24gb MIG instance and this pod is + # the only GPU workload pinned here, so "all" resolves to that MIG — and, unlike a + # hardcoded UUID, survives node reboots (which regenerate MIG UUIDs and otherwise CrashLoop + # the pod with "failed to get device handle from UUID: Not Found"). + # Long-term fix: a non-MIG (time-sliced) vGPU profile removes all of this. nodeSelector: gpu: "true" kubernetes.io/hostname: k3s-gpu-node-2 @@ -46,11 +50,12 @@ spec: env: - name: CELERY_JOB_QUEUE value: "GPU" - # MIG compute-instance UUID on k3s-gpu-node-2 (nvidia-smi -L). The runtime - # injects this exact MIG device (incl. /dev/nvidia-caps/nvidia-cap4) so CUDA can - # open it. This is node-specific — it must match the node pinned above. + # "all" -> the single 1g.24gb MIG instance on the pinned node (the runtime injects + # it incl. /dev/nvidia-caps/nvidia-cap4 so CUDA can open it). Reboot-proof: avoids a + # node-specific MIG UUID that changes on every reboot/MIG-reconfig. See the workaround + # note above for why we don't request nvidia.com/gpu. - name: NVIDIA_VISIBLE_DEVICES - value: "MIG-a724b14d-5057-529c-b9ef-87ebd0d2d92d" + value: "all" - name: NVIDIA_DRIVER_CAPABILITIES value: "compute,utility" # TF 2.15.1 has no sm_90 (Hopper) cubins, so kernels JIT-compile from PTX on first use From 4b1e1dabc15350950048ad48e4d7307f459b1b13 Mon Sep 17 00:00:00 2001 From: amirsadraabdollahi Date: Mon, 20 Jul 2026 09:31:14 -0400 Subject: [PATCH 3/9] Fix: load staticfiles changed to load static --- rodan-main/code/rodan/templates/diva.html | 4 ++-- rodan-main/code/rodan/templates/neon_square_viewer.html | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/rodan-main/code/rodan/templates/diva.html b/rodan-main/code/rodan/templates/diva.html index d1dedaada..3c71c7baa 100644 --- a/rodan-main/code/rodan/templates/diva.html +++ b/rodan-main/code/rodan/templates/diva.html @@ -1,4 +1,4 @@ -{% load staticfiles %} +{% load static %} @@ -49,7 +49,7 @@ diff --git a/rodan-main/code/rodan/templates/neon_square_viewer.html b/rodan-main/code/rodan/templates/neon_square_viewer.html index e0be1a73f..b07589cb6 100644 --- a/rodan-main/code/rodan/templates/neon_square_viewer.html +++ b/rodan-main/code/rodan/templates/neon_square_viewer.html @@ -1,4 +1,4 @@ -{% load staticfiles %} +{% load static %} View Resource: {{mei_name}} From 7d0ad2f5450808f64c77c14f344197fd95e67187 Mon Sep 17 00:00:00 2001 From: amirsadraabdollahi Date: Mon, 20 Jul 2026 13:14:49 -0400 Subject: [PATCH 4/9] Fix: local docker compose --- Makefile | 25 ++++++++++++++++--------- build.yml | 10 ++++++++++ docker-compose.yml | 6 +++++- 3 files changed, 31 insertions(+), 10 deletions(-) diff --git a/Makefile b/Makefile index 125c1ac00..68ead3418 100644 --- a/Makefile +++ b/Makefile @@ -19,7 +19,7 @@ build: @docker compose -f build.yml build --no-cache py3-celery # rodan(-main) and rodan-client next — nginx is FROM rodan-main. @docker compose -f build.yml build --no-cache --parallel rodan rodan-client - @docker compose -f build.yml build --no-cache --parallel nginx gpu-celery postgres + @docker compose -f build.yml build --no-cache --parallel nginx gpu-celery postgres iipsrv @echo "[+] Done." run: remote_jobs @@ -50,14 +50,21 @@ clean_git: health: @docker inspect --format "{{json .State.Health }}" $(log) | jq -$(JOBS_PATH)/neon_wrapper/Neon/package.json: - @cd $(JOBS_PATH); \ - git clone --recurse-submodules -b develop https://github.com/DDMAL/neon_wrapper.git - -$(JOBS_PATH)/neon_wrapper/static/editor.html: $(JOBS_PATH)/neon_wrapper/Neon/package.json - @cd $(JOBS_PATH)/neon_wrapper; \ - yarn install && \ - yarn build +# Neon's webpack build needs the exact toolchain baked into the rodan-python3-celery image +# (Alpine node 16 / yarn) and fails on newer host Node versions. So instead of building on the +# host, extract the already-built neon_wrapper (source + static/editor.html + Python package) +# out of that image — built by `make build`, or pulled from GHCR if absent. +$(JOBS_PATH)/neon_wrapper/static/editor.html: + @echo "[-] Extracting pre-built neon_wrapper from rodan-python3-celery:$(DOCKER_TAG)..." + @docker image inspect ghcr.io/ddmal/rodan-python3-celery:$(DOCKER_TAG) >/dev/null 2>&1 || \ + docker pull ghcr.io/ddmal/rodan-python3-celery:$(DOCKER_TAG) + @# Remove any existing copy as root — the containers run as root and may own __pycache__ + @# files under the bind-mounted jobs dir that the host user cannot delete. + @docker run --rm --entrypoint sh -v $(abspath $(JOBS_PATH)):/jobs ghcr.io/ddmal/rodan-python3-celery:$(DOCKER_TAG) -c "rm -rf /jobs/neon_wrapper" + @cid=$$(docker create ghcr.io/ddmal/rodan-python3-celery:$(DOCKER_TAG)); \ + docker cp $$cid:/code/Rodan/rodan/jobs/neon_wrapper $(JOBS_PATH)/neon_wrapper; \ + docker rm $$cid >/dev/null + @echo "[+] neon_wrapper ready." $(JOBS_PATH)/pixel_wrapper/package.json: @cd $(JOBS_PATH); git clone --recurse-submodules -b develop https://github.com/DDMAL/pixel_wrapper.git diff --git a/build.yml b/build.yml index 9a71a38fb..9612abb48 100644 --- a/build.yml +++ b/build.yml @@ -22,6 +22,11 @@ services: args: BRANCHES: develop VERSION: nightly + # nginx/Dockerfile does `FROM ddmal/rodan-main:${VERSION}` (Docker Hub namespace). + # Emit that tag alongside the ghcr one so a local `make build` resolves the base + # without a manual retag (CI does the same via an extra `-t ddmal/rodan-main:$TAG`). + tags: + - "ddmal/rodan-main:nightly" image: "ghcr.io/ddmal/rodan-main:nightly" py3-celery: @@ -30,6 +35,11 @@ services: dockerfile: ./python3-celery/Dockerfile args: BRANCHES: develop + # rodan-main/Dockerfile does `FROM ddmal/rodan-python3-celery:${VERSION}` (Docker Hub + # namespace). Emit that tag alongside the ghcr one so a local `make build` resolves the + # base without a manual retag (CI does the same via an extra `-t ddmal/...:$TAG`). + tags: + - "ddmal/rodan-python3-celery:nightly" image: "ghcr.io/ddmal/rodan-python3-celery:nightly" gpu-celery: diff --git a/docker-compose.yml b/docker-compose.yml index 76c4fa2ee..f2530dfb4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -153,7 +153,11 @@ services: - ./scripts/local.env rabbitmq: - image: "rabbitmq:alpine" + # Pinned to the 3.13 line to match production (3.13.7). RabbitMQ 4.x disables the + # `transient_nonexcl_queues` feature by default, which Celery's pidbox/reply/event + # queues (and `celery inspect`) declare — an unpinned `rabbitmq:alpine` now resolves + # to 4.x and breaks the workers with "Feature transient_nonexcl_queues is deprecated". + image: "rabbitmq:3.13-alpine" healthcheck: test: [ "CMD", "rabbitmq-diagnostics", "-q", "ping" ] interval: "30s" From 889c1805c6264a712bae4357f7f543a8a5608042 Mon Sep 17 00:00:00 2001 From: amirsadraabdollahi Date: Thu, 23 Jul 2026 11:08:34 -0400 Subject: [PATCH 5/9] Fix: local deployment --- docker-compose.yml | 14 ++++--- nginx/config/rodan.local.conf | 75 +++++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 5 deletions(-) create mode 100644 nginx/config/rodan.local.conf diff --git a/docker-compose.yml b/docker-compose.yml index f2530dfb4..ff08a71ee 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -18,6 +18,10 @@ services: - "9002:9002" volumes: - "resources:/rodan/data" + # Local-only override: pass the real request scheme instead of the hardcoded + # `X-Scheme https` baked into the prod image, so DRF emits http:// URLs over + # plain-HTTP local dev (Django's SECURE_PROXY_SSL_HEADER trusts X-Scheme). + - "./nginx/config/rodan.local.conf:/etc/nginx/sites-available/rodan.conf" rodan-main: image: "ghcr.io/ddmal/rodan-main:${DOCKER_TAG}" @@ -31,7 +35,7 @@ services: timeout: "5s" retries: 2 start_period: "2m" - command: bash -c "tail -f /dev/null" + command: /run/start environment: TZ: America/Toronto SERVER_HOST: localhost @@ -60,7 +64,7 @@ services: celery: image: "ghcr.io/ddmal/rodan-main:${DOCKER_TAG}" - command: bash -c "tail -f /dev/null" + command: /run/start-celery environment: TZ: America/Toronto SERVER_HOST: localhost @@ -70,12 +74,12 @@ services: [ "CMD", "celery", - "inspect", - "ping", "-A", "rodan", "--workdir", "/code/Rodan", + "inspect", + "ping", "-d", "celery@celery" ] @@ -115,7 +119,7 @@ services: gpu-celery: image: "ghcr.io/ddmal/rodan-gpu-celery:${DOCKER_TAG}" - command: bash -c "tail -f /dev/null" + command: /run/start-celery environment: TZ: America/Toronto SERVER_HOST: localhost diff --git a/nginx/config/rodan.local.conf b/nginx/config/rodan.local.conf new file mode 100644 index 000000000..9ab2914ae --- /dev/null +++ b/nginx/config/rodan.local.conf @@ -0,0 +1,75 @@ +server { # Rodan API + listen 80; + listen [::]:80; # IPv6 + access_log /var/log/nginx/rodan.log main; + include /etc/nginx/conf.d/error.conf; + + location /static/ { # Static files for API + limit_except GET { + deny all; + } + root /rodan; + } + + location /fcgi-bin/iipsrv.fcgi { # iipsrv + fastcgi_pass iipsrv:9003; + limit_except GET { + deny all; + } + proxy_set_header Host $http_host; + fastcgi_param QUERY_STRING $query_string; + fastcgi_param REQUEST_METHOD $request_method; + fastcgi_param CONTENT_TYPE $content_type; + fastcgi_param CONTENT_LENGTH $content_length; + fastcgi_param PATH_INFO $fastcgi_script_name; + } + + location /uploads { # NFS storage + limit_except GET { + deny all; + } + add_header Host $http_host; + add_header 'Access-Control-Allow-Origin' '*'; + proxy_pass_request_headers on; + alias /rodan/data; + } + + location /ws/ { # WebSocket broadcast (Django Channels ASGI on rodan-main; replaces ws4redis) + proxy_pass http://rodan-main:8000; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto https; + proxy_connect_timeout 10; + proxy_read_timeout 3600; # long-lived broadcast socket + proxy_send_timeout 3600; + } + + location /ht { # Django Health Check + limit_except GET { + deny all; + } + proxy_pass http://rodan-main:8000/ht; + } + + location /api { # Gunicorn Portal + limit_except GET POST HEAD OPTIONS PATCH PUT DELETE { + deny all; + } + proxy_set_header Host $http_host; + proxy_set_header X-Scheme $scheme; + proxy_set_header 'Access-Control-Allow-Origin' '*'; + proxy_set_header 'Access-Control-Allow-Headers' 'Content-Type'; + proxy_pass_request_headers on; + proxy_pass http://rodan-main:8000; + } + + location / { + proxy_pass_request_headers on; + include /etc/nginx/conf.d/error.conf; + proxy_pass http://rodan-client; + } +} From 76f254c80ca6716674d3119bf47aab2611af5afc Mon Sep 17 00:00:00 2001 From: amirsadraabdollahi Date: Thu, 23 Jul 2026 14:03:53 -0400 Subject: [PATCH 6/9] Fix: pixel js interactive editor black screen --- nginx/config/rodan.conf | 1 + nginx/config/rodan.local.conf | 1 + 2 files changed, 2 insertions(+) diff --git a/nginx/config/rodan.conf b/nginx/config/rodan.conf index 4b8096d15..75b602e66 100644 --- a/nginx/config/rodan.conf +++ b/nginx/config/rodan.conf @@ -18,6 +18,7 @@ server { # Rodan API } proxy_set_header Host $http_host; fastcgi_param QUERY_STRING $query_string; + fastcgi_param REQUEST_URI $request_uri; fastcgi_param REQUEST_METHOD $request_method; fastcgi_param CONTENT_TYPE $content_type; fastcgi_param CONTENT_LENGTH $content_length; diff --git a/nginx/config/rodan.local.conf b/nginx/config/rodan.local.conf index 9ab2914ae..54ad48827 100644 --- a/nginx/config/rodan.local.conf +++ b/nginx/config/rodan.local.conf @@ -18,6 +18,7 @@ server { # Rodan API } proxy_set_header Host $http_host; fastcgi_param QUERY_STRING $query_string; + fastcgi_param REQUEST_URI $request_uri; fastcgi_param REQUEST_METHOD $request_method; fastcgi_param CONTENT_TYPE $content_type; fastcgi_param CONTENT_LENGTH $content_length; From e57cf7b7df99dc2d5a9420577d97eca20be340e0 Mon Sep 17 00:00:00 2001 From: amirsadraabdollahi Date: Thu, 23 Jul 2026 15:37:55 -0400 Subject: [PATCH 7/9] Fix: iipsrv black image --- iipsrv/Dockerfile | 5 +++++ rodan-main/code/rodan/jobs/core.py | 19 ++++++++++++++++++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/iipsrv/Dockerfile b/iipsrv/Dockerfile index a13bdbf7d..5011e3498 100644 --- a/iipsrv/Dockerfile +++ b/iipsrv/Dockerfile @@ -11,6 +11,11 @@ RUN apk add --no-cache \ RUN wget https://github.com/ruven/iipsrv/archive/refs/heads/master.zip && unzip master.zip WORKDIR /iipsrv-master +# Patch upstream OpenJPEG 8-bit decode bug: the mask for 8-bit output data is +# 0x000000f (4-bit / low nibble only), which renders every 8-bit JP2 ~32x too +# dark. It must be 0x000000ff (full 8-bit byte). See src/OpenJPEGImage.cc. +RUN sed -i 's/& 0x000000f)/\& 0x000000ff)/' src/OpenJPEGImage.cc \ + && grep -q '& 0x000000ff)' src/OpenJPEGImage.cc RUN ./autogen.sh RUN ./configure --enable-openjpeg RUN make diff --git a/rodan-main/code/rodan/jobs/core.py b/rodan-main/code/rodan/jobs/core.py index ec8446188..3e75156e4 100644 --- a/rodan-main/code/rodan/jobs/core.py +++ b/rodan-main/code/rodan/jobs/core.py @@ -1,6 +1,7 @@ from __future__ import absolute_import from collections import OrderedDict +import math import os import shutil import subprocess @@ -220,13 +221,29 @@ def create_diva(resource_id): # Tiff is one of those formats. # This means we can convert to JPEG2000 for IIPSRV after converting the # original to tiff. + # + # IIPImage/Diva serve the JP2 as a multi-resolution pyramid. The JP2 must + # therefore be TILED (-t) and carry enough resolution levels (-n) that the + # full image reduces down into a single tile. Otherwise IIP has to decode + # huge single-tile regions and fabricate the missing "virtual" resolution + # levels itself, which corrupts tiles (rainbow noise / 404s) when zooming. + # Match the tile size to Diva's 256px request tiles, and derive the number + # of resolution levels from the image size so the smallest level fits in + # one tile: n = ceil(log2(max_dim / tile)) + 1. + tile_size = 256 + max_dim = max(rgb_im.size) + # +1 because n counts levels (decompositions + 1); cap at 9 since a + # 256px tile cannot hold more than 8 wavelet decompositions. + num_resolutions = max(1, math.ceil(math.log2(max_dim / tile_size))) + 1 + num_resolutions = min(num_resolutions, 9) subprocess.check_call( args=[ # With Grok + OpenJPEG # noqa "/vendor/grok/build/bin/grk_compress", "-i", tiff_file, "-o", jp2_file, - "-n", "5", + "-t", "{0},{0}".format(tile_size), + "-n", str(num_resolutions), "-c", "[256,256],[256,256],[128,128]", "-SOP", "-p", "LRCP", From 7cc2414799b921ad5b57a39e01bd3d245e2ba169 Mon Sep 17 00:00:00 2001 From: amirsadraabdollahi Date: Thu, 23 Jul 2026 20:00:44 -0400 Subject: [PATCH 8/9] Fix: Pixel.js/Diva zoom garbling (IIP region-decode bug) --- iipsrv/Dockerfile | 13 +++++++++++++ rodan-main/code/rodan/jobs/core.py | 19 +------------------ 2 files changed, 14 insertions(+), 18 deletions(-) diff --git a/iipsrv/Dockerfile b/iipsrv/Dockerfile index 5011e3498..6042dfee0 100644 --- a/iipsrv/Dockerfile +++ b/iipsrv/Dockerfile @@ -16,6 +16,19 @@ WORKDIR /iipsrv-master # dark. It must be 0x000000ff (full 8-bit byte). See src/OpenJPEGImage.cc. RUN sed -i 's/& 0x000000f)/\& 0x000000ff)/' src/OpenJPEGImage.cc \ && grep -q '& 0x000000ff)' src/OpenJPEGImage.cc +# Patch upstream OpenJPEG region-clamp bug in OpenJPEGImage::process(). +# w0/h0 are END coordinates passed to +# opj_set_decode_area(codec, img, start_x, start_y, end_x, end_y), +# but the overrun clamp treats them as widths (x0 + w0) and reassigns +# w0 = image_widths[0] - x0. For any tile whose x0 > width/2 this makes +# end_x < start_x, so OpenJPEG gets a negative decode width and errors out +# -> the right/bottom half of every image 404s (and the boundary band +# garbles) when zooming. w0/h0 must be clamped to the image dimensions +# directly, not offset by x0/y0. +RUN sed -i 's/if( x0 + w0 > image_widths\[0\][[:space:]]*) w0 = image_widths\[0\][[:space:]]*- x0;/if( w0 > image_widths[0] ) w0 = image_widths[0];/' src/OpenJPEGImage.cc \ + && sed -i 's/if( y0 + h0 > image_heights\[0\][[:space:]]*) h0 = image_heights\[0\][[:space:]]*- y0;/if( h0 > image_heights[0] ) h0 = image_heights[0];/' src/OpenJPEGImage.cc \ + && grep -q 'if( w0 > image_widths\[0\] ) w0 = image_widths\[0\];' src/OpenJPEGImage.cc \ + && grep -q 'if( h0 > image_heights\[0\] ) h0 = image_heights\[0\];' src/OpenJPEGImage.cc RUN ./autogen.sh RUN ./configure --enable-openjpeg RUN make diff --git a/rodan-main/code/rodan/jobs/core.py b/rodan-main/code/rodan/jobs/core.py index 3e75156e4..ec8446188 100644 --- a/rodan-main/code/rodan/jobs/core.py +++ b/rodan-main/code/rodan/jobs/core.py @@ -1,7 +1,6 @@ from __future__ import absolute_import from collections import OrderedDict -import math import os import shutil import subprocess @@ -221,29 +220,13 @@ def create_diva(resource_id): # Tiff is one of those formats. # This means we can convert to JPEG2000 for IIPSRV after converting the # original to tiff. - # - # IIPImage/Diva serve the JP2 as a multi-resolution pyramid. The JP2 must - # therefore be TILED (-t) and carry enough resolution levels (-n) that the - # full image reduces down into a single tile. Otherwise IIP has to decode - # huge single-tile regions and fabricate the missing "virtual" resolution - # levels itself, which corrupts tiles (rainbow noise / 404s) when zooming. - # Match the tile size to Diva's 256px request tiles, and derive the number - # of resolution levels from the image size so the smallest level fits in - # one tile: n = ceil(log2(max_dim / tile)) + 1. - tile_size = 256 - max_dim = max(rgb_im.size) - # +1 because n counts levels (decompositions + 1); cap at 9 since a - # 256px tile cannot hold more than 8 wavelet decompositions. - num_resolutions = max(1, math.ceil(math.log2(max_dim / tile_size))) + 1 - num_resolutions = min(num_resolutions, 9) subprocess.check_call( args=[ # With Grok + OpenJPEG # noqa "/vendor/grok/build/bin/grk_compress", "-i", tiff_file, "-o", jp2_file, - "-t", "{0},{0}".format(tile_size), - "-n", str(num_resolutions), + "-n", "5", "-c", "[256,256],[256,256],[128,128]", "-SOP", "-p", "LRCP", From 0520e8a6e311edd2394fece84ed170ea9aac2373 Mon Sep 17 00:00:00 2001 From: amirsadraabdollahi Date: Fri, 24 Jul 2026 18:34:04 -0400 Subject: [PATCH 9/9] Fix: readme --- readme.md | 39 +++++++++++++++++++++++++++++++++++++-- rodan-client/readme.md | 5 ++--- 2 files changed, 39 insertions(+), 5 deletions(-) diff --git a/readme.md b/readme.md index 46f121ea3..8109e0289 100644 --- a/readme.md +++ b/readme.md @@ -13,9 +13,44 @@ This repository contains Docker images that can be used to set up [Rodan](https: ## Quick Start +### Run the stack locally + +Requires Docker (with Compose). The app images are pulled from `ghcr.io/ddmal/*:nightly`. + +```bash +make run +``` + +`make run` runs `docker compose up` (with `DOCKER_TAG=nightly` already set for you). **Every container starts itself** — `rodan-main` runs migrations, creates the admin user, collects static files, and launches gunicorn on `:8000`; the `celery`, `py3-celery`, and `gpu-celery` workers start their Celery workers; `nginx` serves the app. There is **no manual start step**. + +> Do **not** run `docker compose exec … /run/start` by hand. The container already runs it on startup, so a second one just fails to bind port 8000 — and because gunicorn logs to `/code/Rodan/gunicorn-error.log` (not your terminal), the command looks like it "silently exited". + +Give it a minute or two on first boot (migrations + `collectstatic`), then open: + +``` +http://localhost +``` + +nginx serves the app on port 80 over plain HTTP — you do **not** need to expose `8000:8000`. Log in with the seeded admin account (Rodan logs in by **email**, not username): + +- **email:** `admin@rodan2.simssa.ca` +- **password:** `rodan` + +(These come from `scripts/local.env` — `ADMIN_EMAIL` / `ADMIN_PASS`.) + +If you run `docker compose up` directly instead of `make run`, set the image tag first — otherwise the images resolve to a blank tag and fail: + +```bash +DOCKER_TAG=nightly docker compose up -d # or put DOCKER_TAG=nightly in a .env file +``` + +Other useful targets: `make stop`, `make clean`, `make build` (rebuild all images), `make health`. + +### Working on Rodan / Rodan Jobs (source checkout) + If you are working on **Rodan** or **Rodan Jobs** -- Make sure you have Rodan submodule cloned in `${repository_root}/rodan/code` and **it is up to date** with the branch you wish to work with. The branches should be either `develop`, or the **name of the feature** you would like to include into develop. The `master` branch is only for version releases and is supposed to be a guaranteed working version. +- Make sure you have Rodan submodule cloned in `${repository_root}/rodan-main/code` and **it is up to date** with the branch you wish to work with. The branches should be either `develop`, or the **name of the feature** you would like to include into develop. The `master` branch is only for version releases and is supposed to be a guaranteed working version. - Follow the instructions here: https://github.com/DDMAL/Rodan/wiki/Working-on-Rodan - Note the `BRANCHES` environment variable in the installation scripts, you can set the environment variable locally by running the following command: `export BRANCHES="develop"`. @@ -36,7 +71,7 @@ A similar concept to using `exec` is using SSH to connect to another computer. W - `docker compose exec ` - The command could be anything eg: `/opt/some_directory/my_shell_script.sh` -- A command you will use frequently is: `docker compose exec rodan bash` or `docker compose exec celery bash` for investigating problems. **You should not be using this command to edit files, use `docker volumes` and your IDE outside of the container.** +- A command you will use frequently is: `docker compose exec rodan-main bash` or `docker compose exec celery bash` for investigating problems. **You should not be using this command to edit files, use `docker volumes` and your IDE outside of the container.** Consult the documentation of the [Docker command line](https://docs.docker.com/engine/reference/commandline/cli/) for additional information. diff --git a/rodan-client/readme.md b/rodan-client/readme.md index 070c017c5..f9dbaa8bf 100644 --- a/rodan-client/readme.md +++ b/rodan-client/readme.md @@ -3,8 +3,7 @@ 2. Navigate to rodan. `make run_arm` 3. Navigate to rodan. `docker compose -f arm-compose.yml exec rodan-main /run/start` 4. Navigate to rodan. `docker compose -f arm-compose.yml exec celery /run/start-celery` -5. Navigate to rodan. `docker compose -f arm-compose.yml exec py3-celery /run/start-celery` -6. `docker compose -f arm-compose.yml exec rodan-client bash` to go to rodan-client container +5. `docker compose -f arm-compose.yml exec rodan-client bash` to go to rodan-client container 7. Inside the container: `cd /code; yarn install` 8. Navigate to rodan-cleint: `cp local-dev/COPYconfiguration code/configuration.json` 9. Navigate to rodan-client: `cp local-dev/CPCONFIGFILE code/src/js/configuration.js` @@ -16,7 +15,7 @@ To Run Rodan Client locally, you must have docker installed and have the docker images pulled from the nightly tag. After installing docker, you can continue by replacing the docker-compose file with the one in the local-dev directory under the rodan-client directory. (dont forget to make the nginx container the same way as you do to run Rodan main for M1 systems) -After the installation, you have to `make run` and `docker compose exec rodan-main /run/start` to have the rodan-main container run. +After the installation, `make run` brings the whole stack up and `rodan-main` starts automatically — you do **not** need to run `docker compose exec rodan-main /run/start` by hand. The main `docker-compose.yml` auto-starts it, so running it a second time just fails to bind port 8000. Next, you will need to CLI into the rodan client and make some manual changes to the config.json file. do `docker compose exec rodan-client bash`.