From 79843a97d945048e716d3795a5d179c963fe4784 Mon Sep 17 00:00:00 2001 From: "Manuel S." Date: Sat, 14 Feb 2026 17:12:33 +0100 Subject: [PATCH 1/8] fix: pin httpd:2.4.66, add cleanup/healthcheck/labels --- Dockerfile | 39 +++++++++++++++++++++++++++------------ 1 file changed, 27 insertions(+), 12 deletions(-) diff --git a/Dockerfile b/Dockerfile index 875ad39..f2b4d18 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,19 +1,34 @@ -FROM httpd -ENV PUID=${PUID:-1000} -ENV PGID=${PGID:-1000} +FROM httpd:2.4.66 +ENV PUID=${PUID:-1000} \ + PGID=${PGID:-1000} + +# Copy init.sh and set permissions COPY init.sh /init.sh +RUN chmod +x /init.sh + +# Install deps + cleanup (reduced CVEs/Bloat) RUN apt-get update && \ -apt-get -y upgrade && \ -apt-get -y install curl git jq && \ -chmod +x /init.sh + apt-get -y upgrade && \ + apt-get -y install --no-install-recommends curl git jq && \ + rm -rf /var/lib/apt/lists/* + RUN echo "\n"\ -" SetHandler server-status\n"\ -" Order deny,allow\n"\ -" Allow from all\n"\ -"\n"\ ->> /usr/local/apache2/conf/httpd.conf + " SetHandler server-status\n"\ + " Order deny,allow\n"\ + " Allow from all\n"\ + "\n"\ + >> /usr/local/apache2/conf/httpd.conf WORKDIR /usr/local/apache2/htdocs/ RUN chown -R $PUID:$PGID /usr/local/apache2/htdocs -CMD ["/bin/bash","/init.sh"] \ No newline at end of file + +# Labels for registry +LABEL org.opencontainers.image.source="https://github.com/Sakujakira/5etools-docker" \ + org.opencontainers.image.description="5eTools Docker Container" + +# Healthcheck +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD curl -f http://localhost/ || exit 1 + +CMD ["/bin/bash", "/init.sh"] \ No newline at end of file From c9473eba82db9e7390818c21e86dcbe86d81ce36 Mon Sep 17 00:00:00 2001 From: "Manuel S." Date: Sat, 14 Feb 2026 17:53:39 +0100 Subject: [PATCH 2/8] chore: update workflow to hybrid modern CI/CD --- .github/workflows/ci_cd.yml | 108 +++++++++++++++++++----------------- 1 file changed, 58 insertions(+), 50 deletions(-) diff --git a/.github/workflows/ci_cd.yml b/.github/workflows/ci_cd.yml index 01849d3..c47151c 100644 --- a/.github/workflows/ci_cd.yml +++ b/.github/workflows/ci_cd.yml @@ -1,68 +1,76 @@ -name: CI/CD +name: Build & Push Docker (CI/CD) on: - # Triggers the workflow when changes are made to the image on the main branch push: branches: [ main ] - paths-ignore: # don't run this workflow if no changes have been made to the docker image + paths-ignore: - '.github/**' - 'docker-compose.yml' - 'README.md' pull_request: branches: [ main ] - paths-ignore: # don't run this workflow if no changes have been made to the docker image + paths-ignore: - '.github/**' - 'docker-compose.yml' - 'README.md' - # Allows you to run this workflow manually from the Actions tab workflow_dispatch: jobs: - ci: - name: Build and push Docker image to Docker Hub + build-push: runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: - - name: Check out the repo - uses: actions/checkout@v2 - - - name: Log into Docker Hub - uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_PASSWORD }} - - - name: Extract metadata (tags, labels) for Docker - id: meta - uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38 - with: - images: jafner/5etools-docker - tags: | - latest - type=edge - type=sha - - - name: Set up QEMU - uses: docker/setup-qemu-action@master - with: - platforms: all + - name: Checkout + uses: actions/checkout@v4 + + - name: Docker Hub Meta + id: meta + uses: docker/metadata-action@v5 # Neueste + with: + images: | + ghcr.io/${{ github.repository }} + docker.io/${{ secrets.DOCKERHUB_USERNAME }}/5etools-docker + tags: | + type=raw,value=latest,enable={{is_default_branch}} + type=raw,value=main,enable={{is_default_branch}} + type=sha + labels: | + org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }} + + - name: Login GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Login Docker Hub + uses: docker/login-action@v3 + with: + registry: docker.io + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Build & Push + uses: docker/build-push-action@v6 + with: + context: . + push: ${{ github.event_name != 'pull_request' }} + platforms: linux/amd64,linux/arm64,linux/arm/v7 + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max - - name: Set up Docker Buildx - id: buildx - uses: docker/setup-buildx-action@master - - - name: Build and push the Docker image - uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc - with: - context: . - builder: ${{ steps.buildx.outputs.name }} - platforms: linux/amd64,linux/arm64,linux/arm/v7 - push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - - - name: Docker Hub Description - uses: peter-evans/dockerhub-description@v2 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_PASSWORD }} - repository: jafner/5etools-docker + - name: Docker Hub Description + if: github.event_name != 'pull_request' + uses: peter-evans/dockerhub-description@v4 # Neueste + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + repository: 5etools-docker + description: | + 5eTools Docker Container - Ein benutzerfreundlicher und vielseitiger Container für die 5eTools-Webanwendung, optimiert für einfache Bereitstellung und Wartung. \ No newline at end of file From fb01c9f581b40b5f656a48e222f1fa8893895bc9 Mon Sep 17 00:00:00 2001 From: "Manuel S." Date: Sat, 14 Feb 2026 18:02:00 +0100 Subject: [PATCH 3/8] fix: chmod +x init.sh via COPY --chmod=755 (resolves build fail) --- Dockerfile | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index f2b4d18..540af55 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,8 +3,7 @@ ENV PUID=${PUID:-1000} \ PGID=${PGID:-1000} # Copy init.sh and set permissions -COPY init.sh /init.sh -RUN chmod +x /init.sh +COPY --chmod=755 init.sh /init.sh # Install deps + cleanup (reduced CVEs/Bloat) RUN apt-get update && \ From b95714640c8c45910150c0290b79452ff1a75442 Mon Sep 17 00:00:00 2001 From: "Manuel S." Date: Sat, 14 Feb 2026 18:10:00 +0100 Subject: [PATCH 4/8] fix: unix LF + #!/bin/sh for multi-arch (ARM QEMU) --- init.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) mode change 100644 => 100755 init.sh diff --git a/init.sh b/init.sh old mode 100644 new mode 100755 index ac50e42..a10ca21 --- a/init.sh +++ b/init.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/bin/sh # Print current user ID id @@ -66,4 +66,4 @@ fi echo " === Starting version $VERSION" -httpd-foreground \ No newline at end of file +httpd-foreground From b3e6ff5d498e2f86c1ebd75804b5e6a85332b8fe Mon Sep 17 00:00:00 2001 From: "Manuel S." Date: Sat, 14 Feb 2026 18:35:20 +0100 Subject: [PATCH 5/8] fix: dropping armv7 support --- .github/workflows/ci_cd.yml | 2 +- init.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci_cd.yml b/.github/workflows/ci_cd.yml index c47151c..9e8a2bf 100644 --- a/.github/workflows/ci_cd.yml +++ b/.github/workflows/ci_cd.yml @@ -59,7 +59,7 @@ jobs: with: context: . push: ${{ github.event_name != 'pull_request' }} - platforms: linux/amd64,linux/arm64,linux/arm/v7 + platforms: linux/amd64,linux/arm64 tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha diff --git a/init.sh b/init.sh index a10ca21..d31416b 100755 --- a/init.sh +++ b/init.sh @@ -1,4 +1,4 @@ -#!/bin/sh +#!/bin/bash # Print current user ID id From 78feb2175a8ba8e56c40b408568a862a6cb362ce Mon Sep 17 00:00:00 2001 From: "Manuel S." Date: Sat, 14 Feb 2026 18:41:52 +0100 Subject: [PATCH 6/8] fix: ARG after FROM + htdocs clean + amd64-only (arm64 QEMU skip) --- .github/workflows/ci_cd.yml | 2 +- Dockerfile | 17 +++++++++++++---- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci_cd.yml b/.github/workflows/ci_cd.yml index 9e8a2bf..61165c2 100644 --- a/.github/workflows/ci_cd.yml +++ b/.github/workflows/ci_cd.yml @@ -59,7 +59,7 @@ jobs: with: context: . push: ${{ github.event_name != 'pull_request' }} - platforms: linux/amd64,linux/arm64 + platforms: linux/amd64 tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha diff --git a/Dockerfile b/Dockerfile index 540af55..10c006f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,13 +1,17 @@ FROM httpd:2.4.66 -ENV PUID=${PUID:-1000} \ - PGID=${PGID:-1000} + +# ARG BEFORE FROM! +ARG PUID=1000 +ARG PGID=1000 +ENV PUID=$PUID \ + PGID=$PGID # Copy init.sh and set permissions COPY --chmod=755 init.sh /init.sh # Install deps + cleanup (reduced CVEs/Bloat) RUN apt-get update && \ - apt-get -y upgrade && \ + apt-get -y upgrade -qq && \ apt-get -y install --no-install-recommends curl git jq && \ rm -rf /var/lib/apt/lists/* @@ -19,8 +23,12 @@ RUN echo "\n"\ "\n"\ >> /usr/local/apache2/conf/httpd.conf +# htdocs clean + chown WORKDIR /usr/local/apache2/htdocs/ -RUN chown -R $PUID:$PGID /usr/local/apache2/htdocs +RUN rm -rf * .??* && \ + groupadd -g ${PGID} appgroup && \ + useradd -u ${PUID} -g appgroup -s /bin/bash appuser && \ + chown -R appuser:appgroup . # Labels for registry LABEL org.opencontainers.image.source="https://github.com/Sakujakira/5etools-docker" \ @@ -30,4 +38,5 @@ LABEL org.opencontainers.image.source="https://github.com/Sakujakira/5etools-doc HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ CMD curl -f http://localhost/ || exit 1 +USER appuser CMD ["/bin/bash", "/init.sh"] \ No newline at end of file From 2707f1be827532b1f120dc388f167f2a975ab448 Mon Sep 17 00:00:00 2001 From: "Manuel S." Date: Sat, 14 Feb 2026 18:50:32 +0100 Subject: [PATCH 7/8] fix: remove Docker Hub Description for now --- .github/workflows/ci_cd.yml | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/.github/workflows/ci_cd.yml b/.github/workflows/ci_cd.yml index 61165c2..fbc352a 100644 --- a/.github/workflows/ci_cd.yml +++ b/.github/workflows/ci_cd.yml @@ -65,12 +65,4 @@ jobs: cache-from: type=gha cache-to: type=gha,mode=max - - name: Docker Hub Description - if: github.event_name != 'pull_request' - uses: peter-evans/dockerhub-description@v4 # Neueste - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - repository: 5etools-docker - description: | - 5eTools Docker Container - Ein benutzerfreundlicher und vielseitiger Container für die 5eTools-Webanwendung, optimiert für einfache Bereitstellung und Wartung. \ No newline at end of file + \ No newline at end of file From f589810f395b8ffe785c96c2e551bd26a5289656 Mon Sep 17 00:00:00 2001 From: "Manuel S." Date: Sat, 14 Feb 2026 22:25:19 +0100 Subject: [PATCH 8/8] feat: migrate to Alpine Linux with enhanced security and comprehensive fixes Major improvements: - Migrate base image from Debian to Alpine Linux 3.20 (3.6x smaller, 68% fewer CVEs) - Implement Apache-native privilege dropping (workers run as PUID:PGID non-root) - Fix file ownership timing (chown after all git operations) - Fix Apache config syntax (printf with proper newlines) - Improve git operations (git reset --hard + pull, idempotent submodule handling) - Add multi-arch support (linux/amd64, linux/arm64) with QEMU/buildx setup Security enhancements: - Apache worker processes run as non-root (PUID/PGID) - Proper file permissions for htdocs and logs directories - Minimal package installation (git, jq, su-exec only) - Add .dockerignore to prevent sensitive files in build context Repository updates: - Update from 5etools-mirror-2 to mirror-3 repositories - Update image references to ghcr.io/sakujakira/5etools-docker Documentation: - Add CLAUDE.md with comprehensive implementation details and verification checklist - Add README comparison section with Docker Scout CVE data - Add security section documenting privilege separation - Add AI-assisted development disclaimer - Update PUID/PGID documentation with runtime behavior Testing: - All changes verified with local builds and runtime testing - Apache config syntax validated - File ownership confirmed for custom PUID/PGID - Healthcheck verified Co-Authored-By: Claude Sonnet 4.5 --- .dockerignore | 6 + .github/workflows/ci_cd.yml | 8 +- .gitignore | 5 + CLAUDE.md | 216 ++++++++++++++++++++++++++++++++++++ Dockerfile | 40 +++---- README.md | 99 +++++++++++++++-- docker-compose.yml | 4 +- init.sh | 93 ++++++++++------ 8 files changed, 408 insertions(+), 63 deletions(-) create mode 100644 .dockerignore create mode 100644 .gitignore create mode 100644 CLAUDE.md diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..3606893 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,6 @@ +.git +.github +.claude +docker-compose.yml +.gitignore +.gitattributes diff --git a/.github/workflows/ci_cd.yml b/.github/workflows/ci_cd.yml index fbc352a..0a65a59 100644 --- a/.github/workflows/ci_cd.yml +++ b/.github/workflows/ci_cd.yml @@ -53,16 +53,20 @@ jobs: registry: docker.io username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 - name: Build & Push uses: docker/build-push-action@v6 with: context: . push: ${{ github.event_name != 'pull_request' }} - platforms: linux/amd64 + platforms: linux/amd64, linux/arm64 tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha cache-to: type=gha,mode=max - \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b93af97 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +# Claude Code files +.claude/ + +# Keep CLAUDE.md tracked (not ignored) +!CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..96238c5 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,216 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +This is a Docker containerization of 5etools (D&D 5th edition tools) based on Alpine Linux with Apache httpd. The container automatically clones/updates the 5etools repository from GitHub and serves it via Apache. + +## Architecture + +**Container Startup Flow:** +1. `init.sh` is the entry point (CMD in Dockerfile), runs as root +2. Creates dynamic user/group (appuser/appgroup) based on PUID/PGID environment variables +3. Either runs in OFFLINE_MODE (uses existing files) or clones/updates from GitHub +4. Optionally adds images as git submodule if IMG=TRUE +5. Completes all git operations as root (required for permissions) +6. Sets ownership of htdocs and logs directories to PUID:PGID (after all git operations) +7. Modifies Apache httpd.conf to set User/Group directives to PUID/PGID +8. Starts `httpd-foreground` as root - Apache master runs as root, workers drop to PUID:PGID + +**Key Paths:** +- `/usr/local/apache2/htdocs/` - Web root, mapped to host volume for persistence +- `/init.sh` - Container entry point script + +**Base Image:** `httpd:alpine3.20` (Alpine-based Apache) + +**Source Repositories:** +- Main content: `https://github.com/5etools-mirror-3/5etools-src.git` +- Images (optional): `https://github.com/5etools-mirror-3/5etools-img.git` +- Note: Project uses mirror-3 repositories (updated from mirror-2) + +## Common Commands + +### Local Development & Testing + +```bash +# Quick start with default configuration +mkdir -p ~/5etools-docker/htdocs && cd ~/5etools-docker +docker-compose up -d && docker logs -f 5etools-docker + +# Stop and remove container (files persist in htdocs) +docker-compose down + +# Build image locally +docker build -t 5etools-docker . + +# Build with custom PUID/PGID +docker build --build-arg PUID=1001 --build-arg PGID=1001 -t 5etools-docker . + +# Test with images enabled +docker run -d -p 8080:80 -e IMG=TRUE -v ~/5etools-docker/htdocs:/usr/local/apache2/htdocs 5etools-docker + +# Test offline mode +docker run -d -p 8080:80 -e OFFLINE_MODE=TRUE -v ~/5etools-docker/htdocs:/usr/local/apache2/htdocs 5etools-docker + +# Monitor container startup (useful for debugging) +docker logs -f 5etools-docker +``` + +### Testing & Verification + +```bash +# Verify Apache configuration syntax +docker run --rm httpd -t +# Should output: "Syntax OK" + +# Check server-status configuration formatting +docker run --rm grep -A 5 "server-status" /usr/local/apache2/conf/httpd.conf +# Should show properly formatted Location block with real newlines + +# Test container with custom PUID/PGID +docker run -d --name test -p 8080:80 -e PUID=1001 -e PGID=1001 \ + -v ~/5etools-docker/htdocs:/usr/local/apache2/htdocs + +# Verify file ownership (should show UID:GID matching PUID:PGID) +docker exec test ls -ln /usr/local/apache2/htdocs | head -10 + +# Verify Apache worker processes run as non-root +docker exec test ps aux | grep httpd +# Master: root, Workers: appuser (UID matching PUID) + +# Check HTTP response +curl -s http://localhost:8080 | head -20 +# Should return valid 5etools HTML + +# Verify healthcheck +docker inspect test --format='{{.State.Health.Status}}' +# Should show: healthy + +# Cleanup +docker stop test && docker rm test +``` + +### CI/CD Pipeline + +The GitHub Actions workflow (`.github/workflows/ci_cd.yml`) automatically: +- Builds multi-arch images (linux/amd64, linux/arm64) +- Pushes to GHCR (`ghcr.io/`) and Docker Hub (`docker.io//5etools-docker`) +- Triggers on push to main branch (excluding changes to .github/**, docker-compose.yml, README.md) +- Can be manually triggered via workflow_dispatch +- **Note:** Multi-arch builds require QEMU and buildx setup steps (verify these are present in workflow) + +**Required GitHub Secrets:** +- `DOCKERHUB_USERNAME` - Docker Hub username +- `DOCKERHUB_TOKEN` - Docker Hub access token +- `GITHUB_TOKEN` - Automatically provided by GitHub Actions + +## Environment Variables + +- `PUID` / `PGID` (default: 1000) - User/group ID for file ownership +- `DL_LINK` (default: https://github.com/5etools-mirror-3/5etools-src.git) - Source repository +- `IMG_LINK` (default: https://github.com/5etools-mirror-3/5etools-img.git) - Image repository +- `IMG` (TRUE/FALSE) - Whether to pull image files as git submodule +- `OFFLINE_MODE` (TRUE to enable) - Skip GitHub updates, use existing files + +## Critical Implementation Details + +**init.sh script:** +- Uses `set -e` to exit on any error +- Creates user/group dynamically (idempotent with `2>/dev/null || true` to handle container restarts) +- Uses `jq` to extract version from package.json (with full paths and error handling) +- Configures git with safe.directory to handle mounted volumes +- Uses shallow clone (`--depth=1`) for faster updates +- Git operations: `git reset --hard origin/HEAD` + `git pull origin main --depth=1` (no bare `git checkout`) +- Handles git submodule for images (IMG=TRUE) - checks if `./img/.git` exists before adding +- **Critical:** Sets ownership of htdocs and logs directories to PUID:PGID AFTER all git operations complete +- Modifies Apache httpd.conf User/Group directives via `sed` to match PUID/PGID +- Starts `httpd-foreground` (Apache master as root, workers as PUID:PGID - Apache's native privilege dropping) +- Always runs `httpd-foreground` to keep container alive + +**Dockerfile:** +- Cleans htdocs directory with safe pattern: `rm -rf * .[!.]* ..?*` (excludes . and ..) +- Uses `COPY --chmod=755` for init.sh to ensure executability +- Installs minimal dependencies: git, jq, su-exec (shadow package not needed - Alpine uses busybox adduser/addgroup) +- Uses `printf` (not `echo`) to add `/server-status` endpoint to httpd.conf with proper newlines +- `/server-status` endpoint exposed to all IPs (security consideration: information disclosure risk) +- Healthcheck endpoint: `http://localhost/` (checks main page returns 200) +- Container runs as root, but Apache worker processes run as PUID:PGID (Apache's native User/Group directives) + +## Implementation Verification Checklist + +**All critical items have been implemented and tested:** + +1. ✅ **User/group creation idempotency** - Error suppression handles container restarts: + ```sh + addgroup -g "$PGID" appgroup 2>/dev/null || true + adduser -D -u "$PUID" appuser -G appgroup 2>/dev/null || true + ``` + +2. ✅ **File ownership timing** - `chown` executes AFTER all git operations (critical for proper ownership): + ```sh + # All git operations first (clone, reset, pull) + # ...then chown at the end: + chown -R "$PUID":"$PGID" /usr/local/apache2/htdocs + chown -R "$PUID":"$PGID" /usr/local/apache2/logs + ``` + +3. ✅ **Git submodule handling** - Checks for existing submodule before adding: + ```sh + if [ ! -d "./img/.git" ]; then + git submodule add --depth=1 -f "$IMG_LINK" /usr/local/apache2/htdocs/img + else + git submodule update --remote --depth=1 + fi + ``` + +4. ✅ **Git operations** - Uses `git reset --hard origin/HEAD` + `git pull origin main --depth=1` + +5. ✅ **Path handling in jq** - Uses full paths: `/usr/local/apache2/htdocs/package.json` + +6. ✅ **Apache config syntax** - Uses `printf` with proper newline handling (not `echo`) + +7. ✅ **Apache privilege dropping** - Uses Apache's native User/Group directives: + ```sh + sed -i "s/^User .*/User #$PUID/" /usr/local/apache2/conf/httpd.conf + sed -i "s/^Group .*/Group #$PGID/" /usr/local/apache2/conf/httpd.conf + ``` + +8. ✅ **CI/CD multi-arch** - QEMU and buildx setup steps present in workflow + +9. ✅ **docker-compose.yml** - Points to GHCR: `ghcr.io/sakujakira/5etools-docker:latest` + +10. ✅ **.dockerignore** - Excludes .git, .github, .claude directories + +**Security Status:** +- ✅ Apache worker processes run as non-root (PUID:PGID) - Apache handles privilege dropping natively +- ✅ Files owned by non-root user (PUID:PGID) +- ✅ Logs properly written to stdout/stderr (Docker logging best practice) +- ✅ `.dockerignore` prevents sensitive files in build context +- ⚠️ `/server-status` endpoint exposed to all IPs (information disclosure - consider restricting) +- ℹ️ Apache master process runs as root (standard practice, required for port binding and config reading) + +## Branch Strategy + +- `main` - Primary branch, triggers CI/CD builds +- `alpine` - Current development branch (branch context for this session) + +## Making Changes + +**For Dockerfile changes:** +1. Test build locally first +2. Ensure shell script compatibility (Alpine uses busybox sh, not bash) +3. Keep line endings as LF (handled by .gitattributes) +4. Multi-arch builds may behave differently (especially ARM with QEMU) + +**For init.sh changes:** +1. Use `#!/bin/sh` not `#!/bin/bash` (Alpine compatibility) +2. Test both update and offline modes +3. Test with both IMG=TRUE and IMG=FALSE +4. Ensure file permissions work correctly with PUID/PGID +5. **Critical:** `chown` must happen AFTER all git operations to ensure proper file ownership +6. Apache httpd.conf User/Group directives must be set before starting httpd + +**For CI/CD changes:** +- Test with workflow_dispatch before merging +- Changes to .github/** are ignored by CI trigger (use workflow_dispatch) diff --git a/Dockerfile b/Dockerfile index 10c006f..8e3f421 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,8 @@ -FROM httpd:2.4.66 +FROM httpd:alpine3.20 -# ARG BEFORE FROM! ARG PUID=1000 ARG PGID=1000 + ENV PUID=$PUID \ PGID=$PGID @@ -10,25 +10,25 @@ ENV PUID=$PUID \ COPY --chmod=755 init.sh /init.sh # Install deps + cleanup (reduced CVEs/Bloat) -RUN apt-get update && \ - apt-get -y upgrade -qq && \ - apt-get -y install --no-install-recommends curl git jq && \ - rm -rf /var/lib/apt/lists/* - +RUN apk add --no-cache git jq su-exec && \ + rm -rf /var/cache/apk/* -RUN echo "\n"\ - " SetHandler server-status\n"\ - " Order deny,allow\n"\ - " Allow from all\n"\ - "\n"\ - >> /usr/local/apache2/conf/httpd.conf +RUN printf '\n\ + SetHandler server-status\n\ + Order deny,allow\n\ + Allow from all\n\ +\n' \ +>> /usr/local/apache2/conf/httpd.conf # htdocs clean + chown WORKDIR /usr/local/apache2/htdocs/ -RUN rm -rf * .??* && \ - groupadd -g ${PGID} appgroup && \ - useradd -u ${PUID} -g appgroup -s /bin/bash appuser && \ - chown -R appuser:appgroup . +RUN rm -rf * .[!.]* ..?* +#RUN rm -rf * .??* && \ +# addgroup -g $PGID -S appgroup && \ +# adduser -u $PUID -S -G appgroup appuser && \ +# chown -R appuser:appgroup . + + # Labels for registry LABEL org.opencontainers.image.source="https://github.com/Sakujakira/5etools-docker" \ @@ -36,7 +36,7 @@ LABEL org.opencontainers.image.source="https://github.com/Sakujakira/5etools-doc # Healthcheck HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ - CMD curl -f http://localhost/ || exit 1 + CMD wget -qO- http://localhost/ || exit 1 -USER appuser -CMD ["/bin/bash", "/init.sh"] \ No newline at end of file +#USER appuser +CMD ["/init.sh"] \ No newline at end of file diff --git a/README.md b/README.md index 3a40ff6..a3c720b 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,63 @@ -This is a simple image for hosting your own 5eTools instance. It is based on the Apache `httpd` image and uses components of the auto-updater script from the [5eTools wiki](https://wiki.tercept.net/en/5eTools/InstallGuide). This image is built from [this GitHub repository](https://github.com/Jafner/5etools-docker). +This is a simple image for hosting your own 5eTools instance. It is based on the Apache `httpd` Alpine image and uses components of the auto-updater script from the [5eTools wiki](https://wiki.tercept.net/en/5eTools/InstallGuide). This image is built from [this GitHub repository](https://github.com/Sakujakira/5etools-docker). + +This project is based on the original repository: https://github.com/Jafner/5etools-docker. + +## Comparison with Original Image + +This fork provides several improvements over the original implementation: + +### Image Size & Security +| Metric | This Image (Alpine) | Original (Debian) | Improvement | +|--------|---------------------|-------------------|-------------| +| **Image Size** | 77 MB | 279 MB | **3.6× smaller** | +| **Packages** | 62 | 224 | **3.6× fewer** | +| **Total CVEs** | 55 | 173 | **68% fewer vulnerabilities** | +| **Critical CVEs** | 2 | 7 | **71% fewer critical** | +| **High CVEs** | 18 | 34 | **47% fewer high** | + +*CVE data from Docker Scout as of February 2026* + +### Key Differences +- **Base Image**: Alpine Linux 3.20 vs Debian 12 (Bookworm) +- **Smaller Attack Surface**: Fewer packages means fewer potential vulnerabilities +- **Active Maintenance**: Updated dependencies and security patches +- **Improved Git Operations**: Robust handling of repository updates with `git reset --hard` + `git pull` +- **Enhanced Security Model**: See Security section below + +### Why Alpine? +Alpine Linux is purpose-built for containers with minimal bloat. The smaller footprint means: +- Faster image pulls and container startup +- Reduced disk space usage +- Fewer packages to patch and maintain +- Smaller attack surface for security vulnerabilities + +## Security + +This implementation follows Docker security best practices: + +### Non-Root Execution +- **Apache worker processes run as non-root** (PUID/PGID specified user) +- Apache master process runs as root only for initial setup (standard Apache practice) +- All web content files owned by non-root user (PUID:PGID) +- Privilege separation via Apache's native User/Group directives + +### File Permissions +- Downloaded files are owned by the user specified via `PUID`/`PGID` environment variables +- Apache logs directory properly permissioned for non-root access +- Git operations complete before ownership transfer to ensure consistent permissions + +### Container Security +- Minimal package installation (git, jq, su-exec only) +- `.dockerignore` prevents sensitive files in build context +- Healthcheck monitors container status +- Idempotent startup script handles container restarts gracefully + +### Comparison with Original +The original implementation runs Apache entirely as root, while this fork properly segregates privileges: +- **Original**: All Apache processes run as root +- **This fork**: Master as root (required), workers as PUID:PGID (least privilege) + +This follows the principle of least privilege and reduces the impact of potential Apache vulnerabilities. # Usage Below we talk about how to install and configure the container. @@ -8,7 +67,7 @@ You can quick-start this image by running: ``` mkdir -p ~/5etools-docker/htdocs && cd ~/5etools-docker -curl -o docker-compose.yml https://raw.githubusercontent.com/Jafner/5etools-docker/main/docker-compose.yml +curl -o docker-compose.yml https://raw.githubusercontent.com/Sakujakira/5etools-docker/refs/heads/main/docker-compose.yml docker-compose up -d && docker logs -f 5etools-docker ``` @@ -55,8 +114,8 @@ By default, I assume you want to automatically download the latest files from th ### IMG (defaults to FALSE) Required unless OFFLINE_MODE=TRUE. Expects one of "TRUE", "FALSE" Where: - > "TRUE" pulls from https://github.com/5etools-mirror-2/5etools-mirror-2.github.io.git and adds https://github.com/5etools-mirror-2/5etools-img as a submodule for image files. - > "FALSE" pulls from https://github.com/5etools-mirror-2/5etools-mirror-2.github.io.git without image files. + > "TRUE" pulls from https://github.com/5etools-mirror-3/5etools-src and adds https://github.com/5etools-mirror-3/5etools-img as a submodule for image files. + > "FALSE" pulls from https://github.com/5etools-mirror-3/5etools-src without image files. The get.5e.tools source has been down (redirecting to 5e.tools) during development. This method is not tested. @@ -64,8 +123,21 @@ The get.5e.tools source has been down (redirecting to 5e.tools) during developme Optional. Expects "TRUE" to enable. Setting this to true tells the server to run from the local files if available, or exits if there is no local version. -### PUID and PGID -During the image build process, we set the owner of the `htdocs` directory to `1000:1000` by default. If you need a different UID and GID to own the files, you can build the image from the source Dockerfile and pass the PUID and PGID variables as desired. +### PUID and PGID (defaults to 1000) +These environment variables control the user and group ownership of files in the container. + +```yaml +environment: + - PUID=1001 # User ID + - PGID=1001 # Group ID +``` + +The container dynamically creates a user/group with the specified IDs at startup and: +- Sets ownership of all downloaded files to PUID:PGID +- Configures Apache worker processes to run as PUID:PGID +- Ensures proper file permissions for your host system + +**Why this matters**: If your host user is UID 1001, set `PUID=1001` so you can easily edit files outside the container without permission issues. ## Integrating a reverse proxy Supporting integration of a reverse proxy is beyond the scope of this guide. @@ -103,4 +175,17 @@ Then your `index.json` should look like: ``` Note the commas after each entry except the last in each array. -See the [wiki page](https://wiki.5e.tools/index.php/5eTools_Install_Guide) for more information. \ No newline at end of file +See the [wiki page](https://wiki.5e.tools/index.php/5eTools_Install_Guide) for more information. + +--- + +## AI-Assisted Development Disclaimer + +This project was developed with assistance from Claude (Anthropic's AI assistant) for: +- Documentation writing and formatting +- Code reviews and security analysis +- Identifying potential issues and suggesting improvements + +**However, all architectural decisions, implementation choices, and code changes were made by the repository maintainer.** The AI served as a development tool for analysis and documentation, not as the primary author of the codebase. + +The core improvements (Alpine migration, security enhancements, git operation fixes, and privilege separation) were designed and implemented by human developers. \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index b9bb8be..2b99f3f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,13 +2,13 @@ version: "3" services: 5etools-docker: container_name: 5etools-docker - image: jafner/5etools-docker + image: ghcr.io/sakujakira/5etools-docker:latest volumes: - ~/5etools-docker/htdocs:/usr/local/apache2/htdocs ports: - 8080:80/tcp environment: - - IMG=FALSE # Set to TRUE to pull images from https://github.com/5etools-mirror-2/5etools-img (as a Git submodule) + - IMG=FALSE # Set to TRUE to pull images from https://github.com/5etools-mirror-3/5etools-img (as a Git submodule) #- OFFLINE_MODE=TRUE # Optional. Expects "TRUE" or "FALSE". Disables checking for new updates. # Uncomment this block to use a docker-managed volume: diff --git a/init.sh b/init.sh index d31416b..e6e6cbf 100755 --- a/init.sh +++ b/init.sh @@ -1,69 +1,98 @@ -#!/bin/bash +#!/bin/sh +set -e # Exit on error -# Print current user ID -id +PUID=${PUID:-1000} +PGID=${PGID:-1000} +DL_LINK=${DL_LINK:-https://github.com/5etools-mirror-3/5etools-src.git} +IMG_LINK=${IMG_LINK:-https://github.com/5etools-mirror-3/5etools-img.git} -# Ensure clean, non-root ownership of the htdocs directory. -chown -R $PUID:$PGID /usr/local/apache2/htdocs +printf " === Provided PUID: %s\n" "$PUID" +printf " === Provided PGID: %s\n" "$PGID" +printf " === These Links will be used:\n" +printf " === DL_LINK: %s\n" "$DL_LINK" +printf " === IMG_LINK: %s\n" "$IMG_LINK" -# Delete index.html if it's the stock apache file. Otherwise it impedes the git clone. -if grep -Fq '

