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
76 changes: 76 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## What This Repo Does

Builds and publishes Docker images for LabKey Server (a biomedical data management platform) to AWS ECR. A single `Dockerfile` produces multiple distributions (`community`, `enterprise`, `lims_starter`, `allpg`) via the `LABKEY_DISTRIBUTION` build arg.

## Common Commands

```bash
# Local development cycle
make build # Build image locally (uses local .jar if present)
make up # Run community via docker-compose (https://localhost:8443)
make up-enterprise # Run enterprise distribution
make up-lims_starter
make down # Tear down containers
make test # Run smoke.bash health check against running container

# Lint
# Hadolint runs in CI; run locally via:
docker run --rm -i hadolint/hadolint < Dockerfile

# AWS ECR workflow
make login # Authenticate to ECR
make tag # Tag image for ECR
make push # Push to ECR
make all # login → build → tag → push (default)
```

## Architecture

### Build Flow

`Dockerfile` downloads the LabKey `.tar.gz` from a URL (or uses a local `.jar` file placed in the repo root for development). The `LABKEY_VERSION` and `LABKEY_DISTRIBUTION` build args control which artifact is fetched. Base image is `eclipse-temurin:25-jre-noble` (Debian); Alpine variant is also supported.

### Runtime

`entrypoint.sh` is the container entry point. It:
1. Validates required `LABKEY_*` env vars (excludes `*SSM*`, `*GUID*`, `*MEK*`, initial-user vars)
2. Optionally downloads startup properties from S3
3. Handles SSM vs. non-AWS mode: if `LABKEY_SSM_PREFIX` is set, normalizes trailing slashes on both prefix vars; otherwise removes the `context.awsParameterStore.prefix` line and substitutes `ssm:` references in `application.properties` with direct env var values
4. Runs `envsubst` on all `.properties` files, then `sed` to substitute `@@placeholder@@` values
5. Generates a self-signed TLS keystore via `openssl`
6. Unsets connection/SMTP env vars, then `exec`s `java -jar labkeyServer.jar`

### Multi-Distribution

The `startup/` directory contains per-distribution `.properties` files (`community.properties`, `enterprise.properties`, etc.). The `LABKEY_DISTRIBUTION` env var selects which file is copied in at build time and passed to the JVM.

### Configuration Surface

Almost all runtime behavior is controlled via environment variables. The major groups are documented in `README.md`:
- **DB**: `POSTGRES_*` — connection, pooling
- **App**: `LABKEY_*` — version, distribution, base URL, encryption key, initial user
- **SSM (AWS, 26.6+)**: `LABKEY_SSM_PREFIX` (app-level prefix) and `LABKEY_VPC_SSM_PREFIX` (VPC-level prefix) — when set, DB credentials (`database_user`, `database_password`), encryption key (`ek`), and SMTP credentials (`smtp_user`, `smtp_password`) are fetched from SSM instead of env vars; see `application.properties` for the `ssm:` references and `README.md` for the full SSM parameter table
- **JVM**: `JAVA_*`, `MAX_JVM_RAM_PERCENT`, `JAVA_PRE_JAR_EXTRA` / `JAVA_POST_JAR_EXTRA`
- **SSL**: `CERT_*`, `TOMCAT_KEYSTORE_*`
- **Observability**: Datadog APM (`dd-java-agent.jar` baked in), `LOG_LEVEL_*`, `LOGGER_PATTERN`
- **Debug**: `DEBUG=1` installs extra tools (ping, netcat, vim, etc.) at runtime

### CI/CD (GitHub Actions)

| Workflow | Trigger |
|----------|---------|
| `hadolint.yml` | Push to `fb_*` / `*_fb_*`; PRs to develop/release* |
| `validate_pr.yml` | PR opened/ready for review |
| `merge_release.yml` | PR review approved (auto-merges release branches) |
| `dockle_xeol.yml` | Security scanning |
| `branch_release.yml` | Release branch automation |

Feature branches follow the pattern `fb_<description>` or `<version>_fb_<description>`.

### Local JAR Development

