Skip to content

Commit 1526a70

Browse files
scottstanieclaude
andcommitted
Build and publish Docker image to GHCR in CI
- Add `dockerize` job to ci.yml: builds on every push/PR (no push for PRs), tags `:<version>` always, `:develop` on main, `:latest` on `v*` tags. Uses Buildx + GHA cache so the pixi conda solve survives across runs. - Pass setuptools_scm version into the build via `--build-arg VERSION=` so the image reports the real version; image tag uses a sanitized form (Docker tags forbid `+`, but PEP 440 forbids `-`, so we keep both). - Add `.dockerignore` to keep build context lean (skips node_modules, .pixi, data/, screenshots, .git, etc). - Add OCI labels (title, description, source, licenses, ...) to Dockerfile. - Update deploy/README.md: pull-from-GHCR is now the primary path, and document the local-build prerequisites (frontend bundle, --platform linux/amd64 on Apple Silicon). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 3c92876 commit 1526a70

4 files changed

Lines changed: 179 additions & 19 deletions

File tree

.dockerignore

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# Pixi environment (recreated inside the container)
2+
.pixi/
3+
4+
# Python bytecode
5+
__pycache__/
6+
*.py[cod]
7+
*$py.class
8+
*.so
9+
10+
# Distribution / packaging (root-anchored — does NOT exclude src/bowser/dist/)
11+
build/
12+
dist/
13+
*.egg-info/
14+
*.egg
15+
16+
# Test / coverage / type-check caches
17+
.pytest_cache/
18+
.coverage
19+
.coverage.*
20+
htmlcov/
21+
.mypy_cache/
22+
.ruff_cache/
23+
24+
# Documentation build output
25+
docs/_build/
26+
site/
27+
28+
# IDE / editor / OS files
29+
.vscode/
30+
.idea/
31+
*.swp
32+
*~
33+
.DS_Store
34+
Thumbs.db
35+
*.local
36+
37+
# Git
38+
.git/
39+
.gitignore
40+
.git-blame-ignore-revs
41+
.gitattributes
42+
43+
# Frontend tooling: bundle is built on the runner before `docker build`,
44+
# so node_modules itself is not needed in the image context.
45+
node_modules/
46+
47+
# Local data and artifacts that have no business in the build context
48+
data/
49+
tests/data/
50+
screenshots/
51+
iris-screenshots/
52+
tre-maps-screenshots/
53+
viz-lonboard-*.html
54+
viz-lonboard-*.ipynb
55+
*.parquet/
56+
work-walkthrough2
57+
58+
# Notebook checkpoints
59+
.ipynb_checkpoints/
60+
61+
# Env / secrets
62+
.env