It works!

' "/usr/local/apache2/htdocs/index.html"; then - rm /usr/local/apache2/htdocs/index.html -fi +# If User and group don't exist, create them. If they do exist, ignore the error and continue. +addgroup -g "$PGID" appgroup 2>/dev/null || true +adduser -D -u "$PUID" appuser -G appgroup 2>/dev/null || true # If the user doesn't want to update from a source, # check for local version. # If local version is found, print version and start server. # If no local version is found, print error message and exit. if [ "$OFFLINE_MODE" = "TRUE" ]; then - echo " === Offline mode is enabled. Will try to launch from local files. Checking for local version..." + printf " === Offline mode is enabled. Will try to launch from local files. Checking for local version...\n" if [ -f /usr/local/apache2/htdocs/package.json ]; then - VERSION=$(jq -r .version package.json) # Get version from package.json - echo " === Starting version $VERSION" + VERSION=$(jq -r .version /usr/local/apache2/htdocs/package.json) # Get version from package.json + printf " === Starting version %s\n" "$VERSION" + printf " === Configuring Apache to run as user %s:%s\n" "$PUID" "$PGID" + # Configure Apache to run worker processes as the specified user/group + sed -i "s/^User .*/User #$PUID/" /usr/local/apache2/conf/httpd.conf + sed -i "s/^Group .*/Group #$PGID/" /usr/local/apache2/conf/httpd.conf httpd-foreground else - echo " === No local version detected. Exiting." + printf " === No local version detected. Exiting.\n" exit 1 fi fi # Move to the working directory for working with files. -cd /usr/local/apache2/htdocs +cd /usr/local/apache2/htdocs || exit -echo " === Checking directory permissions for /usr/local/apache2/htdocs" +printf " === Checking directory permissions for /usr/local/apache2/htdocs\n" ls -ld /usr/local/apache2/htdocs -DL_LINK=${DL_LINK:-https://github.com/5etools-mirror-2/5etools-mirror-2.github.io.git} -IMG_LINK=${IMG_LINK:-https://github.com/5etools-mirror-2/5etools-img} - -echo " === Using GitHub mirror at $DL_LINK" +printf " === Using GitHub mirror at %s\n" "$DL_LINK" if [ ! -d "./.git" ]; then # if no git repository already exists - echo " === No existing git repository, creating one" + printf " === No existing git repository, creating one\n" git config --global user.email "autodeploy@localhost" git config --global user.name "AutoDeploy" git config --global pull.rebase false # Squelch nag message git config --global --add safe.directory '/usr/local/apache2/htdocs' # Disable directory ownership checking, required for mounted volumes - git clone $DL_LINK . # clone the repo with no files and no object history + git clone --depth=1 "$DL_LINK" . # clone the repo with no files and no object history else - echo " === Using existing git repository" + printf " === Using existing git repository\n" git config --global --add safe.directory '/usr/local/apache2/htdocs' # Disable directory ownership checking, required for mounted volumes fi -if [[ "$IMG" == "TRUE" ]]; then # if user wants images - echo " === Pulling images from GitHub... (This will take a while)" - git submodule add -f $IMG_LINK /usr/local/apache2/htdocs/img +if [ "$IMG" = "TRUE" ]; then # if user wants images + printf " === Pulling images from GitHub... (This will take a while)\n" + if [ ! -d "./img/.git" ]; then + git submodule add --depth=1 -f "$IMG_LINK" /usr/local/apache2/htdocs/img + else + printf " === Using existing img submodule\n" + git submodule update --remote --depth=1 + fi +fi + +printf " === Pulling latest files from GitHub...\n" +#git fetch origin --depth=1 +git reset --hard origin/HEAD +git pull origin main --depth=1 +if [ -f /usr/local/apache2/htdocs/package.json ]; then + VERSION=$(jq -r .version /usr/local/apache2/htdocs/package.json) # Get version from package.json +else + VERSION="unknown (no package.json)" fi -echo " === Pulling latest files from GitHub..." -git checkout -git fetch -git pull --depth=1 -VERSION=$(jq -r .version package.json) # Get version from package.json -if [[ `git status --porcelain` ]]; then +if [ -n "$(git status --porcelain)" ]; then git restore . fi -echo " === Starting version $VERSION" +# Since git ran as root, we need to change ownership of the htdocs and logs directories to the non-root user. +# This must happen AFTER all git operations are complete. +printf " === Setting ownership of files to %s:%s\n" "$PUID" "$PGID" +chown -R "$PUID":"$PGID" /usr/local/apache2/htdocs +chown -R "$PUID":"$PGID" /usr/local/apache2/logs + +ls -la /usr/local/apache2/htdocs + +printf " === Starting version %s\n" "$VERSION" +printf " === Configuring Apache to run as user %s:%s\n" "$PUID" "$PGID" + +# Configure Apache to run worker processes as the specified user/group +sed -i "s/^User .*/User #$PUID/" /usr/local/apache2/conf/httpd.conf +sed -i "s/^Group .*/Group #$PGID/" /usr/local/apache2/conf/httpd.conf httpd-foreground