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
79 changes: 23 additions & 56 deletions .github/workflows/test-readonly-filesystem.yml
Original file line number Diff line number Diff line change
@@ -1,74 +1,41 @@
name: Test Read-Only Filesystem Support
name: Test Read-Only Filesystem and Non-Root User Support

# This workflow tests Lucee Docker images with read-only root filesystem enabled
# for security compliance (Kubernetes security best practices)
# Manual-only workflow that pulls a published Lucee image from Docker Hub and
# runs tests/test-readonly-filesystem.sh against it to verify the non-root user
# and read-only root filesystem features (Lucee 6.2+). Does NOT run automatically
# on push or pull request.
#
# For testing local (unpublished) image changes during development, run
# tests/test-readonly-filesystem.sh directly against a locally-built image instead.

on:
# Allows manual triggering from Actions tab
workflow_dispatch:
inputs:
LUCEE_VERSION:
description: 'Lucee version to test (e.g., 7.0.0.395)'
required: false
default: '7.0.0.395'
type: string
LUCEE_MINOR:
description: 'Lucee minor version (e.g., 7.0)'
required: false
default: '7.0'
type: string
# Can be triggered by other workflows
workflow_call:
inputs:
LUCEE_VERSION:
IMAGE_TAG:
description: 'Lucee image tag to test (e.g. lucee/lucee:7.0.3.43 or lucee/lucee:7.0-nginx)'
required: true
type: string
LUCEE_MINOR:
VARIANT:
description: 'Image variant (must match the IMAGE_TAG)'
required: true
type: string
type: choice
options:
- tomcat
- nginx
default: tomcat

jobs:
test-readonly-filesystem:
smoke:
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.9.0

- name: Placeholder - Build test image
run: |
echo "TODO: Build Docker image without prewarm"
echo "LUCEE_VERSION: ${{ inputs.LUCEE_VERSION || '7.0.0.395' }}"
echo "LUCEE_MINOR: ${{ inputs.LUCEE_MINOR || '7.0' }}"
echo ""
echo "This will:"
echo " 1. Build image with USER directive (non-root)"
echo " 2. Skip prewarm step"
echo " 3. Test with --read-only flag"
echo " 4. Verify volume mounts work"
echo " 5. Check Lucee starts and responds"

- name: Placeholder - Test read-only filesystem
run: |
echo "TODO: Test with read-only root filesystem"
echo ""
echo "Tests will include:"
echo " - Verify container runs as non-root (uid=1000)"
echo " - Verify root filesystem is read-only"
echo " - Verify writable volumes work"
echo " - Verify Lucee initializes correctly"
echo " - Verify HTTP requests work"
echo " - Measure startup time"
- name: Pull image
run: docker pull ${{ inputs.IMAGE_TAG }}