Place a `labkeyServer.jar.*` file in the repo root (already gitignored). The `Makefile` detects it and uses it as the build artifact instead of downloading from a remote URL, enabling local iteration without publishing.
33 changes: 32 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ A better description of the LabKey settings can be found in the LabKey docs [her
| LABKEY_DISTRIBUTION | "flavor" of labkey; | `community` |
| LABKEY_FILES_ROOT | path within which will serve as the root of the "files" directory | `/labkey/files` |
| LABKEY_GUID | LabKey [server GUID](https://www.labkey.org/Documentation/wiki-page.view?name=stagingServerTips#guid) | `<empty>` |
| LABKEY_EK | LabKey [encryption key](https://www.labkey.org/Documentation/wiki-page.view?name=cpasxml#encrypt) | `123abc456` |
| LABKEY_EK | LabKey [encryption key](https://www.labkey.org/Documentation/wiki-page.view?name=cpasxml#encrypt); not needed when using AWS SSM integration (see below) | `123abc456` |
| LABKEY_PORT | port to which labkey will bind within the container | `8443` |
| LABKEY_SYSTEM_DESCRIPTION | brief description of server; appears in emails | `Sirius Cybernetics` |
| LABKEY_SYSTEM_EMAIL_ADDRESS | email address system email will be sent "from" | `do_not_reply@localhost` |
Expand Down Expand Up @@ -216,6 +216,8 @@ Initial user API key creation was implemented in LabKey Server 20.11.

The `POSTGRES_*` default values are meant to match those of the [library/postgres](https://hub.docker.com/_/postgres) containers.

`POSTGRES_USER` and `POSTGRES_PASSWORD` are not needed when using AWS SSM integration (see below).

| name | purpose | default |
| ------------------- | ------------------------------------------------------------------------- | ----------- |
| POSTGRES_DB | "name" of database; compounds to URI connection string | `postgres` |
Expand All @@ -229,6 +231,8 @@ The `POSTGRES_*` default values are meant to match those of the [library/postgre

These replace values previously housed in `context.xml` (`ROOT.xml` or `labkey.xml`) governing `mail/Session` resources.

`SMTP_USER` and `SMTP_PASSWORD` are not needed when using AWS SSM integration (see below).

| name | purpose | default |
| ------------- | --------------------------- | ----------- |
| SMTP_HOST | SMTP host configuration | `localhost` |
Expand All @@ -239,6 +243,33 @@ These replace values previously housed in `context.xml` (`ROOT.xml` or `labkey.x
| SMTP_AUTH | SMTP Auth flag | `false` |
| SMTP_STARTTLS | SMTP STARTTLS flag | `<empty>` |

## AWS SSM Integration (LabKey 26.6+)

For AWS deployments on LabKey 26.6+, DB credentials, the encryption key, and SMTP credentials can be resolved directly from AWS SSM Parameter Store by the JVM at startup, rather than being injected as container env vars. This uses LabKey's `AwsParameterStoreEnvironmentPostProcessor`.

Set two path-prefix env vars and create the corresponding SSM parameters:

| name | purpose | example |
| ---------------------- | ----------------------------------------------------- | -------------------------- |
| `LABKEY_SSM_PREFIX` | App-specific SSM prefix (DB creds, encryption key) | `/myapp/myenv/` |
| `LABKEY_VPC_SSM_PREFIX`| VPC-level shared SSM prefix (SMTP credentials) | `/shared/vpc/myvpc/` |

Trailing slashes on both prefixes are normalized automatically by `entrypoint.sh`.

**Expected SSM parameters:**

| SSM path | replaces env var |
| ----------------------------------------- | ------------------- |
| `${LABKEY_SSM_PREFIX}database_user` | `POSTGRES_USER` |
| `${LABKEY_SSM_PREFIX}database_password` | `POSTGRES_PASSWORD` |
| `${LABKEY_SSM_PREFIX}ek` | `LABKEY_EK` |
| `${LABKEY_VPC_SSM_PREFIX}smtp_user` | `SMTP_USER` |
| `${LABKEY_VPC_SSM_PREFIX}smtp_password` | `SMTP_PASSWORD` |

When `LABKEY_SSM_PREFIX` is set, `POSTGRES_USER`, `POSTGRES_PASSWORD`, `LABKEY_EK`, `SMTP_USER`, and `SMTP_PASSWORD` env vars are not used. When `LABKEY_SSM_PREFIX` is unset (local / non-AWS), the container falls back to those env vars as before.

In ECS, the container task role provides credentials via IMDS — no AWS credential env vars needed. For local testing with SSM, export `AWS_REGION`, `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, and `AWS_SESSION_TOKEN` (or use aws-vault) so the JVM can reach SSM.

## SSL/Keystore/Self-signed Cert

The `CERT_*` ENVs should look familiar to anyone that has used the `openssl` command to generate a pkcs12 keystore.
Expand Down
21 changes: 15 additions & 6 deletions application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,15 @@ context.resources.jdbc.labkeyDataSource.driverClassName=org.postgresql.Driver
# context.resources.jdbc.labkeyDataSource.username=${POSTGRES_USER:-postgres}
# context.resources.jdbc.labkeyDataSource.password=${POSTGRES_PASSWORD:-}

context.resources.jdbc.labkeyDataSource.url=@@jdbcUrl@@
context.resources.jdbc.labkeyDataSource.username=@@jdbcUser@@
context.resources.jdbc.labkeyDataSource.password=@@jdbcPassword@@
# AWS SSM prefix for resolving ssm: references below. Set LABKEY_SSM_PREFIX to /${app_param_path} (e.g. /myapp/myenv/).
# Leave unset for non-AWS deployments as the post-processor is a no-op when no ssm: values are present.
context.awsParameterStore.prefix=${LABKEY_SSM_PREFIX}

context.resources.jdbc.labkeyDataSource.url=@@jdbcUrl@@
# context.resources.jdbc.labkeyDataSource.username=@@jdbcUser@@
# context.resources.jdbc.labkeyDataSource.password=@@jdbcPassword@@
context.resources.jdbc.labkeyDataSource.username=ssm:database_user
context.resources.jdbc.labkeyDataSource.password=ssm:database_password
context.resources.jdbc.labkeyDataSource.maxTotal=${POSTGRES_MAX_TOTAL_CONNECTIONS}
context.resources.jdbc.labkeyDataSource.maxIdle=${POSTGRES_MAX_IDLE_CONNECTIONS}
context.resources.jdbc.labkeyDataSource.maxWaitMillis=${POSTGRES_MAX_WAIT_MILLIS}
Expand Down Expand Up @@ -57,7 +62,8 @@ server.ssl.key-store=${LABKEY_HOME}/${TOMCAT_KEYSTORE_FILENAME}
# server.ssl.key-store-password=${TOMCAT_KEYSTORE_PASSWORD}
server.ssl.key-store-type=${TOMCAT_KEYSTORE_FORMAT}

context.encryptionKey=@@encryptionKey@@
# context.encryptionKey=@@encryptionKey@@
context.encryptionKey=ssm:ek


#
Expand All @@ -69,9 +75,12 @@ server.servlet.context-path=/_
server.error.whitelabel.enabled=false

mail.smtpHost=@@smtpHost@@
mail.smtpUser=@@smtpUser@@
# mail.smtpUser=@@smtpUser@@
# LABKEY_VPC_SSM_PREFIX is the VPC-level shared SSM path prefix (e.g. /shared/vpc/myvpc/), expanded by envsubst at startup.
mail.smtpUser=ssm:${LABKEY_VPC_SSM_PREFIX}smtp_user
mail.smtpPort=@@smtpPort@@
mail.smtpPassword=@@smtpPassword@@
# mail.smtpPassword=@@smtpPassword@@
mail.smtpPassword=ssm:${LABKEY_VPC_SSM_PREFIX}smtp_password
mail.smtpAuth=@@smtpAuth@@
mail.smtpFrom=@@smtpFrom@@
mail.smtpStartTlsEnable=@@smtpStartTlsEnable@@
Expand Down
78 changes: 61 additions & 17 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ services:

- LOGGER_PATTERN=%-80.80logger{79}

- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-a"placeholder#'password}
- POSTGRES_HOST=pg-community

- MAX_JVM_RAM_PERCENT=${MAX_JVM_RAM_PERCENT:-75.0}
Expand All @@ -48,13 +47,24 @@ services:
- SMTP_HOST=${SMTP_HOST}
- SMTP_AUTH=true
- SMTP_PORT=587
- SMTP_USER=${SMTP_USER}
- SMTP_PASSWORD=${SMTP_PASSWORD}
- SMTP_STARTTLS=true

- LABKEY_SYSTEM_EMAIL_ADDRESS=${SMTP_FROM}
# - SMTP_FROM=

- LABKEY_SSM_PREFIX=${LABKEY_SSM_PREFIX}
- LABKEY_VPC_SSM_PREFIX=${LABKEY_VPC_SSM_PREFIX}
- AWS_REGION=${AWS_REGION}
- AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID}
- AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY}
- AWS_SESSION_TOKEN=${AWS_SESSION_TOKEN}
# non-AWS: set these to supply credentials directly instead of via SSM
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-}
- POSTGRES_USER=${POSTGRES_USER:-}
- LABKEY_EK=${LABKEY_EK:-}
- SMTP_USER=${SMTP_USER:-}
- SMTP_PASSWORD=${SMTP_PASSWORD:-}

# uncomment to enable CAS against labkey.org
# - |
# LABKEY_STARTUP_BASIC_EXTRA='
Expand Down Expand Up @@ -83,7 +93,7 @@ services:
- LOG4J_CONFIG_FILE=${LOG4J_CONFIG_FILE-log4j2.xml}
- LOG4J_CONFIG_OVERRIDE=${LOG4J_CONFIG_OVERRIDE}
- JSON_OUTPUT=${JSON_OUTPUT-false}
- DD_COLLECT_APM=${DD_COLLECT_APM-false}
- DD_COLLECT_APM=${DD_COLLECT_APM-false}
- SLEEP=${SLEEP:-0}

pg-community:
Expand All @@ -99,7 +109,8 @@ services:
- "-c"
- "docker-entrypoint.sh postgres >/dev/null 2>&1"
environment:
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-a"placeholder#'password}
- POSTGRES_USER=${POSTGRES_USER:-postgres}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-localdevpassword}
healthcheck:
test: ["CMD", "pg_isready", "-U", "postgres"]
interval: 30s
Expand Down Expand Up @@ -148,7 +159,6 @@ services:

- LOGGER_PATTERN=%-80.80logger{79}

- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-a"placeholder#'password}
- POSTGRES_HOST=pg-allpg

- MAX_JVM_RAM_PERCENT=${MAX_JVM_RAM_PERCENT:-75.0}
Expand All @@ -160,13 +170,24 @@ services:
- SMTP_HOST=${SMTP_HOST}
- SMTP_AUTH=true
- SMTP_PORT=587
- SMTP_USER=${SMTP_USER}
- SMTP_PASSWORD=${SMTP_PASSWORD}
- SMTP_STARTTLS=true

- LABKEY_SYSTEM_EMAIL_ADDRESS=${SMTP_FROM}
# - SMTP_FROM=

- LABKEY_SSM_PREFIX=${LABKEY_SSM_PREFIX}
- LABKEY_VPC_SSM_PREFIX=${LABKEY_VPC_SSM_PREFIX}
- AWS_REGION=${AWS_REGION}
- AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID}
- AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY}
- AWS_SESSION_TOKEN=${AWS_SESSION_TOKEN}
# non-AWS: set these to supply credentials directly instead of via SSM
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-}
- POSTGRES_USER=${POSTGRES_USER:-}
- LABKEY_EK=${LABKEY_EK:-}
- SMTP_USER=${SMTP_USER:-}
- SMTP_PASSWORD=${SMTP_PASSWORD:-}

