Replace Singularity with Docker; add production deployment setup
- Remove Singularity.def; add Dockerfile (python:3.11-slim + ImageMagick, non-root user, installs gallery package with dev extras) - Add .dockerignore to keep image lean - Rewrite .gitlab-ci.yml: build→test→publish stages using Docker-in-Docker; push per-commit SHA tag and promote to :latest on main - Add docker-compose.yml: gallery-generator + nginx services sharing a named volume; configurable GENERATE_INTERVAL env var - Add deploy/nginx.conf: gzip, security headers, correct caching policy (immutable for assets, no-store for HTML) - Add deploy/entrypoint.sh: runs gallery generate on startup then loops on GENERATE_INTERVAL; exits cleanly when interval is 0 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
.git
|
||||
.venv
|
||||
*.sif
|
||||
*.ipynb
|
||||
__pycache__
|
||||
*.pyc
|
||||
*.pyo
|
||||
.pytest_cache
|
||||
build
|
||||
*.egg-info
|
||||
.claude
|
||||
config.yaml
|
||||
deploy/
|
||||
docs/
|
||||
README.md
|
||||
CLAUDE.md
|
||||
uv.lock
|
||||
+72
-38
@@ -1,59 +1,70 @@
|
||||
# GitLab CI/CD Pipeline for Gallery Generator
|
||||
# Builds Apptainer container and runs tests inside it
|
||||
# GitLab CI/CD Pipeline for ETPlot
|
||||
# Builds a Docker image and runs tests inside it.
|
||||
|
||||
stages:
|
||||
- build
|
||||
- test
|
||||
- publish
|
||||
|
||||
variables:
|
||||
CONTAINER_IMAGE: "gallery-generator.sif"
|
||||
APPTAINER_CACHE_DIR: "$CI_PROJECT_DIR/.apptainer-cache"
|
||||
IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
|
||||
IMAGE_LATEST: $CI_REGISTRY_IMAGE:latest
|
||||
|
||||
# Cache to speed up builds
|
||||
cache:
|
||||
key: "$CI_COMMIT_REF_SLUG"
|
||||
paths:
|
||||
- .apptainer-cache/
|
||||
# ---------------------------------------------------------------------------
|
||||
# Build
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# Build the Apptainer container
|
||||
build:container:
|
||||
build:image:
|
||||
stage: build
|
||||
tags:
|
||||
- apptainer
|
||||
image: docker:27
|
||||
services:
|
||||
- docker:27-dind
|
||||
variables:
|
||||
DOCKER_TLS_CERTDIR: "/certs"
|
||||
before_script:
|
||||
- docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" "$CI_REGISTRY"
|
||||
script:
|
||||
- echo "Building Apptainer container..."
|
||||
- apptainer --version
|
||||
- apptainer build --fakeroot $CONTAINER_IMAGE Singularity.def
|
||||
- ls -lh $CONTAINER_IMAGE
|
||||
artifacts:
|
||||
paths:
|
||||
- $CONTAINER_IMAGE
|
||||
expire_in: 1 hour
|
||||
- docker build --pull -t "$IMAGE_TAG" .
|
||||
- docker push "$IMAGE_TAG"
|
||||
rules:
|
||||
- if: $CI_COMMIT_BRANCH
|
||||
|
||||
# Run tests inside the container
|
||||
test:pytest:
|
||||
# ---------------------------------------------------------------------------
|
||||
# Test
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
.test_base:
|
||||
stage: test
|
||||
tags:
|
||||
- apptainer
|
||||
dependencies:
|
||||
- build:container
|
||||
image: docker:27
|
||||
services:
|
||||
- docker:27-dind
|
||||
variables:
|
||||
DOCKER_TLS_CERTDIR: "/certs"
|
||||
before_script:
|
||||
- docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" "$CI_REGISTRY"
|
||||
- docker pull "$IMAGE_TAG"
|
||||
needs:
|
||||
- build:image
|
||||
|
||||
test:pytest:
|
||||
extends: .test_base
|
||||
script:
|
||||
- echo "Running pytest inside container..."
|
||||
- apptainer exec $CONTAINER_IMAGE pytest /src/tests/ -v
|
||||
- docker run --rm -w /app "$IMAGE_TAG" python -m pytest tests/ -v
|
||||
artifacts:
|
||||
when: always
|
||||
expire_in: 30 days
|
||||
|
||||
# Run tests with coverage
|
||||
test:coverage:
|
||||
stage: test
|
||||
tags:
|
||||
- apptainer
|
||||
dependencies:
|
||||
- build:container
|
||||
extends: .test_base
|
||||
script:
|
||||
- echo "Running coverage analysis inside container..."
|
||||
- apptainer exec $CONTAINER_IMAGE pytest /src/tests/ --cov=/src --cov-report=xml --cov-report=term
|
||||
- apptainer exec $CONTAINER_IMAGE cat /src/coverage.xml > coverage.xml || echo "No coverage.xml found"
|
||||
- |
|
||||
docker run --rm -w /app \
|
||||
-v "$CI_PROJECT_DIR:/artifacts" \
|
||||
"$IMAGE_TAG" \
|
||||
python -m pytest tests/ \
|
||||
--cov=gallery \
|
||||
--cov-report=xml:/artifacts/coverage.xml \
|
||||
--cov-report=term
|
||||
coverage: '/TOTAL.*\s+(\d+%)$/'
|
||||
artifacts:
|
||||
reports:
|
||||
@@ -63,3 +74,26 @@ test:coverage:
|
||||
paths:
|
||||
- coverage.xml
|
||||
expire_in: 30 days
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Publish latest tag on main
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
publish:latest:
|
||||
stage: publish
|
||||
image: docker:27
|
||||
services:
|
||||
- docker:27-dind
|
||||
variables:
|
||||
DOCKER_TLS_CERTDIR: "/certs"
|
||||
before_script:
|
||||
- docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" "$CI_REGISTRY"
|
||||
script:
|
||||
- docker pull "$IMAGE_TAG"
|
||||
- docker tag "$IMAGE_TAG" "$IMAGE_LATEST"
|
||||
- docker push "$IMAGE_LATEST"
|
||||
needs:
|
||||
- test:pytest
|
||||
- test:coverage
|
||||
rules:
|
||||
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
|
||||
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
FROM python:3.11-slim
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends imagemagick \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY pyproject.toml .
|
||||
COPY gallery/ gallery/
|
||||
COPY tests/ tests/
|
||||
COPY deploy/entrypoint.sh deploy/entrypoint.sh
|
||||
|
||||
RUN pip install --no-cache-dir ".[dev]"
|
||||
|
||||
RUN useradd -r -s /bin/false gallery
|
||||
USER gallery
|
||||
|
||||
ENTRYPOINT ["gallery"]
|
||||
CMD ["generate"]
|
||||
@@ -1,20 +0,0 @@
|
||||
Bootstrap: docker
|
||||
From: python:3.11-slim
|
||||
|
||||
%post
|
||||
apt-get update
|
||||
apt-get install -y --no-install-recommends imagemagick
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
pip install --no-cache-dir jinja2 pyyaml coverage pytest pytest-cov
|
||||
mkdir -p /src
|
||||
|
||||
%files
|
||||
. /src
|
||||
|
||||
%environment
|
||||
export PYTHONPATH=/src
|
||||
|
||||
%runscript
|
||||
cd /src
|
||||
exec python3 generate_gallery.py "$@"
|
||||
Executable
+26
@@ -0,0 +1,26 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
CONFIG_FILE="${CONFIG_FILE:-/config/config.yaml}"
|
||||
GENERATE_INTERVAL="${GENERATE_INTERVAL:-300}" # seconds between regenerations; 0 = run once and exit
|
||||
|
||||
if [ ! -f "$CONFIG_FILE" ]; then
|
||||
echo "ERROR: config file not found at $CONFIG_FILE" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
run_generate() {
|
||||
echo "[$(date -u +%FT%TZ)] Running gallery generate..."
|
||||
gallery --config "$CONFIG_FILE" generate --verbose
|
||||
}
|
||||
|
||||
run_generate
|
||||
|
||||
if [ "$GENERATE_INTERVAL" -eq 0 ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
while true; do
|
||||
sleep "$GENERATE_INTERVAL"
|
||||
run_generate
|
||||
done
|
||||
@@ -0,0 +1,40 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
|
||||
root /var/www/gallery;
|
||||
index index.html;
|
||||
|
||||
# Security headers
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
add_header X-XSS-Protection "1; mode=block" always;
|
||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||
|
||||
# Gzip
|
||||
gzip on;
|
||||
gzip_vary on;
|
||||
gzip_types text/plain text/css application/javascript application/json image/svg+xml;
|
||||
gzip_min_length 1024;
|
||||
|
||||
# Static assets: cache aggressively
|
||||
location ~* \.(css|js|png|jpg|jpeg|gif|ico|svg|woff2?)$ {
|
||||
expires 7d;
|
||||
add_header Cache-Control "public, immutable";
|
||||
}
|
||||
|
||||
# HTML: no cache so regenerated galleries are picked up immediately
|
||||
location ~* \.html$ {
|
||||
expires -1;
|
||||
add_header Cache-Control "no-store";
|
||||
}
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ $uri/index.html =404;
|
||||
}
|
||||
|
||||
# Deny access to hidden files
|
||||
location ~ /\. {
|
||||
deny all;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
services:
|
||||
generator:
|
||||
build: .
|
||||
image: etplot-gallery:latest
|
||||
entrypoint: ["/app/deploy/entrypoint.sh"]
|
||||
environment:
|
||||
CONFIG_FILE: /config/config.yaml
|
||||
# How often to regenerate the gallery (seconds). Set to 0 to run once and exit.
|
||||
GENERATE_INTERVAL: "300"
|
||||
volumes:
|
||||
- ./config.yaml:/config/config.yaml:ro
|
||||
# Mount your plot source directories here, matching the paths in config.yaml.
|
||||
# Example:
|
||||
# - /path/to/plots:/plots:ro
|
||||
- gallery_output:/var/www/gallery
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD", "gallery", "--help"]
|
||||
interval: 60s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 30s
|
||||
|
||||
web:
|
||||
image: nginx:1.27-alpine
|
||||
ports:
|
||||
- "${HTTP_PORT:-8080}:80"
|
||||
volumes:
|
||||
- ./deploy/nginx.conf:/etc/nginx/conf.d/default.conf:ro
|
||||
- gallery_output:/var/www/gallery:ro
|
||||
depends_on:
|
||||
- generator
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "-qO-", "http://localhost/"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 10s
|
||||
|
||||
volumes:
|
||||
gallery_output:
|
||||
Reference in New Issue
Block a user