alessandro trinca tornidor commited on
Commit
f17138a
·
1 Parent(s): d81ec6c

build: migrate Docker images to Docker Hardened Images (DHI)

Browse files

- Builder: dhi.io/python:3.13-dev (has apt, pip, shell)
- Runtime: dhi.io/python:3.13 (no shell, no pkg manager, nonroot user)
- Remove libGL (not needed: PIL manylinux bundles its own)
- Remove system GDAL (not needed: rasterio manylinux wheels bundle it)
- Keep libexpat (needed by rasterio's bundled GDAL at runtime)
- Use nonroot user (65532) instead of custom python (999)
- HEALTHCHECK exec form (no shell in runtime)
- Clean up debug RUN layers in Dockerfile (57→23 lines)
- Add .cursor to .dockerignore
- Update CLAUDE.md Docker structure docs

Base image: 203MB 25CVEs → 22MB 3CVEs (2L + 1 unspecified)

Files changed (4) hide show
  1. .dockerignore +1 -0
  2. CLAUDE.md +6 -2
  3. Dockerfile +14 -47
  4. dockerfiles/dockerfile-samgis-base +32 -61
.dockerignore CHANGED
@@ -2,6 +2,7 @@ venv/
2
  *.pyc
3
  __cache__
4
  .idea
 
5
  tmp/
6
  .env*
7
  __pycache__
 
2
  *.pyc
3
  __cache__
4
  .idea
5
+ .cursor
6
  tmp/
7
  .env*
8
  __pycache__
CLAUDE.md CHANGED
@@ -128,8 +128,12 @@ curl -d@./events/payload_point_colico.json -H 'content-type: application/json' $
128
  ### Docker Structure
129
 
130
  - `Dockerfile`: Main application image (uses pre-built base)
131
- - `dockerfiles/dockerfile-samgis-base`: Multi-stage base image with all dependencies
132
- - `dockerfiles/dockerfile-samgis-base-alpinelinux3.23`: Alpine Linux variant
 
 
 
 
133
 
134
  ### Model Files
135
 
 
128
  ### Docker Structure
129
 
130
  - `Dockerfile`: Main application image (uses pre-built base)
131
+ - `dockerfiles/dockerfile-samgis-base`: Multi-stage base image using Docker Hardened Images (`dhi.io/python:3.13`)
132
+ - Builder: `dhi.io/python:3.13-dev` (has `apt`, `pip`, shell)
133
+ - Runtime: `dhi.io/python:3.13` (no shell, no pkg manager, `nonroot` user 65532)
134
+ - `libexpat` copied from builder (required by rasterio's bundled GDAL)
135
+ - No libGL or system GDAL needed (manylinux wheels bundle everything)
136
+ - `dockerfiles/dockerfile-samgis-base-alpinelinux3.23`: Alpine Linux variant (legacy)
137
 
138
  ### Model Files
139
 
Dockerfile CHANGED
@@ -1,56 +1,23 @@
1
  FROM registry.gitlab.com/aletrn/gis-prediction:1.12.3
2
 
3
- # Include global arg in this stage of the build
4
  ARG WORKDIR_ROOT="/var/task"
5
- ENV VIRTUAL_ENV=${WORKDIR_ROOT}/.venv PATH="${WORKDIR_ROOT}/.venv/bin:$PATH"
6
- ENV WRITE_TMP_ON_DISK=""
7
- ENV VITE__STATIC_INDEX_URL="/static"
8
- ENV VITE__INDEX_URL="/"
9
- ENV HOME_USER=/home/python
10
 
11
- # Set working directory to function root directory
12
  WORKDIR ${WORKDIR_ROOT}
13
- # workaround for missing /home folder
14
- RUN ls -ld ${HOME_USER}
15
- RUN ls -lA ${HOME_USER}
16
 
17
- COPY --chown=python:python app.py ${WORKDIR_ROOT}/
18
- COPY --chown=python:python pyproject.toml poetry.lock README.md ${WORKDIR_ROOT}
19
- RUN chmod 755 ${WORKDIR_ROOT}/client_health.py
20
 
21
- #RUN ls -l ${WORKDIR_ROOT}/app.py ${WORKDIR_ROOT}/client_health.py && chmod 755 ${WORKDIR_ROOT}/app.py ${WORKDIR_ROOT}/client_health.py
22
- # RUN . ${WORKDIR_ROOT}/.venv && which python && echo "# install samgis #" && pip install .
23
- RUN if [ "${WRITE_TMP_ON_DISK}" != "" ]; then mkdir {WRITE_TMP_ON_DISK}; fi
24
- RUN if [ "${WRITE_TMP_ON_DISK}" != "" ]; then ls -l {WRITE_TMP_ON_DISK}; fi
25
 
26
- RUN ls -l /usr/bin/which
27
- RUN /usr/bin/which python
28
- RUN python --version
29
- RUN pip list
30
- RUN echo "PATH: ${PATH}."
31
- RUN echo "WORKDIR_ROOT: ${WORKDIR_ROOT}."
32
- RUN ls -l ${WORKDIR_ROOT}
33
- RUN ls -ld ${WORKDIR_ROOT}
34
- RUN ls -l ${WORKDIR_ROOT}/
35
- RUN ls -l ${WORKDIR_ROOT}/sam-quantized/
36
- RUN ls -l ${WORKDIR_ROOT}/sam-quantized/machine_learning_models
37
- RUN ls -ld ${WORKDIR_ROOT}/sam-quantized/machine_learning_models
38
- RUN python -c "import sys; print(sys.path)"
39
- RUN python -c "import fastapi"
40
- RUN python -c "import geopandas"
41
- RUN python -c "import onnxruntime"
42
- RUN python -c "import rasterio"
43
- RUN python -c "import uvicorn"
44
- RUN python -c "import jinja2"
45
- RUN df -h
46
- RUN ls -l ${WORKDIR_ROOT}/app.py
47
- RUN ls -l ${WORKDIR_ROOT}/client_health.py
48
- RUN ls -l ${WORKDIR_ROOT}/static/
49
- RUN ls -l ${WORKDIR_ROOT}/static/dist
50
- RUN ls -l ${WORKDIR_ROOT}/static/node_modules
51
 
52
- USER 999
53
-
54
- CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
55
- # client_health.py default server_url value is http://localhost:7860/health
56
- HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 CMD /var/task/client_health.py || exit 1
 
1
  FROM registry.gitlab.com/aletrn/gis-prediction:1.12.3
2
 
 
3
  ARG WORKDIR_ROOT="/var/task"
4
+ ENV VIRTUAL_ENV=${WORKDIR_ROOT}/.venv \
5
+ PATH="${WORKDIR_ROOT}/.venv/bin:/opt/python/bin:$PATH" \
6
+ WRITE_TMP_ON_DISK="" \
7
+ VITE__STATIC_INDEX_URL="/static" \
8
+ VITE__INDEX_URL="/"
9
 
 
10
  WORKDIR ${WORKDIR_ROOT}
 
 
 
11
 
12
+ COPY --chown=65532:65532 app.py ${WORKDIR_ROOT}/
13
+ COPY --chown=65532:65532 pyproject.toml poetry.lock README.md ${WORKDIR_ROOT}/
 
14
 
15
+ # Smoke tests: verify imports and model files
16
+ RUN ["python3", "-c", "import fastapi; import onnxruntime; import rasterio; import uvicorn; import jinja2; import geopandas"]
17
+ RUN ["python3", "-c", "from pathlib import Path; assert Path('/var/task/sam-quantized/machine_learning_models').is_dir()"]
 
18
 
19
+ USER 65532
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
 
21
+ CMD ["python3", "-m", "uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
22
+ HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 \
23
+ CMD ["/opt/python/bin/python3", "/var/task/client_health.py"]
 
 
dockerfiles/dockerfile-samgis-base CHANGED
@@ -1,84 +1,62 @@
1
  # Include global ARGs at the dockerfile top
2
- ARG ARCH="x86_64"
3
  ARG WORKDIR_ROOT="/var/task"
4
  ARG FASTAPI_STATIC="${WORKDIR_ROOT}/static"
5
- ARG PYTHONPATH="${WORKDIR_ROOT}:${PYTHONPATH}:/usr/local/lib/python3/dist-packages"
6
  ARG POETRY_NO_INTERACTION=1
7
  ARG POETRY_VIRTUALENVS_IN_PROJECT=1
8
  ARG POETRY_VIRTUALENVS_CREATE=1
9
  ARG POETRY_CACHE_DIR=/tmp/poetry_cache
10
- ARG HOME_USER=/home/python
11
 
12
 
13
- FROM python:3.13-trixie AS builder_global
14
 
15
- ARG ARCH
16
  ARG WORKDIR_ROOT
17
- ARG PYTHONPATH
18
  ARG POETRY_NO_INTERACTION
19
  ARG POETRY_VIRTUALENVS_IN_PROJECT
20
  ARG POETRY_VIRTUALENVS_CREATE
21
  ARG POETRY_CACHE_DIR
22
 
23
- RUN echo "ARCH: $ARCH ..."
24
-
25
- RUN echo "ARG POETRY_CACHE_DIR: ${POETRY_CACHE_DIR} ..."
26
- RUN echo "ARG PYTHONPATH: $PYTHONPATH ..."
27
- RUN echo "arg dep:"
28
-
29
- RUN groupadd -g 999 python && useradd -r -u 999 -g python python
30
  # Set working directory to function root directory
31
- RUN mkdir ${WORKDIR_ROOT} && chown python:python ${WORKDIR_ROOT}
32
  WORKDIR ${WORKDIR_ROOT}
33
- COPY --chown=python:python requirements_poetry.txt pyproject.toml poetry.lock README.md ${WORKDIR_ROOT}/
34
 
35
- # avoid segment-geospatial exception caused by missing libGL.so.1 library
36
- RUN apt update && apt install -y libgl1 python3-pip libexpat1 && apt clean
 
 
 
37
 
38
- # poetry installation path is NOT within ${WORKDIR_ROOT}: not needed for runtime docker image
39
- RUN python -m pip install --upgrade pip \
40
- && python -m pip install -r ${WORKDIR_ROOT}/requirements_poetry.txt && python -m pip list
41
 
42
- RUN which poetry && poetry --version && poetry config --list
43
- RUN poetry config virtualenvs.path ${WORKDIR_ROOT}
44
- RUN echo "# poetry config --list #" && poetry config --list
45
- RUN poetry install --no-cache
46
 
 
 
 
 
47
 
48
- FROM python:3.13-slim-trixie AS runtime
49
 
50
- ARG ARCH
51
- ARG WORKDIR_ROOT
52
- ARG HOME_USER
53
 
54
- ENV VIRTUAL_ENV=${WORKDIR_ROOT}/.venv PATH="${WORKDIR_ROOT}/.venv/bin:$PATH"
55
 
56
- # workaround for missing /home folder
57
- RUN mkdir -p /home
58
- RUN groupadd -g 999 python && useradd -r -u 999 -g python python
59
- RUN ls -l /home
60
- RUN ls -ld /home
61
- RUN mkdir -p ${HOME_USER} ${HOME_USER}/.config && chown python:python -R ${HOME_USER}
62
- RUN ls -ld ${HOME_USER}
63
- RUN ls -lA ${HOME_USER}
64
- RUN mkdir ${WORKDIR_ROOT} && chown python:python ${WORKDIR_ROOT}
65
 
66
- RUN echo "RUNTIME: check libexpat.so*, libGL.so* before start" && ls -l /usr/lib/${ARCH}-linux-gnu/libexpat.so* /usr/lib/${ARCH}-linux-gnu/libGL.so* || echo "libraries libexpat.so*, libGL.so* not found"
67
- COPY --from=builder_global /usr/lib/${ARCH}-linux-gnu/libGL.so* /usr/lib/${ARCH}-linux-gnu/
68
- COPY --from=builder_global /lib/${ARCH}-linux-gnu/libexpat.so* /lib/${ARCH}-linux-gnu/
69
- RUN echo "RUNTIME: check libexpat.so*, libGL.so* after copy" && ls -l /usr/lib/${ARCH}-linux-gnu/libexpat.so* /usr/lib/${ARCH}-linux-gnu/libGL.so*
70
- COPY --from=builder_global ${WORKDIR_ROOT}/.venv ${WORKDIR_ROOT}/.venv
71
- RUN . ${WORKDIR_ROOT}/.venv && which python && pip list
72
 
73
- COPY --chown=python:python ./app.py ./scripts/client_health.py ${WORKDIR_ROOT}/
 
 
74
 
75
- RUN echo "new WORKDIR_ROOT after hidden venv copy => ${WORKDIR_ROOT}"
76
- RUN ls -ld ${WORKDIR_ROOT}/
77
- RUN ls -lA ${WORKDIR_ROOT}/
78
 
79
- RUN python -c "import rasterio"
80
 
81
- ### conditional section
82
  FROM node:24-alpine AS node_fastapi
83
 
84
  ENV PNPM_HOME="/pnpm"
@@ -92,7 +70,6 @@ WORKDIR /appnode
92
  FROM node_fastapi AS node_prod_deps
93
 
94
  RUN --mount=type=cache,id=pnpm,target=/pnpm/store; pnpm install --prod --frozen-lockfile
95
- # here multiple conditions concatenated to avoid failing on check
96
  RUN if [ ! -d /appnode/node_modules ]; then echo "no node_modules folder" && exit 1; fi
97
 
98
 
@@ -101,9 +78,6 @@ FROM node_fastapi AS node_build
101
  ARG VITE__MAP_DESCRIPTION
102
  ARG VITE__SAMGIS_SPACE
103
  ENV VITE__INDEX_URL="/"
104
- RUN echo "VITE__MAP_DESCRIPTION:" ${VITE__MAP_DESCRIPTION}
105
- RUN echo "VITE__SAMGIS_SPACE:" ${VITE__SAMGIS_SPACE}
106
- RUN echo "VITE__INDEX_URL:" ${VITE__INDEX_URL}
107
 
108
  RUN --mount=type=cache,id=pnpm,target=/pnpm/store; pnpm install --frozen-lockfile
109
  RUN --mount=type=cache,id=pnpm,target=/pnpm/store; pnpm build
@@ -116,10 +90,7 @@ FROM runtime
116
  ARG FASTAPI_STATIC
117
  ARG WORKDIR_ROOT
118
 
119
- RUN mkdir ${FASTAPI_STATIC} && chown python:python ${FASTAPI_STATIC}
120
-
121
- COPY --chown=python:python ./sam-quantized ${WORKDIR_ROOT}/sam-quantized
122
- COPY --chown=python:python --from=node_prod_deps /appnode/node_modules* ${FASTAPI_STATIC}/node_modules
123
- COPY --chown=python:python --from=node_build /appnode/dist* ${FASTAPI_STATIC}/dist
124
- COPY --chown=python:python static/list_files.html ${FASTAPI_STATIC}/
125
- RUN ls -l ${FASTAPI_STATIC}/
 
1
  # Include global ARGs at the dockerfile top
 
2
  ARG WORKDIR_ROOT="/var/task"
3
  ARG FASTAPI_STATIC="${WORKDIR_ROOT}/static"
 
4
  ARG POETRY_NO_INTERACTION=1
5
  ARG POETRY_VIRTUALENVS_IN_PROJECT=1
6
  ARG POETRY_VIRTUALENVS_CREATE=1
7
  ARG POETRY_CACHE_DIR=/tmp/poetry_cache
 
8
 
9
 
10
+ FROM dhi.io/python:3.13-dev AS builder_global
11
 
 
12
  ARG WORKDIR_ROOT
 
13
  ARG POETRY_NO_INTERACTION
14
  ARG POETRY_VIRTUALENVS_IN_PROJECT
15
  ARG POETRY_VIRTUALENVS_CREATE
16
  ARG POETRY_CACHE_DIR
17
 
 
 
 
 
 
 
 
18
  # Set working directory to function root directory
19
+ RUN mkdir -p ${WORKDIR_ROOT}
20
  WORKDIR ${WORKDIR_ROOT}
21
+ COPY requirements_poetry.txt pyproject.toml poetry.lock README.md ${WORKDIR_ROOT}/
22
 
23
+ # Install poetry and project dependencies into a venv
24
+ RUN pip install --upgrade pip \
25
+ && pip install -r ${WORKDIR_ROOT}/requirements_poetry.txt
26
+ RUN poetry config virtualenvs.path ${WORKDIR_ROOT} \
27
+ && poetry install --no-cache
28
 
 
 
 
29
 
30
+ FROM dhi.io/python:3.13-dev AS dir_creator
 
 
 
31
 
32
+ ARG WORKDIR_ROOT
33
+ # Pre-create workdir and copy libexpat (needed by rasterio's bundled GDAL at runtime)
34
+ RUN mkdir -p ${WORKDIR_ROOT} && chown 65532:65532 ${WORKDIR_ROOT}
35
+ RUN apt update && apt install -y --no-install-recommends libexpat1 && apt clean && rm -rf /var/lib/apt/lists/*
36
 
 
37
 
38
+ FROM dhi.io/python:3.13 AS runtime
 
 
39
 
40
+ ARG WORKDIR_ROOT
41
 
42
+ ENV VIRTUAL_ENV=${WORKDIR_ROOT}/.venv \
43
+ PATH="${WORKDIR_ROOT}/.venv/bin:/opt/python/bin:$PATH"
 
 
 
 
 
 
 
44
 
45
+ # Copy pre-created workdir with correct ownership
46
+ COPY --from=dir_creator --chown=65532:65532 ${WORKDIR_ROOT} ${WORKDIR_ROOT}
47
+ # Copy libexpat (required by rasterio's bundled GDAL)
48
+ COPY --from=dir_creator /usr/lib/*-linux-gnu/libexpat.so* /usr/lib/
49
+ WORKDIR ${WORKDIR_ROOT}
 
50
 
51
+ # Copy venv from builder and app files
52
+ COPY --from=builder_global --chown=65532:65532 ${WORKDIR_ROOT}/.venv ${WORKDIR_ROOT}/.venv
53
+ COPY --chown=65532:65532 ./app.py ./scripts/client_health.py ${WORKDIR_ROOT}/
54
 
55
+ # Smoke test: verify critical imports work without libGL, libexpat, or system GDAL
56
+ RUN ["python3", "-c", "import rasterio; import onnxruntime; import fastapi; import uvicorn"]
 
57
 
 
58
 
59
+ ### Frontend build (unchanged)
60
  FROM node:24-alpine AS node_fastapi
61
 
62
  ENV PNPM_HOME="/pnpm"
 
70
  FROM node_fastapi AS node_prod_deps
71
 
72
  RUN --mount=type=cache,id=pnpm,target=/pnpm/store; pnpm install --prod --frozen-lockfile
 
73
  RUN if [ ! -d /appnode/node_modules ]; then echo "no node_modules folder" && exit 1; fi
74
 
75
 
 
78
  ARG VITE__MAP_DESCRIPTION
79
  ARG VITE__SAMGIS_SPACE
80
  ENV VITE__INDEX_URL="/"
 
 
 
81
 
82
  RUN --mount=type=cache,id=pnpm,target=/pnpm/store; pnpm install --frozen-lockfile
83
  RUN --mount=type=cache,id=pnpm,target=/pnpm/store; pnpm build
 
90
  ARG FASTAPI_STATIC
91
  ARG WORKDIR_ROOT
92
 
93
+ COPY --chown=65532:65532 ./sam-quantized ${WORKDIR_ROOT}/sam-quantized
94
+ COPY --chown=65532:65532 --from=node_prod_deps /appnode/node_modules* ${FASTAPI_STATIC}/node_modules
95
+ COPY --chown=65532:65532 --from=node_build /appnode/dist* ${FASTAPI_STATIC}/dist
96
+ COPY --chown=65532:65532 static/list_files.html ${FASTAPI_STATIC}/