# uncomment to enable CAS against labkey.org
# - |
# LABKEY_STARTUP_BASIC_EXTRA='
Expand Down Expand Up @@ -211,7 +232,8 @@ services:
- "-c"
- "docker-entrypoint.sh postgres >/dev/null 2>&1"
environment:
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-a"placeholder#'password}
- POSTGRES_USER=${POSTGRES_USER:-postgres}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-localdevpassword}
healthcheck:
test: ["CMD", "pg_isready", "-U", "postgres"]
interval: 30s
Expand Down Expand Up @@ -258,7 +280,6 @@ services:

- LOGGER_PATTERN=%-80.80logger{79}

- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-a"placeholder#'password}
- POSTGRES_HOST=pg-enterprise

- MAX_JVM_RAM_PERCENT=${MAX_JVM_RAM_PERCENT:-75.0}
Expand All @@ -270,13 +291,24 @@ services:
- SMTP_HOST=${SMTP_HOST}
- SMTP_AUTH=true
- SMTP_PORT=587
- SMTP_USER=${SMTP_USER}
- SMTP_PASSWORD=${SMTP_PASSWORD}
- SMTP_STARTTLS=true

- LABKEY_SYSTEM_EMAIL_ADDRESS=${SMTP_FROM}
# - SMTP_FROM=

