Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 10 additions & 7 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM ubuntu:latest
FROM python:3.14

RUN apt-get update && apt-get install -y \
git \
Expand Down Expand Up @@ -30,13 +30,16 @@ RUN --mount=type=cache,target=/root/.cache/uv uv sync --no-dev --frozen
# multi-hundred-MB downloads on first use. Stored in ~/.brainglobe/ inside
# the image layer; mount a Docker volume there in production to persist any
# additional atlases users request across container restarts.
# check_latest=False avoids hanging the build if the GIN server is down.
#
# The GIN server that hosts the atlases is frequently slow or unresponsive. We
# require the atlases to be baked in: `timeout` bounds the download so a stalled
# GIN server fails the build instead of hanging it forever, and any failure
# propagates (the build fails) rather than shipping an image without atlases.
# Bump ATLAS_FETCH_TIMEOUT if downloads legitimately need longer.
ENV PATH="/app/.venv/bin:$PATH"
RUN python -c "\
from brainglobe_atlasapi import BrainGlobeAtlas; \
BrainGlobeAtlas('allen_mouse_25um', check_latest=False); \
BrainGlobeAtlas('whs_sd_rat_39um', check_latest=False); \
"
ENV ATLAS_FETCH_TIMEOUT=1200
COPY fetch_atlases.py /fetch_atlases.py
RUN timeout "${ATLAS_FETCH_TIMEOUT}" python /fetch_atlases.py

# Expose the port
ENV INTERNAL_PORT=5008
Expand Down
20 changes: 18 additions & 2 deletions entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,25 @@ echo "$(date '+%Y-%m-%d %H:%M:%S') ADDRESS $ADDRESS"
echo "$(date '+%Y-%m-%d %H:%M:%S') ALLOW_WEBSOCKET_ORIGIN $ALLOW_WEBSOCKET_ORIGIN"
echo "$(date '+%Y-%m-%d %H:%M:%S') NUM_PROCS $NUM_PROCS"

# Verify the required atlases are present before serving. They are normally
# baked into the image at build time, so this is a fast no-op; it only does work
# if a runtime volume mount shadowed ~/.brainglobe/. REQUIRE_ATLASES=1
# (default) makes a still-missing atlas a hard startup failure; set
# REQUIRE_ATLASES=0 to warn and start anyway (atlases download lazily on use).
REQUIRE_ATLASES=${REQUIRE_ATLASES:-1}
echo "$(date '+%Y-%m-%d %H:%M:%S') verifying atlases ..."
if ! uv run --no-dev python /fetch_atlases.py; then
if [ "$REQUIRE_ATLASES" = "1" ]; then
echo "$(date '+%Y-%m-%d %H:%M:%S') ERROR: required atlases unavailable; aborting startup" >&2
exit 1
fi
echo "$(date '+%Y-%m-%d %H:%M:%S') WARNING: atlases unavailable; continuing (will download lazily)" >&2
fi

# Start the Panel application
exec uv run panel serve ./app.py \
# Start the Panel application. --no-dev matches the build-time `uv sync
# --no-dev` so `uv run` doesn't re-sync dev dependencies (sphinx, etc.) into the
# environment at container startup.
exec uv run --no-dev panel serve ./app.py \
--address "$ADDRESS" \
--port "$INTERNAL_PORT" \
--allow-websocket-origin "$ALLOW_WEBSOCKET_ORIGIN" \
Expand Down
61 changes: 61 additions & 0 deletions fetch_atlases.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
"""Ensure the atlases PixelMap needs are present in the local brainglobe cache.

Used in two places:

* At Docker build time (under `timeout`), to bake the atlases into the image
layer. The build *requires* this to succeed: a non-zero exit fails the build
so we never ship an image without atlases, and `timeout` turns a stalled GIN
server into a bounded failure instead of an infinite hang.
* At container startup (via entrypoint.sh), as a cheap safety net: it re-verifies
the atlases are present (e.g. in case a runtime volume mount shadowed
~/.brainglobe/) and re-fetches anything missing before the app boots.

Because both callers run the same code, the startup check is a no-op in the
normal case β€” brainglobe stores atlases on disk and we skip any that are already
downloaded.

Exit codes:
0 all required atlases are present (downloaded now or already cached)
1 at least one required atlas is still missing after attempting a download
"""

from __future__ import annotations

import sys

from brainglobe_atlasapi import BrainGlobeAtlas
from brainglobe_atlasapi.list_atlases import get_downloaded_atlases

# Atlases baked into the image / required for the app to be useful offline.
REQUIRED_ATLASES = (
"allen_mouse_25um",
"whs_sd_rat_39um",
)


def ensure_atlas(name: str) -> bool:
"""Make sure `name` is downloaded. Return True if present afterwards."""
if name in get_downloaded_atlases():
print(f"atlas already cached: {name}")
return True
try:
# Constructing the atlas triggers the download into ~/.brainglobe/.
# check_latest=False avoids the remote version check hanging the boot.
print(f"downloading atlas: {name} ...")
BrainGlobeAtlas(name, check_latest=False)
except Exception as exc: # network/GIN failure β€” report, don't crash caller
print(f"failed to download {name}: {exc}", file=sys.stderr)
return name in get_downloaded_atlases()


def main() -> int:
missing = [name for name in REQUIRED_ATLASES if not ensure_atlas(name)]
if missing:
print(f"missing atlases after fetch attempt: {', '.join(missing)}",
file=sys.stderr)
return 1
return 0


if __name__ == "__main__":
raise SystemExit(main())