-
Notifications
You must be signed in to change notification settings - Fork 3
feat: Add distroless image for Ghost #603
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -40,7 +40,7 @@ | |
| VERSION="${VERSION}" \ | ||
| GHOST_CLI_VERSION="${GHOST_CLI_VERSION}" | ||
|
|
||
| LABEL org.opencontainers.image.authors="Pascal Andy https://firepress.org/en/contact/" \ | ||
|
Check warning on line 43 in v5/Dockerfile
|
||
| org.opencontainers.image.vendor="https://firepress.org/" \ | ||
| org.opencontainers.image.created="${BUILD_DATE}" \ | ||
| org.opencontainers.image.revision="${VCS_REF}" \ | ||
|
|
@@ -57,7 +57,7 @@ | |
| com.firepress.image.schema_version="1.0" | ||
|
|
||
| # Install gosu for easy step-down from root | ||
| ENV GOSU_VERSION 1.17 | ||
|
Check warning on line 60 in v5/Dockerfile
|
||
| RUN set -eux; \ | ||
| apk add --no-cache --virtual .gosu-deps \ | ||
| ca-certificates \ | ||
|
|
@@ -182,8 +182,34 @@ | |
| npm cache clean --force; \ | ||
| rm -rv /tmp/yarn* /tmp/v8* | ||
|
|
||
| # Pre-initialize Ghost content for distroless compatibility | ||
| # This replaces the functionality of docker-entrypoint.sh for distroless runtime | ||
| RUN set -eux; \ | ||
| cd "$GHOST_INSTALL"; \ | ||
| if [ -d "content.orig" ]; then \ | ||
| echo "Pre-initializing Ghost content for distroless runtime..."; \ | ||
| # Copy default content if target doesn't exist \ | ||
| for src in content.orig/*/ content.orig/themes/*; do \ | ||
| if [ -d "$src" ]; then \ | ||
| src="${src%/}"; \ | ||
| target="$GHOST_CONTENT/${src#*/content.orig/}"; \ | ||
| mkdir -p "$(dirname "$target")"; \ | ||
| if [ ! -e "$target" ]; then \ | ||
| echo "Copying $src to $target"; \ | ||
| cp -r "$src" "$target"; \ | ||
| fi; \ | ||
| fi; \ | ||
| done; \ | ||
| # Set proper ownership for distroless nonroot user (UID 65532) \ | ||
| chown -R 65532:65532 "$GHOST_CONTENT"; \ | ||
| chown -R 65532:65532 "$GHOST_INSTALL"; \ | ||
| echo "Content initialization completed for distroless runtime"; \ | ||
| else \ | ||
| echo "Warning: content.orig directory not found, skipping content initialization"; \ | ||
| fi | ||
|
|
||
| # ---------------------------------------------- | ||
| # 5) LAYER final | ||
| # 5) LAYER final (Alpine-based - kept for compatibility) | ||
| # ---------------------------------------------- | ||
| FROM mynode AS final | ||
|
|
||
|
|
@@ -198,4 +224,52 @@ | |
| # HEALTHCHECK must be done during the runtime | ||
|
|
||
| ENTRYPOINT [ "docker-entrypoint.sh" ] | ||
| CMD [ "node", "current/index.js" ] | ||
| CMD [ "node", "current/index.js" ] | ||
|
|
||
| # ---------------------------------------------- | ||
| # 6) LAYER distroless (Production runtime) | ||
| # ---------------------------------------------- | ||
| FROM gcr.io/distroless/nodejs20-debian12 AS distroless | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Pin distroless base image 🧰 Tools🪛 Checkov (3.2.334)[LOW] 232-232: Ensure the base image uses a non latest version tag (CKV_DOCKER_7) 🪛 Hadolint (2.12.0)[warning] 232-232: Always tag the version of an image explicitly (DL3006) 🤖 Prompt for AI Agents |
||
|
|
||
| ARG VERSION | ||
| ARG GHOST_CLI_VERSION | ||
| ARG NODE_VERSION | ||
|
|
||
| # Set environment variables for distroless runtime | ||
| ENV GHOST_INSTALL="/var/lib/ghost" \ | ||
| GHOST_CONTENT="/var/lib/ghost/content" \ | ||
| NODE_ENV="production" \ | ||
| VERSION="${VERSION}" \ | ||
| GHOST_CLI_VERSION="${GHOST_CLI_VERSION}" \ | ||
| NODE_VERSION="${NODE_VERSION}" | ||
|
|
||
| # Add labels for distroless image | ||
| LABEL org.opencontainers.image.authors="Pascal Andy https://firepress.org/en/contact/" \ | ||
| org.opencontainers.image.vendor="https://firepress.org/" \ | ||
| org.opencontainers.image.title="Ghost (Distroless)" \ | ||
| org.opencontainers.image.description="Distroless Docker image for Ghost ${VERSION}" \ | ||
| org.opencontainers.image.url="https://hub.docker.com/r/devmtl/ghostfire/tags/" \ | ||
| org.opencontainers.image.source="https://github.com/firepress-org/ghostfire" \ | ||
| org.opencontainers.image.licenses="GNUv3 https://github.com/pascalandy/GNU-GENERAL-PUBLIC-LICENSE/blob/master/LICENSE.md" \ | ||
| com.firepress.image.ghost_cli_version="${GHOST_CLI_VERSION}" \ | ||
| com.firepress.image.user="nonroot" \ | ||
| com.firepress.image.node_env="${NODE_ENV}" \ | ||
| com.firepress.image.node_version="${NODE_VERSION}" \ | ||
| com.firepress.image.base_os="distroless" \ | ||
| com.firepress.image.schema_version="1.0" | ||
|
|
||
| # Copy Ghost installation from builder stage with nonroot ownership | ||
| COPY --from=builder --chown=nonroot:nonroot "${GHOST_INSTALL}" "${GHOST_INSTALL}" | ||
|
|
||
| # Set working directory and volume | ||
| WORKDIR "${GHOST_INSTALL}" | ||
| VOLUME "${GHOST_CONTENT}" | ||
|
|
||
| # Use nonroot user (UID 65532) for security | ||
| USER nonroot | ||
|
|
||
| # Expose Ghost port | ||
| EXPOSE 2368 | ||
|
|
||
| # Direct Node.js execution (no shell entrypoint needed in distroless) | ||
| CMD ["node", "current/index.js"] | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,253 @@ | ||
| # Ghost Distroless Migration | ||
|
|
||
| This document describes the migration of the Ghost Docker image to use Google's distroless base image for enhanced security and reduced attack surface. | ||
|
|
||
| ## Overview | ||
|
|
||
| The Ghost Docker image now supports two runtime options: | ||
| - **Alpine-based** (original): Full-featured with shell access for debugging | ||
| - **Distroless** (new): Minimal, secure runtime without shell or package manager | ||
|
|
||
| ## Architecture | ||
|
|
||
| ### Multi-Stage Build Process | ||
|
|
||
| ``` | ||
| ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ | ||
| │ Build Stages │ │ Alpine Runtime │ │Distroless Runtime│ | ||
| │ │ │ │ │ │ | ||
| │ 1. mynode │───▶│ 5. final │ │ 6. distroless │ | ||
| │ 2. debug │ │ │ │ │ | ||
| │ 3. builder │ │ + Shell access │ │ + Minimal size │ | ||
| │ 4. (packages) │ │ + Debugging │ │ + Enhanced security│ | ||
| │ │ │ + Flexibility │ │ + No shell access │ | ||
| └─────────────────┘ └─────────────────┘ └─────────────────┘ | ||
| ``` | ||
|
|
||
| ### Key Changes | ||
|
|
||
| #### Base Image | ||
| - **From**: `node:20.19.2-alpine3.22` | ||
| - **To**: `gcr.io/distroless/nodejs20-debian12` | ||
|
|
||
| #### User Management | ||
| - **From**: `node` user (UID 1000) with `gosu` privilege dropping | ||
| - **To**: `nonroot` user (UID 65532) - no privilege dropping needed | ||
|
|
||
| #### Entrypoint Strategy | ||
| - **From**: Shell-based `docker-entrypoint.sh` with runtime content initialization | ||
| - **To**: Direct Node.js execution with build-time content initialization | ||
|
|
||
| ## Building Images | ||
|
|
||
| ### Build Both Versions | ||
| ```bash | ||
| ./v5/build-distroless.sh | ||
| ``` | ||
|
|
||
| ### Build Specific Versions | ||
| ```bash | ||
| # Alpine version (original) | ||
| docker build --target final -t ghostfire:alpine -f v5/Dockerfile . | ||
|
|
||
| # Distroless version (recommended for production) | ||
| docker build --target distroless -t ghostfire:distroless -f v5/Dockerfile . | ||
| ``` | ||
|
|
||
| ## Testing | ||
|
|
||
| ### Test Distroless Version | ||
| ```bash | ||
| ./v5/test-distroless.sh | ||
| ``` | ||
|
|
||
| ### Manual Testing | ||
| ```bash | ||
| # Run distroless container | ||
| docker run -d -p 2368:2368 --name ghost-test ghostfire:distroless | ||
|
|
||
| # Check if Ghost is running | ||
| curl http://localhost:2368 | ||
|
|
||
| # View logs (no shell access available) | ||
| docker logs ghost-test | ||
|
|
||
| # Clean up | ||
| docker rm -f ghost-test | ||
| ``` | ||
|
|
||
| ## Security Benefits | ||
|
|
||
| ### Distroless Advantages | ||
| - **Minimal Attack Surface**: No shell, package manager, or unnecessary binaries | ||
| - **Reduced CVE Exposure**: Fewer packages mean fewer potential vulnerabilities | ||
| - **Immutable Runtime**: Cannot install additional packages or modify system | ||
| - **Smaller Image Size**: ~50-100MB reduction compared to Alpine version | ||
|
|
||
| ### Security Comparison | ||
|
|
||
| | Feature | Alpine | Distroless | | ||
| |---------|--------|------------| | ||
| | Shell Access | ✅ bash/sh | ❌ None | | ||
| | Package Manager | ✅ apk | ❌ None | | ||
| | Debug Tools | ✅ Available | ❌ None | | ||
| | CVE Surface | 🟡 Medium | 🟢 Minimal | | ||
| | Image Size | 🟡 ~200MB | 🟢 ~150MB | | ||
| | Runtime Modification | 🔴 Possible | 🟢 Impossible | | ||
|
|
||
| ## Production Deployment | ||
|
|
||
| ### Recommended Usage | ||
| ```yaml | ||
| # docker-compose.yml | ||
| version: '3.8' | ||
| services: | ||
| ghost: | ||
| image: devmtl/ghostfire:distroless | ||
| ports: | ||
| - "2368:2368" | ||
| environment: | ||
| - NODE_ENV=production | ||
| volumes: | ||
| - ghost_content:/var/lib/ghost/content | ||
| restart: unless-stopped | ||
| security_opt: | ||
| - no-new-privileges:true | ||
| read_only: true | ||
| tmpfs: | ||
| - /tmp | ||
| ``` | ||
|
|
||
| ### Kubernetes Deployment | ||
| ```yaml | ||
| apiVersion: apps/v1 | ||
| kind: Deployment | ||
| metadata: | ||
| name: ghost-distroless | ||
| spec: | ||
| replicas: 1 | ||
| selector: | ||
| matchLabels: | ||
| app: ghost | ||
| template: | ||
| metadata: | ||
| labels: | ||
| app: ghost | ||
| spec: | ||
| securityContext: | ||
| runAsNonRoot: true | ||
| runAsUser: 65532 | ||
| fsGroup: 65532 | ||
| containers: | ||
| - name: ghost | ||
| image: devmtl/ghostfire:distroless | ||
| ports: | ||
| - containerPort: 2368 | ||
| securityContext: | ||
| allowPrivilegeEscalation: false | ||
| readOnlyRootFilesystem: true | ||
| capabilities: | ||
| drop: | ||
| - ALL | ||
| volumeMounts: | ||
| - name: ghost-content | ||
| mountPath: /var/lib/ghost/content | ||
| - name: tmp | ||
| mountPath: /tmp | ||
| volumes: | ||
| - name: ghost-content | ||
| persistentVolumeClaim: | ||
| claimName: ghost-content-pvc | ||
| - name: tmp | ||
| emptyDir: {} | ||
| ``` | ||
|
|
||
| ## Debugging | ||
|
|
||
| ### Distroless Debugging | ||
| Since distroless images don't have shell access, debugging requires different approaches: | ||
|
|
||
| #### 1. Use Debug Variant | ||
| ```bash | ||
| # Build with debug variant for troubleshooting | ||
| FROM gcr.io/distroless/nodejs20-debian12:debug AS distroless-debug | ||
| # ... rest of distroless stage | ||
| ``` | ||
|
|
||
| #### 2. Log Analysis | ||
| ```bash | ||
| # View container logs | ||
| docker logs <container-name> | ||
|
|
||
| # Follow logs in real-time | ||
| docker logs -f <container-name> | ||
| ``` | ||
|
|
||
| #### 3. External Debugging Tools | ||
| ```bash | ||
| # Use external tools to inspect running container | ||
| docker exec <container-name> cat /proc/1/status | ||
| docker inspect <container-name> | ||
| ``` | ||
|
|
||
| #### 4. Fallback to Alpine | ||
| For complex debugging, temporarily use the Alpine version: | ||
| ```bash | ||
| docker run -it --rm ghostfire:alpine sh | ||
| ``` | ||
|
|
||
| ## Migration Checklist | ||
|
|
||
| - [x] ✅ Multi-stage Dockerfile with distroless target | ||
| - [x] ✅ Content initialization moved to build stage | ||
| - [x] ✅ User management updated for nonroot user | ||
| - [x] ✅ Build scripts for both versions | ||
| - [x] ✅ Test scripts for validation | ||
| - [x] ✅ Documentation and deployment examples | ||
| - [ ] 🔄 Production testing and validation | ||
| - [ ] 🔄 CI/CD pipeline updates | ||
| - [ ] 🔄 Monitoring and alerting adjustments | ||
|
|
||
| ## Troubleshooting | ||
|
|
||
| ### Common Issues | ||
|
|
||
| #### Content Permissions | ||
| If Ghost fails to start due to content permissions: | ||
| ```bash | ||
| # Check content ownership in Alpine version | ||
| docker run --rm -v ghost_content:/content ghostfire:alpine ls -la /content | ||
|
|
||
| # Fix permissions if needed | ||
| docker run --rm -v ghost_content:/content ghostfire:alpine chown -R 65532:65532 /content | ||
| ``` | ||
|
|
||
|
Comment on lines
+218
to
+224
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick (assertive) Grammar: Insert missing comma -If Ghost starts but content is missing:
+If Ghost starts, but content is missing:
🤖 Prompt for AI Agents |
||
| #### Missing Content | ||
| If Ghost starts but content is missing: | ||
| ```bash | ||
| # Verify content initialization in build logs | ||
| docker build --target distroless --progress=plain -f v5/Dockerfile . 2>&1 | grep -i content | ||
| ``` | ||
|
|
||
| #### Performance Issues | ||
| Monitor startup time and resource usage: | ||
| ```bash | ||
| # Compare startup times | ||
| time docker run --rm ghostfire:alpine node --version | ||
| time docker run --rm ghostfire:distroless node --version | ||
| ``` | ||
|
|
||
| ## Support | ||
|
|
||
| For issues related to the distroless migration: | ||
| 1. Check the troubleshooting section above | ||
| 2. Review container logs for error messages | ||
| 3. Test with the Alpine version to isolate distroless-specific issues | ||
| 4. Use the debug variant for deeper investigation | ||
|
|
||
| ## References | ||
|
|
||
| - [Google Distroless Images](https://github.com/GoogleContainerTools/distroless) | ||
| - [Ghost.js Documentation](https://ghost.org/docs/) | ||
| - [Docker Multi-stage Builds](https://docs.docker.com/develop/dev-best-practices/dockerfile_best-practices/#use-multi-stage-builds) | ||
| - [Container Security Best Practices](https://cloud.google.com/architecture/best-practices-for-building-containers) | ||
|
Comment on lines
+1
to
+253
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick (assertive) Improve Markdown formatting and readability
🧰 Tools🪛 LanguageTool[uncategorized] ~226-~226: Use a comma before ‘but’ if it connects two independent clauses (unless they are closely connected and short). (COMMA_COMPOUND_SENTENCE_2) [style] ~246-~246: Consider a different adjective to strengthen your wording. (DEEP_PROFOUND) 🪛 markdownlint-cli2 (0.17.2)8-8: Lists should be surrounded by blank lines (MD032, blanks-around-lists) 15-15: Fenced code blocks should have a language specified (MD040, fenced-code-language) 29-29: Headings should be surrounded by blank lines (MD022, blanks-around-headings) 30-30: Lists should be surrounded by blank lines (MD032, blanks-around-lists) 33-33: Headings should be surrounded by blank lines (MD022, blanks-around-headings) 34-34: Lists should be surrounded by blank lines (MD032, blanks-around-lists) 37-37: Headings should be surrounded by blank lines (MD022, blanks-around-headings) 38-38: Lists should be surrounded by blank lines (MD032, blanks-around-lists) 43-43: Headings should be surrounded by blank lines (MD022, blanks-around-headings) 44-44: Fenced code blocks should be surrounded by blank lines (MD031, blanks-around-fences) 48-48: Headings should be surrounded by blank lines (MD022, blanks-around-headings) 49-49: Fenced code blocks should be surrounded by blank lines (MD031, blanks-around-fences) 59-59: Headings should be surrounded by blank lines (MD022, blanks-around-headings) 60-60: Fenced code blocks should be surrounded by blank lines (MD031, blanks-around-fences) 64-64: Headings should be surrounded by blank lines (MD022, blanks-around-headings) 65-65: Fenced code blocks should be surrounded by blank lines (MD031, blanks-around-fences) 81-81: Headings should be surrounded by blank lines (MD022, blanks-around-headings) 82-82: Lists should be surrounded by blank lines (MD032, blanks-around-lists) 100-100: Headings should be surrounded by blank lines (MD022, blanks-around-headings) 101-101: Fenced code blocks should be surrounded by blank lines (MD031, blanks-around-fences) 121-121: Headings should be surrounded by blank lines (MD022, blanks-around-headings) 122-122: Fenced code blocks should be surrounded by blank lines (MD031, blanks-around-fences) 167-167: Headings should be surrounded by blank lines (MD022, blanks-around-headings) 170-170: Headings should be surrounded by blank lines (MD022, blanks-around-headings) 171-171: Fenced code blocks should be surrounded by blank lines (MD031, blanks-around-fences) 177-177: Headings should be surrounded by blank lines (MD022, blanks-around-headings) 178-178: Fenced code blocks should be surrounded by blank lines (MD031, blanks-around-fences) 186-186: Headings should be surrounded by blank lines (MD022, blanks-around-headings) 187-187: Fenced code blocks should be surrounded by blank lines (MD031, blanks-around-fences) 193-193: Headings should be surrounded by blank lines (MD022, blanks-around-headings) 195-195: Fenced code blocks should be surrounded by blank lines (MD031, blanks-around-fences) 215-215: Headings should be surrounded by blank lines (MD022, blanks-around-headings) 217-217: Fenced code blocks should be surrounded by blank lines (MD031, blanks-around-fences) 225-225: Headings should be surrounded by blank lines (MD022, blanks-around-headings) 227-227: Fenced code blocks should be surrounded by blank lines (MD031, blanks-around-fences) 232-232: Headings should be surrounded by blank lines (MD022, blanks-around-headings) 234-234: Fenced code blocks should be surrounded by blank lines (MD031, blanks-around-fences) 243-243: Lists should be surrounded by blank lines (MD032, blanks-around-lists) 253-253: Files should end with a single newline character (MD047, single-trailing-newline) 🤖 Prompt for AI Agents |
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick (assertive)
Refactor build step to use WORKDIR
Instead of using
cd "$GHOST_INSTALL"inside the RUN, introduce aWORKDIR ${GHOST_INSTALL}before this RUN. This improves layer caching and readability.🧰 Tools
🪛 Hadolint (2.12.0)
[warning] 187-187: Use WORKDIR to switch to a directory
(DL3003)
🤖 Prompt for AI Agents