- name: Placeholder - Security validation
- name: Run smoke test
run: |
echo "TODO: Run security checks"
echo ""
echo "Security checks will include:"
echo " - Verify no writes to root filesystem"
echo " - Verify running as non-root user"
echo " - Verify no new privileges"
echo " - Check for vulnerable permissions"
chmod +x tests/test-readonly-filesystem.sh
tests/test-readonly-filesystem.sh ${{ inputs.IMAGE_TAG }} ${{ inputs.VARIANT }}
29 changes: 29 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ RUN rm -rf /usr/local/tomcat/webapps/*
# -Xmx<size> set maximum Java heap size
ENV LUCEE_JAVA_OPTS "-Xms64m -Xmx512m"

# Replace web.xml init-params with env vars (honored by Lucee 6.2+)
ENV LUCEE_SERVER_DIR=/opt/lucee/server
ENV LUCEE_WEB_DIR=/opt/lucee/web

# Download Lucee JAR
RUN mkdir -p /usr/local/tomcat/lucee
ADD ${LUCEE_JAR_URL} /usr/local/tomcat/lucee/lucee.jar
Expand Down Expand Up @@ -83,7 +87,32 @@ RUN mkdir -p /var/www
COPY www/ /var/www/
ONBUILD RUN rm -rf /var/www/*

# Non-root user and read-only rootfs support (Lucee 6.2+ / Tomcat 11.x+ only)
RUN MAJOR_VERSION=$(echo ${TOMCAT_VERSION} | awk -F. '{print $1}') && \
if [ "$MAJOR_VERSION" -ge 11 ]; then \
groupadd -r -g 999 lucee && \
useradd -r -u 999 -g lucee -s /bin/false -M lucee && \
mkdir -p /opt/lucee/server-runtime && \
chown -R lucee:lucee \
/opt/lucee \
/usr/local/tomcat/logs \
/usr/local/tomcat/temp \
/usr/local/tomcat/work \
/var/www && \
chmod 644 /usr/local/tomcat/lucee/lucee.jar; \
fi

# Declare VOLUMEs so writable paths work under --read-only without --tmpfs
VOLUME ["/usr/local/tomcat/logs", "/usr/local/tomcat/temp", "/usr/local/tomcat/work", "/opt/lucee/server-runtime", "/tmp"]

# Lucee first time startup; explodes lucee and installs bundles/extensions
COPY supporting/prewarm.sh /usr/local/tomcat/bin/
RUN chmod +x /usr/local/tomcat/bin/prewarm.sh
RUN /usr/local/tomcat/bin/prewarm.sh ${LUCEE_MINOR}

# Entrypoint handles LUCEE_RUNTIME_DIR seeding for read-only rootfs support
COPY supporting/docker-entrypoint.sh /usr/local/tomcat/bin/
RUN chmod +x /usr/local/tomcat/bin/docker-entrypoint.sh
ENTRYPOINT ["/usr/local/tomcat/bin/docker-entrypoint.sh"]
# Setting ENTRYPOINT above clears the Tomcat base image's CMD; restore it
CMD ["catalina.sh", "run"]
20 changes: 20 additions & 0 deletions Dockerfile.nginx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,23 @@ RUN set -x && \
apt-get clean && \
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*

# Chown nginx writable dirs to lucee user (Lucee 6.2+ base images only)
RUN if id lucee >/dev/null 2>&1; then \
chown -R lucee:lucee \
/var/lib/nginx \
/var/log/nginx \
/var/log/supervisor \
/run; \
fi

# Allow non-root nginx to bind privileged ports (Lucee 6.2+ base images only)
RUN if id lucee >/dev/null 2>&1; then \
apt-get update && \
apt-get install -y --no-install-recommends libcap2-bin && \
setcap 'cap_net_bind_service=+ep' /usr/sbin/nginx && \
rm -rf /var/lib/apt/lists/*; \
fi

# Copy default nginx config files
COPY nginx/nginx.conf /etc/nginx/
COPY nginx/default.conf /etc/nginx/conf.d/
Expand All @@ -26,6 +43,9 @@ RUN mkdir -p /var/www
COPY www/ /var/www/
ONBUILD RUN rm -rf /var/www/*

# Declare nginx-writable VOLUMEs for --read-only support
VOLUME ["/var/lib/nginx", "/var/log/nginx", "/var/log/supervisor", "/run"]

# Expose HTTP and HTTPS ports
EXPOSE 80 443

Expand Down
56 changes: 49 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,17 @@

## Supported tags and respective Dockerfile links

### Latest Stable Release - 6.2.0.321 - Tomcat 10.1 with Java 21 (recommended)
### Latest Stable Release - 7.0.3.43 - Tomcat 11.0 with Java 21 (recommended)

- lucee/lucee:6.2.0.321-nginx-tomcat10.1-jre21-temurin-jammy
- lucee/lucee:6.2.0.321-tomcat10.1-jre21-temurin-jammy
- lucee/lucee:6.2.0.321
- lucee/lucee:6.2.0.321-light-tomcat10.1-jre21-temurin-jammy
- lucee/lucee:6.2.0.321-light
- `7.0.3.43-tomcat11.0-jre21-temurin-noble`, `7.0.3.43`, **`7.0`** ([Dockerfile](https://github.com/lucee/lucee-dockerfiles/blob/master/Dockerfile))
- `7.0.3.43-nginx-tomcat11.0-jre21-temurin-noble`, `7.0.3.43-nginx`, **`7.0-nginx`** ([Dockerfile.nginx](https://github.com/lucee/lucee-dockerfiles/blob/master/Dockerfile.nginx))

Tomcat 10.1 uses the Jakarta namespace, Tomcat 9 used the javax namespace, so you might need to update some libraries like urlrewrite
### Long-Term Support (LTS) Release - 6.2.6.19 - Tomcat 11.0 with Java 21

- `6.2.6.19-tomcat11.0-jre21-temurin-noble`, `6.2.6.19`, **`6.2`** ([Dockerfile](https://github.com/lucee/lucee-dockerfiles/blob/master/Dockerfile))
- `6.2.6.19-nginx-tomcat11.0-jre21-temurin-noble`, `6.2.6.19-nginx`, **`6.2-nginx`** ([Dockerfile.nginx](https://github.com/lucee/lucee-dockerfiles/blob/master/Dockerfile.nginx))

Tomcat 11.0 and 10.1 use the Jakarta namespace, Tomcat 9 used the javax namespace, so you might need to update some libraries like urlrewrite.

### Previous stable release (LTS)

Expand Down Expand Up @@ -117,6 +119,45 @@ The default configuration serves a single application for any hostname on the li

Lucee 6 by default runs in single mode (only one configuration and Administrator), if you prefer to run it in multi mode you need to to set the flag "mode" to "multi" in the of the .CFConfig.json file.

### Non-Root User and Read-Only Root Filesystem (Lucee 6.2+)

Lucee 6.2 and later images include opt-in support for running as a non-root user and with a read-only root filesystem. Earlier Lucee minors (5.x, 6.0, 6.1) are unchanged and continue to run as root with a writable filesystem.

**To enable the non-root user**, opt in using any one of:

- `USER lucee` in your downstream Dockerfile
- `--user lucee` (Docker CLI)
- `user: "lucee"` (docker-compose)
- `securityContext.runAsUser: 999` (Kubernetes)
- `"user": "lucee"` (AWS ECS task definition)

The image creates a `lucee` user with uid 999.

**To enable a read-only root filesystem**, set `LUCEE_RUNTIME_DIR=/opt/lucee/server-runtime` so the entrypoint can seed a writable copy of the Lucee server context at container start. Without this, Lucee will fail to perform any operation that needs to write to its server context. Then opt in using any one of:

- `--read-only` (Docker CLI)
- `read_only: true` (docker-compose)
- `securityContext.readOnlyRootFilesystem: true` (Kubernetes)
- `"readonlyRootFilesystem": true` (AWS ECS task definition)

The image declares writable runtime paths (`/usr/local/tomcat/{logs,temp,work}`, `/opt/lucee/server-runtime`, `/tmp`, plus nginx-specific paths in the `-nginx` images) as anonymous volumes so `docker run --read-only` works out of the box. **Note that some orchestrators (notably Kubernetes and AWS ECS on Fargate) require writable mounts to be declared explicitly in your deployment manifest** (e.g. `emptyDir` for Kubernetes, or `volumes` + `mountPoints` entries in an ECS task definition; ephemeral storage is fine).

On dev or CI machines, anonymous volumes outlive the container that created them and can accumulate over time — run containers with `--rm` to clean up on exit, run `docker volume prune` periodically, or override the paths with named volumes in your compose file (and use `docker compose down -v` to remove them). For production deployments, named volumes or bind mounts are preferred over anonymous volumes for paths you want to persist across container restarts, such as log directories.

Example with docker-compose:

```yaml
services:
lucee:
image: lucee/lucee:7.0
user: "lucee"
read_only: true
environment:
- LUCEE_RUNTIME_DIR=/opt/lucee/server-runtime
ports:
- "8888:8888"
```

## Using this image

### Accessing the service
Expand Down Expand Up @@ -160,6 +201,7 @@ Following some helpful Environment variables you can use with the Lucee docker i
- `LUCEE_ADMIN_PASSWORD`: The password for the Lucee Administrator
- `LUCEE_VERSION`: If set Lucee will run this version independent of the version installed.
- `LUCEE_JAVA_OPTS`: Additional JVM parameters for Tomcat. Used by /usr/local/tomcat/bin/setenv.sh. Default: "-Xms64m -Xmx512m".
- `LUCEE_RUNTIME_DIR`: When set, the entrypoint seeds a writable copy of the Lucee server context here at container start, and re-points Lucee to use it. Intended for use with a read-only root filesystem. (Lucee 6.2+)

For all possible enviroment variables supported by Lucee, see [here](https://github.com/lucee/lucee-docs/blob/master/docs/recipes/environment-variables-system-properties.md).

Expand Down
2 changes: 2 additions & 0 deletions config/tomcat/11.0/web.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4747,6 +4747,7 @@
<servlet-name>CFMLServlet</servlet-name>
<description>CFML runtime Engine</description>
<servlet-class>lucee.loader.servlet.jakarta.CFMLServlet</servlet-class>
<!-- paths come from the LUCEE_SERVER_DIR and LUCEE_WEB_DIR env vars (set in Dockerfile)
<init-param>
<param-name>lucee-server-directory</param-name>
<param-value>/opt/lucee/server/</param-value>
Expand All @@ -4757,6 +4758,7 @@
<param-value>/opt/lucee/web/</param-value>
<description>Lucee Web Directory (for Website-specific configurations, settings, and libraries)</description>
</init-param>
-->
<load-on-startup>1</load-on-startup>
</servlet>

Expand Down
20 changes: 20 additions & 0 deletions supporting/docker-entrypoint.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#!/bin/sh
set -e

LUCEE_SERVER_DIR="${LUCEE_SERVER_DIR:-/opt/lucee/server}"
LUCEE_RUNTIME_DIR="${LUCEE_RUNTIME_DIR:-}"

if [ -n "$LUCEE_RUNTIME_DIR" ] && [ "$LUCEE_RUNTIME_DIR" != "$LUCEE_SERVER_DIR" ]; then
START=$(date +%s%N)
mkdir -p "$LUCEE_RUNTIME_DIR/lucee-server"
cp -a "$LUCEE_SERVER_DIR/lucee-server/." "$LUCEE_RUNTIME_DIR/lucee-server/"
rm -rf "$LUCEE_RUNTIME_DIR/lucee-server/felix-cache"
END=$(date +%s%N)
# Write to stderr (unbuffered) rather than stdout (block-buffered when
# connected to a pipe) so the message survives the subsequent `exec`
# that replaces this shell with Tomcat.
echo "Seeded LUCEE_RUNTIME_DIR ($LUCEE_RUNTIME_DIR) from LUCEE_SERVER_DIR ($LUCEE_SERVER_DIR) in $(( (END - START) / 1000000 ))ms" >&2
export LUCEE_SERVER_DIR="$LUCEE_RUNTIME_DIR"
fi

exec "$@"
10 changes: 10 additions & 0 deletions supporting/prewarm.sh
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,13 @@ else
/usr/local/tomcat/bin/catalina.sh run

fi

# ensure lucee user can read/write all Lucee and Tomcat files at runtime
if id lucee >/dev/null 2>&1; then
chown -R lucee:lucee \
/opt/lucee \
/usr/local/tomcat/logs \
/usr/local/tomcat/temp \
/usr/local/tomcat/work \
/var/www
fi
Loading
Loading