.github/workflows/ci.yml

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,79 @@ jobs:
3838
- name: Test
3939
run: |
4040
pixi run -e all pytest
41+
42+
dockerize:
43+
name: Build Docker image and push to GHCR
44+
runs-on: ubuntu-latest
45+
permissions:
46+
contents: read
47+
packages: write
48+
steps:
49+
- name: Checkout
50+
uses: actions/checkout@v6
51+
with:
52+
fetch-depth: 0
53+
- name: Set up Node
54+
uses: actions/setup-node@v4
55+
with:
56+
node-version: '20'
57+
cache: 'npm'
58+
- name: Build frontend bundle
59+
# Dockerfile copies src/ which must contain the built frontend at
60+
# src/bowser/dist/. That directory is gitignored, so it has to be
61+
# produced on the runner before `docker build` is invoked.
62+
run: |
63+
npm ci
64+
npm run build
65+
- name: Set up Python
66+
uses: actions/setup-python@v6
67+
with:
68+
python-version: '3.11'
69+
- name: Compute version from setuptools_scm
70+
run: |
71+
pip install setuptools_scm
72+
version=$(python -m setuptools_scm)
73+
# BOWSER_VERSION stays PEP 440 (keeps '+') — used for the build arg
74+
# so setuptools_scm can read it back inside the image.
75+
# BOWSER_IMAGE_TAG sanitizes '+' to '-' because Docker tags forbid '+'.
76+
echo "BOWSER_VERSION=${version}" >> "$GITHUB_ENV"
77+
echo "BOWSER_IMAGE_TAG=${version//+/-}" >> "$GITHUB_ENV"
78+
- name: Set up Docker Buildx
79+
uses: docker/setup-buildx-action@v3
80+
- name: Login to GitHub Container Registry
81+
if: github.event_name != 'pull_request'
82+
uses: docker/login-action@v4
83+
with:
84+
registry: ghcr.io
85+
username: ${{ github.actor }}
86+
password: ${{ secrets.GITHUB_TOKEN }}
87+
- name: Build, tag, and push image to GHCR
88+
uses: docker/build-push-action@v7
89+
with:
90+
context: .
91+
file: ./Dockerfile
92+
push: ${{ github.event_name != 'pull_request' }}
93+
build-args: |
94+
VERSION=${{ env.BOWSER_VERSION }}
95+
tags: |
96+
ghcr.io/${{ github.repository }}:${{ env.BOWSER_IMAGE_TAG }}
97+
labels: |
98+
org.opencontainers.image.version=${{ env.BOWSER_VERSION }}
99+
org.opencontainers.image.revision=${{ github.sha }}
100+
org.opencontainers.image.created=${{ github.event.head_commit.timestamp }}
101+
# GHA-cache layers across runs — biggest win is the pixi conda solve
102+
# in stage 1, which otherwise re-runs from scratch each build.
103+
cache-from: type=gha
104+
cache-to: type=gha,mode=max
105+
- name: Add develop tag (main branch only)
106+
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main'
107+
uses: akhilerm/tag-push-action@v2.2.0
108+
with:
109+
src: ghcr.io/${{ github.repository }}:${{ env.BOWSER_IMAGE_TAG }}
110+
dst: ghcr.io/${{ github.repository }}:develop
111+
- name: Add latest tag (release tags only)
112+
if: startsWith(github.ref, 'refs/tags/v')
113+
uses: akhilerm/tag-push-action@v2.2.0
114+
with:
115+
src: ghcr.io/${{ github.repository }}:${{ env.BOWSER_IMAGE_TAG }}
116+
dst: ghcr.io/${{ github.repository }}:latest

Dockerfile

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,28 +7,47 @@ FROM ghcr.io/prefix-dev/pixi:0.65.0 AS install
77

88
WORKDIR /app
99

10+
# `.git` is excluded from the build context (.dockerignore), so setuptools_scm
11+
# cannot read the version from git. The CI workflow passes the real version
12+
# via --build-arg VERSION=...; local builds default to 0.0.0.
13+
ARG VERSION=0.0.0
14+
ENV SETUPTOOLS_SCM_PRETEND_VERSION_FOR_BOWSER_INSAR=${VERSION}
15+
1016
COPY pyproject.toml pixi.lock ./
1117

12-
# Create minimal source structure so pixi can resolve the editable workspace
18+
# Minimal source structure so pixi can resolve the editable workspace
1319
RUN mkdir -p src/bowser && \
1420
touch src/bowser/__init__.py && \
15-
printf 'version = "0.0.0"\n__version__ = version\n' > src/bowser/_version.py
21+
printf 'version = "%s"\n__version__ = version\n' "${VERSION}" > src/bowser/_version.py
1622

17-
ENV SETUPTOOLS_SCM_PRETEND_VERSION_FOR_BOWSER_INSAR=0.0.0
1823
RUN --mount=type=cache,target=/root/.cache/rattler/cache,sharing=private \
1924
pixi install -e default
2025

2126
# Stage 2: drop the real source + bake the activation script
2227
FROM install AS build
2328

29+
ARG VERSION=0.0.0
30+
ENV SETUPTOOLS_SCM_PRETEND_VERSION_FOR_BOWSER_INSAR=${VERSION}
31+
2432
COPY src/ /app/src/
2533