- LABKEY_SSM_PREFIX=${LABKEY_SSM_PREFIX}
- LABKEY_VPC_SSM_PREFIX=${LABKEY_VPC_SSM_PREFIX}
- AWS_REGION=${AWS_REGION}
- AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID}
- AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY}
- AWS_SESSION_TOKEN=${AWS_SESSION_TOKEN}
# non-AWS: set these to supply credentials directly instead of via SSM
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-}
- POSTGRES_USER=${POSTGRES_USER:-}
- LABKEY_EK=${LABKEY_EK:-}
- SMTP_USER=${SMTP_USER:-}
- SMTP_PASSWORD=${SMTP_PASSWORD:-}

# uncomment to enable CAS against labkey.org
# - |
# LABKEY_STARTUP_BASIC_EXTRA='
Expand Down Expand Up @@ -321,7 +353,8 @@ services:
- "-c"
- "docker-entrypoint.sh postgres >/dev/null 2>&1"
environment:
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-a"placeholder#'password}
- POSTGRES_USER=${POSTGRES_USER:-postgres}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-localdevpassword}
healthcheck:
test: ["CMD", "pg_isready", "-U", "postgres"]
interval: 30s
Expand Down Expand Up @@ -369,7 +402,6 @@ services:

- LOGGER_PATTERN=%-80.80logger{79}

- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-a"placeholder#'password}
- POSTGRES_HOST=pg-lims_starter

- MAX_JVM_RAM_PERCENT=${MAX_JVM_RAM_PERCENT:-75.0}
Expand All @@ -381,13 +413,24 @@ services:
- SMTP_HOST=${SMTP_HOST}
- SMTP_AUTH=true
- SMTP_PORT=587
- SMTP_USER=${SMTP_USER}
- SMTP_PASSWORD=${SMTP_PASSWORD}
- SMTP_STARTTLS=true

- LABKEY_SYSTEM_EMAIL_ADDRESS=${SMTP_FROM}
# - SMTP_FROM=

- LABKEY_SSM_PREFIX=${LABKEY_SSM_PREFIX}
- LABKEY_VPC_SSM_PREFIX=${LABKEY_VPC_SSM_PREFIX}
- AWS_REGION=${AWS_REGION}
- AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID}
- AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY}
- AWS_SESSION_TOKEN=${AWS_SESSION_TOKEN}
# non-AWS: set these to supply credentials directly instead of via SSM
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-}
- POSTGRES_USER=${POSTGRES_USER:-}
- LABKEY_EK=${LABKEY_EK:-}
- SMTP_USER=${SMTP_USER:-}
- SMTP_PASSWORD=${SMTP_PASSWORD:-}

# uncomment to enable CAS against labkey.org
# - |
# LABKEY_STARTUP_BASIC_EXTRA='
Expand Down Expand Up @@ -435,7 +478,8 @@ services:
- "-c"
- "docker-entrypoint.sh postgres >/dev/null 2>&1"
environment:
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-a"placeholder#'password}
- POSTGRES_USER=${POSTGRES_USER:-postgres}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-localdevpassword}
healthcheck:
test: ["CMD", "pg_isready", "-U", "postgres"]
interval: 30s
Expand Down
Loading
Loading