Containerization and deployment infrastructure for Deep Blue Documents, the University of Michigan Library's institutional repository, built on DSpace 7+.
If you are an AI coding agent (GitHub Copilot, Claude, Cursor, or similar), read
AGENTS.mdbefore taking any action in this repository. It contains mandatory guidelines for CLI usage, task tracking, and Markdown formatting that apply to every agent session.
This repository is the source of truth for building and deploying Deep Blue Documents — U-M Library's DSpace-based institutional repository. It produces Docker images from the library's own forks of DSpace (mlibrary/DSpace) and the Angular frontend (mlibrary/dspace-angular), layering U-M-specific configuration and tooling on top of upstream DSpace.
A shared source image is built first by cloning those forks. It is then consumed by the frontend, backend, and solr service images. Together with a db image, these form a complete DSpace stack that can be run locally via Docker Compose or deployed remotely to Kubernetes / OpenShift.
There are two deployment contexts:
- Local — Docker Desktop; images built with
docker compose build, deployed withdocker compose up -d. - Remote — Kubernetes/OpenShift cluster; images built by GitHub Actions, stored in GitHub Packages, and deployed by applying the YAML manifests in
dspace/(Kubernetes) ordspace-uid/(OpenShift). Seedspace/README.mdfor build, deployment, and known security concerns that should be reviewed before production use.
It is recommended to get the stack running locally via Docker Compose before attempting a remote deployment.
The canonical branch is main. All development work and pull requests target main. CI runs on direct pushes to main and on all PRs targeting main.
These are forks of the official DSpace repositories. In each fork:
mainis kept in sync with upstream official DSpace — it is never pushed to directly.umichis the canonical development branch where U-M-specific changes live. It always pulls frommain(to incorporate upstream updates) but never pushes back tomain.
The GITHUB_BRANCH build argument (default: umich) controls which branch of these forks is cloned when building the source image. In CI it is carried as SOURCE_BRANCH because GitHub Actions reserves all GITHUB_* variable names.
While this repository is configured for the University of Michigan's Deep Blue Documents service, it is designed to serve as a reference architecture for how to containerize and orchestrate a heavily customized DSpace 7+ environment using Docker Compose, Kubernetes, and OpenShift.
What is reusable: the multi-stage Dockerfile patterns,
docker-compose.ymlservice structure, Makefile workflow, smoke-test suite (tests/), and GitHub Actions CI pipeline (.github/workflows/ci.yml) are general-purpose and straightforward to adapt.What is U-M-specific: the source forks (
mlibrary/DSpace,mlibrary/dspace-angular), theGITHUB_BRANCH=umichdefault, backend scripts inbackend/bin/, andbackend/local.cfg.To adapt this for your own institution, point
GITHUB_BRANCH(or a fork of your own) at your customized DSpace source and replacebackend/local.cfgwith your own overrides. Note thatbackend/local.cfgis only copied into images built by the rootbackend.dockerfile(local dev andci.yml); thedspace/backend.dockerfileused to build published production/staging images does not copy it — configuration for those environments is supplied at runtime via environment variables or mounted Kubernetes Secrets.
- (Optional) Copy
.env.exampleto.envand adjust build arguments as needed. - Build the shared source image (required once, and whenever the source branch changes):
docker build -t dspace-containerization-source .The
frontend,backend, andsolrimages depend on this image at build time. Usemake build(see Makefile) to build source + all compose services in one step. - Build the compose service images:
docker compose build
- Start the core services:
docker compose up -d
dbandsolrinclude healthchecks;backendwill not start until both are healthy.
The apache and express services are not started by default. To include them:
docker compose --profile optional up -dOr start a single optional service:
docker compose --profile optional up -d apache
docker compose --profile optional up -d express| URL | Container | Comments |
|---|---|---|
| http://localhost:4000/ | frontend | Angular GUI (SSR app shell; Angular router handles /home etc. client-side) |
| jdbc:postgresql://localhost:5432/dspace | db | PostgreSQL (user: dspace, password: dspace) |
| http://localhost:8080/server | backend | Server API |
| http://localhost:8983/solr | solr | Solr GUI |
| http://localhost:8888/ | apache | Apache Web Server – optional (CGI stats scripts) |
| http://localhost:3000/metrics | express | Prometheus metrics endpoint – optional |
Build arguments are read from .env (copy from .env.example):
GITHUB_BRANCH=umich
DSPACE_VERSION=7.6
JDK_VERSION=17
DSPACE_UI_HOST=0.0.0.0
DSPACE_REST_HOST=backend
GITHUB_BRANCH— branch in the mlibrary forks used to build the source image.DSPACE_VERSION— version suffix for DSpace Docker Hub images (e.g.7.6→ image tagdspace-7.6). Use7.6here; the current upstream DSpace patch release targeted by this configuration is 7.6.0.JDK_VERSION— Java version for the backend Tomcat image (must be17; JDK11 is no longer supported). The build useseclipse-temurinimages — the official successor to the deprecatedopenjdkDocker Hub images.DSPACE_UI_HOST— hostname the Angular SSR server binds to. Use0.0.0.0for local Docker development (Node.js 18+ resolveslocalhostto::1, breaking Docker port-mapping). Set to the public hostname for staging/production.DSPACE_REST_HOST— hostname the Angular SSR server (inside the frontend container) uses to reach the backend REST API over Docker's internal DNS. Usebackend(the Docker service name) for local development. The browser-side Angular client re-uses thedspaceServerURL from the HAL root at runtime.
docker-compose.yml passes DSPACE_VERSION and JDK_VERSION automatically to the relevant service builds via build.args.
- The backend container exposes port 8000 (JDWP remote debugger — root
backend.dockerfilefor local dev only) and port 8009 (AJP connector). Neither is mapped indocker-compose.ymlby default. Add a port mapping todocker-compose.ymlif you need to attach a remote debugger locally. - The
backendservice usesdepends_onwithcondition: service_healthyfordbandsolr, and thefrontendservice waits forbackendto be healthy, ensuring correct startup ordering without manual delays. - Use
maketargets (see Makefile) for common workflows:make build,make up,make down,make clean. backend/local.cfgdisables OIDC authentication for local dev and supplies placeholder values forip.bioIPsRange1/ip.bioIPsRange2.OidcAuthenticationBeanis still instantiated by Spring even when removed from the authentication plugin sequence, and its initialisation path callsString.split()on those properties unconditionally — omitting them causes aNullPointerExceptionthat returns HTTP 500 on every/server/apiendpoint and the actuator. This file is only copied into images built by the rootbackend.dockerfile(local dev andci.yml). Thedspace/backend.dockerfile, used to build the production/staging images published toghcr.io, does not copy it at all.
A shell-based smoke test suite lives in tests/. It requires only bash and curl.
bash tests/smoke.shmake testThis is equivalent to:
make up # docker compose up -d
bash tests/wait-for-stack.sh # poll until backend/solr/frontend are ready
bash tests/smoke.sh # run all assertions| Layer | Endpoint | Assertion |
|---|---|---|
| Backend REST API | GET /server/api |
HTTP 200, HAL _links present |
| Backend REST API | GET /server/api |
dspaceVersion and dspaceServer fields present |
| Backend REST API | GET /server/api/core/communities |
HTTP 200 |
| Backend REST API | GET /server/api/core/collections |
HTTP 200 |
| Backend REST API | GET /server/api/authn/status |
HTTP 200, "authenticated":false |
| Backend Actuator | GET /server/actuator/health |
"status":"UP" or "UP_WITH_ISSUES" |
| Solr | GET /solr/admin/info/system |
HTTP 200, version info present |
| Solr | GET /solr/admin/cores |
All four DSpace cores present (authority, oai, search, statistics) |
| Solr | GET /solr/search/admin/ping |
HTTP 200 |
| Frontend | GET / |
HTTP 200, no ng-error boundary |
| Frontend (SSR) | GET /communities/ |
HTTP 200, ds-root element present, DSpace title present |
The workflow .github/workflows/ci.yml is the primary CI workflow. It runs automatically on:
- Direct pushes to
main— validates the branch after a merge. - Pull requests targeting
main— validates every push to a PR branch before it lands.
Feature branches that do not yet have an open PR targeting main will not trigger CI automatically. To run the full smoke-test suite against any branch manually, use the workflow_dispatch trigger from the GitHub Actions UI (or gh workflow run ci.yml) with optional dspace_version, jdk_version, and source_branch inputs.
The workflow is scoped to the canonical mlibrary/dspace-containerization repository so fork runs do not consume runner minutes.
Additional image-building workflows live alongside ci.yml and can be used to publish individual service images to GitHub Packages independently of the full stack test:
| Workflow | Purpose |
|---|---|
build-source-image.yml |
Builds and publishes the shared source image |
build-dspace-images.yml |
Builds frontend, backend, and solr images |
build-db-image.yml |
Builds the PostgreSQL db image |
build-apache-image.yml |
Builds the optional Apache image |
build-express-image.yml |
Builds the optional Express metrics image |
build-dspace-uid-images.yml |
OpenShift UID-safe variants of the DSpace images |
build-db-uid-image.yml |
OpenShift UID-safe db image |
build-apache-uid-image.yml |
OpenShift UID-safe Apache image |
delete-old-workflow-runs.yml |
Housekeeping – prunes stale workflow run history |
Note on
GITHUB_BRANCHvsSOURCE_BRANCH: locally (Makefile /.env) the build arg is calledGITHUB_BRANCHand is passed directly todocker build. In the CI workflow it is stored in an env var calledSOURCE_BRANCH— because GitHub Actions reserves all variables prefixed withGITHUB_and will fail the job if one is set in anenv:block — then forwarded to Docker as--build-arg GITHUB_BRANCH=${SOURCE_BRANCH}, so theDockerfileandMakefilerequire no changes.