34+
# _version.py is gitignored, so the real source tree probably lacks it.
35+
# Re-write it with the build-arg version so the running image reports correctly.
36+
RUN printf 'version = "%s"\n__version__ = version\n' "${VERSION}" > src/bowser/_version.py
37+
2638
RUN pixi shell-hook -e default > /activate.sh && \
2739
chmod +x /activate.sh
2840

2941
# Stage 3: minimal production image — no pixi, just the resolved env
3042
FROM ubuntu:24.04 AS production
3143

44+
LABEL org.opencontainers.image.title="bowser"
45+
LABEL org.opencontainers.image.description="Web browsing tool for InSAR data"
46+
LABEL org.opencontainers.image.source="https://github.com/opera-adt/bowser"
47+
LABEL org.opencontainers.image.url="https://github.com/opera-adt/bowser"
48+
LABEL org.opencontainers.image.documentation="https://github.com/opera-adt/bowser#readme"
49+
LABEL org.opencontainers.image.licenses="BSD-3-Clause OR Apache-2.0"
50+
3251
RUN apt-get update && apt-get install -y --no-install-recommends \
3352
ca-certificates curl \
3453
&& rm -rf /var/lib/apt/lists/*

deploy/README.md

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,27 @@
11
# Deploy
22

3-
Minimal path to run bowser on one EC2 instance serving a catalog of GeoZarr
4-
stores from S3.
3+
Minimal path to run bowser on one EC2 instance serving a catalog of GeoZarr stores from S3.
54

65
## Build the image
76

7+
CI builds and pushes `ghcr.io/opera-adt/bowser:<version>` (plus `:develop` on `main` and `:latest` on `v*` tags) — see `.github/workflows/ci.yml`. Pull the published image with:
8+
9+
```bash
10+
docker pull ghcr.io/opera-adt/bowser:latest
11+
```
12+
13+
To build locally:
14+
815
```bash
9-
docker build -t bowser:local .
10-
# or tag for a registry and push
11-
docker tag bowser:local ghcr.io/opera-adt/bowser:latest
12-
docker push ghcr.io/opera-adt/bowser:latest
16+
# Frontend bundle is gitignored; build it first so it gets COPY'd into the image.
17+
npm ci && npm run build
18+
19+
# On Apple Silicon, force linux/amd64 — pyproject.toml's pixi platforms list
20+
# doesn't include linux-aarch64.
21+
docker build --platform linux/amd64 -t bowser:local .
1322
```
1423

15-
The image uses a three-stage pixi build: pixi resolves conda-forge GDAL /
16-
rasterio / rioxarray in stage 1, the final `ubuntu:24.04` stage ships only
17-
the resolved env and bowser source. No pixi, no python build tools in
18-
production.
24+
The image uses a three-stage pixi build: pixi resolves conda-forge GDAL / rasterio / rioxarray in stage 1, the final `ubuntu:24.04` stage ships only the resolved env and bowser source. No pixi, no python build tools in production.
1925

2026
## Run locally
2127

@@ -49,12 +55,9 @@ aws s3 cp catalog.toml s3://bowser-demo-data/catalog.toml
4955

5056
Launch an EC2 instance (Amazon Linux 2023 or Ubuntu 24.04):
5157
- Instance type: `t3.medium` is enough for a demo; `c6i.large` for concurrent viewers.
52-
- IAM instance profile: needs `s3:GetObject` on the data bucket (both the
53-
catalog object and the dataset prefixes it references).
54-
- Security group: open port 80 (HTTP). For HTTPS, add Caddy or put an ALB
55-
in front.
56-
- User data: `deploy/ec2-bootstrap.sh`, with `IMAGE` and `CATALOG_S3` edited
57-
at the top.
58+
- IAM instance profile: needs `s3:GetObject` on the data bucket (both the catalog object and the dataset prefixes it references).
59+
- Security group: open port 80 (HTTP). For HTTPS, add Caddy or put an ALB in front.
60+
- User data: `deploy/ec2-bootstrap.sh`, with `IMAGE` and `CATALOG_S3` edited at the top.
5861

5962
First boot takes 1-2 min (docker install + image pull). Check progress:
6063
```bash

0 commit comments

Comments
 (0)