Skip to content

feat: Add distroless image for Ghost#603

Closed
pascalandy wants to merge 1 commit intomasterfrom
edge_18e
Closed

feat: Add distroless image for Ghost#603
pascalandy wants to merge 1 commit intomasterfrom
edge_18e

Conversation

@pascalandy
Copy link

@pascalandy pascalandy commented Jun 13, 2025

Introduces a new distroless build stage to the Dockerfile, providing a minimal and secure production runtime for Ghost.

This change includes:

  • Pre-initialization of Ghost content within the build stage to ensure compatibility with the distroless environment, replacing docker-entrypoint.sh functionality.
  • A new distroless stage based on gcr.io/distroless/nodejs20-debian12.
  • Configuration of environment variables and OCI labels specific to the distroless image.
  • Copying of Ghost installation with nonroot ownership (UID 65532) for enhanced security.
  • Direct execution of node current/index.js as the CMD, removing the need for a shell entrypoint in the distroless image.

Summary by CodeRabbit

  • New Features

    • Added support for a minimal, secure distroless Docker image for running Ghost, in addition to the existing Alpine-based image.
    • Introduced automated build and test scripts for both Alpine and distroless image variants.
  • Documentation

    • Added comprehensive documentation outlining the differences, benefits, and usage instructions for the new distroless Docker image, including migration, deployment, and troubleshooting guidance.

Introduces a new distroless build stage to the Dockerfile, providing a minimal and secure production runtime for Ghost.

This change includes:
- Pre-initialization of Ghost content within the build stage to ensure compatibility with the distroless environment, replacing `docker-entrypoint.sh` functionality.
- A new `distroless` stage based on `gcr.io/distroless/nodejs20-debian12`.
- Configuration of environment variables and OCI labels specific to the distroless image.
- Copying of Ghost installation with `nonroot` ownership (UID 65532) for enhanced security.
- Direct execution of `node current/index.js` as the CMD, removing the need for a shell entrypoint in the distroless image.
@coderabbitai
Copy link

coderabbitai bot commented Jun 13, 2025

Walkthrough

Support for a distroless production runtime has been added to the Ghost Docker build, including Dockerfile changes, a new README for distroless usage, and new Bash scripts for building and testing both Alpine and distroless images. The distroless image removes shell access and uses a non-root user for enhanced security.

Changes

File(s) Change Summary
v5/Dockerfile Extended to add a distroless runtime stage, pre-initialize Ghost content, set permissions for nonroot user, and adjust entrypoint.
v5/README-distroless.md Added documentation detailing the migration to and usage of the distroless Docker image, including security and debugging guidance.
v5/build-distroless.sh New Bash script for building both Alpine and distroless Ghost Docker images, tagging, and reporting image sizes.
v5/test-distroless.sh New Bash script for testing the distroless Ghost Docker image, including health checks and metadata display.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant BuildScript as build-distroless.sh
    participant Docker
    participant DistrolessImage
    participant GhostApp

    User->>BuildScript: Run build-distroless.sh
    BuildScript->>Docker: Build Alpine image (Dockerfile default)
    Docker-->>BuildScript: Alpine image built
    BuildScript->>Docker: Build distroless image (Dockerfile distroless stage)
    Docker-->>BuildScript: Distroless image built
    BuildScript->>User: Output image info and usage instructions

    User->>DistrolessImage: Run container (test-distroless.sh)
    DistrolessImage->>GhostApp: Start Node.js process
    GhostApp->>DistrolessImage: Serve HTTP on port 2368
    User->>GhostApp: Send HTTP request (health check)
    GhostApp-->>User: Return HTTP response
Loading
sequenceDiagram
    participant Dockerfile
    participant BuilderStage
    participant DistrolessStage
    participant GhostContent

    Dockerfile->>BuilderStage: Build Ghost, copy content, set permissions
    BuilderStage->>DistrolessStage: Copy Ghost app and content
    DistrolessStage->>GhostContent: Ensure content initialized, set ownership (UID 65532)
    DistrolessStage->>DistrolessStage: Set WORKDIR, USER, VOLUME, EXPOSE, CMD
Loading
✨ Finishing Touches
  • 📝 Generate Docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@pascalandy pascalandy changed the title feat: Add distroless image for Ghost Introduces a new distroless build stage to the Dockerfile, providing a minimal and secure production runtime for Ghost. This change includes: - Pre-initialization of Ghost content within the build stage to ensure compatibility with the distroless environment, replacing docker-entrypoint.sh functionality. - A new distroless stage based on gcr.io/distroless/nodejs20-debian12. - Configuration of environment variables and OCI labels specific to the distroless image. - Copying of Ghost installation with nonroot ownership (UID 65532) for enhanced security. - Direct execution of node current/index.js as the CMD, removing the need for a shell entrypoint in the distroless image. feat: Add distroless image for Ghost Jun 13, 2025
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

📜 Review details

Configuration used: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 173358f and 619d1e2.

📒 Files selected for processing (4)
  • v5/Dockerfile (2 hunks)
  • v5/README-distroless.md (1 hunks)
  • v5/build-distroless.sh (1 hunks)
  • v5/test-distroless.sh (1 hunks)
🧰 Additional context used
🪛 Hadolint (2.12.0)
v5/Dockerfile

[warning] 187-187: Use WORKDIR to switch to a directory

(DL3003)


[warning] 232-232: Always tag the version of an image explicitly

(DL3006)

🪛 Checkov (3.2.334)
v5/Dockerfile

[LOW] 232-232: Ensure the base image uses a non latest version tag

(CKV_DOCKER_7)


[LOW] 1-275: Ensure that HEALTHCHECK instructions have been added to container images

(CKV_DOCKER_2)

🪛 LanguageTool
v5/README-distroless.md

[uncategorized] ~226-~226: Use a comma before ‘but’ if it connects two independent clauses (unless they are closely connected and short).
Context: ...`` #### Missing Content If Ghost starts but content is missing: ```bash # Verify co...

(COMMA_COMPOUND_SENTENCE_2)


[style] ~246-~246: Consider a different adjective to strengthen your wording.
Context: ...fic issues 4. Use the debug variant for deeper investigation ## References - [Google...

(DEEP_PROFOUND)

🪛 markdownlint-cli2 (0.17.2)
v5/README-distroless.md

8-8: Lists should be surrounded by blank lines
null

(MD032, blanks-around-lists)


15-15: Fenced code blocks should have a language specified
null

(MD040, fenced-code-language)


29-29: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


30-30: Lists should be surrounded by blank lines
null

(MD032, blanks-around-lists)


33-33: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


34-34: Lists should be surrounded by blank lines
null

(MD032, blanks-around-lists)


37-37: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


38-38: Lists should be surrounded by blank lines
null

(MD032, blanks-around-lists)


43-43: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


44-44: Fenced code blocks should be surrounded by blank lines
null

(MD031, blanks-around-fences)


48-48: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


49-49: Fenced code blocks should be surrounded by blank lines
null

(MD031, blanks-around-fences)


59-59: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


60-60: Fenced code blocks should be surrounded by blank lines
null

(MD031, blanks-around-fences)


64-64: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


65-65: Fenced code blocks should be surrounded by blank lines
null

(MD031, blanks-around-fences)


81-81: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


82-82: Lists should be surrounded by blank lines
null

(MD032, blanks-around-lists)


100-100: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


101-101: Fenced code blocks should be surrounded by blank lines
null

(MD031, blanks-around-fences)


121-121: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


122-122: Fenced code blocks should be surrounded by blank lines
null

(MD031, blanks-around-fences)


167-167: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


170-170: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


171-171: Fenced code blocks should be surrounded by blank lines
null

(MD031, blanks-around-fences)


177-177: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


178-178: Fenced code blocks should be surrounded by blank lines
null

(MD031, blanks-around-fences)


186-186: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


187-187: Fenced code blocks should be surrounded by blank lines
null

(MD031, blanks-around-fences)


193-193: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


195-195: Fenced code blocks should be surrounded by blank lines
null

(MD031, blanks-around-fences)


215-215: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


217-217: Fenced code blocks should be surrounded by blank lines
null

(MD031, blanks-around-fences)


225-225: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


227-227: Fenced code blocks should be surrounded by blank lines
null

(MD031, blanks-around-fences)


232-232: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


234-234: Fenced code blocks should be surrounded by blank lines
null

(MD031, blanks-around-fences)


243-243: Lists should be surrounded by blank lines
null

(MD032, blanks-around-lists)


253-253: Files should end with a single newline character
null

(MD047, single-trailing-newline)

⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: build_edge

Comment on lines +185 to +205
# 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"; \
Copy link

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 a WORKDIR ${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
In v5/Dockerfile around lines 185 to 205, the RUN command uses 'cd
"$GHOST_INSTALL"' to change directories. To improve layer caching and
readability, add a WORKDIR instruction with the value of $GHOST_INSTALL before
this RUN command, and remove the 'cd "$GHOST_INSTALL"' from the RUN script.

Comment on lines 223 to +227
# HEALTHCHECK must be done during the runtime

ENTRYPOINT [ "docker-entrypoint.sh" ]
CMD [ "node", "current/index.js" ] No newline at end of file
CMD [ "node", "current/index.js" ]
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Add a HEALTHCHECK for production images
Both Alpine and distroless stages currently lack a HEALTHCHECK. Adding one (e.g., HEALTHCHECK --interval=30s --timeout=5s CMD curl -f http://localhost:2368/ || exit 1) allows orchestrators to monitor Ghost’s readiness.

🤖 Prompt for AI Agents
In v5/Dockerfile around lines 224 to 227, add a HEALTHCHECK instruction to the
production image to enable container orchestrators to monitor Ghost's readiness.
Insert a line such as 'HEALTHCHECK --interval=30s --timeout=5s CMD curl -f
http://localhost:2368/ || exit 1' before the ENTRYPOINT to periodically check
the service health.

# ----------------------------------------------
# 6) LAYER distroless (Production runtime)
# ----------------------------------------------
FROM gcr.io/distroless/nodejs20-debian12 AS distroless
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Pin distroless base image
The FROM gcr.io/distroless/nodejs20-debian12 instruction has no digest or patch version. Pinning to a fully qualified tag or digest ensures reproducible builds and mitigates supply-chain risks.

🧰 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
In v5/Dockerfile at line 232, the distroless base image is referenced without a
digest or patch version, which can lead to non-reproducible builds. Update the
FROM instruction to pin the image to a specific digest or a fully qualified tag
with a patch version to ensure consistent and secure builds.

Comment on lines +2 to +3
set -e

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Enable full strict mode
Replace set -e with set -euo pipefail (or set -euxo pipefail) to catch undefined variables and pipeline failures.

🤖 Prompt for AI Agents
In v5/build-distroless.sh around lines 2 to 3, replace the existing `set -e`
command with `set -euo pipefail` to enable full strict mode. This change will
ensure the script exits on errors, treats unset variables as errors, and
properly handles pipeline failures, improving script robustness.

Comment on lines +31 to +38
docker build \
--target distroless \
--build-arg VERSION="$VERSION" \
-t "$DOCKERHUB_USER/$APP_NAME:$VERSION-distroless" \
-t "$DOCKERHUB_USER/$APP_NAME:distroless" \
-t "$DOCKERHUB_USER/$APP_NAME:latest" \
-f v5/Dockerfile \
.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Ensure up-to-date distroless images
Similarly, add --pull to the distroless build command:

-docker build \
+docker build --pull \
     --target distroless \
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
docker build \
--target distroless \
--build-arg VERSION="$VERSION" \
-t "$DOCKERHUB_USER/$APP_NAME:$VERSION-distroless" \
-t "$DOCKERHUB_USER/$APP_NAME:distroless" \
-t "$DOCKERHUB_USER/$APP_NAME:latest" \
-f v5/Dockerfile \
.
docker build --pull \
--target distroless \
--build-arg VERSION="$VERSION" \
-t "$DOCKERHUB_USER/$APP_NAME:$VERSION-distroless" \
-t "$DOCKERHUB_USER/$APP_NAME:distroless" \
-t "$DOCKERHUB_USER/$APP_NAME:latest" \
-f v5/Dockerfile \
.
🤖 Prompt for AI Agents
In v5/build-distroless.sh around lines 31 to 38, the docker build command for
the distroless target is missing the --pull flag. Add --pull to the docker build
command to ensure it always pulls the latest base images before building,
keeping the distroless images up to date.

Comment on lines +18 to +25
docker build \
--target final \
--build-arg VERSION="$VERSION" \
-t "$DOCKERHUB_USER/$APP_NAME:$VERSION-alpine" \
-t "$DOCKERHUB_USER/$APP_NAME:alpine" \
-f v5/Dockerfile \
.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Ensure up-to-date base images
Include --pull in the Alpine build to always fetch the latest base image:

-docker build \
+docker build --pull \
     --target final \
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
docker build \
--target final \
--build-arg VERSION="$VERSION" \
-t "$DOCKERHUB_USER/$APP_NAME:$VERSION-alpine" \
-t "$DOCKERHUB_USER/$APP_NAME:alpine" \
-f v5/Dockerfile \
.
docker build --pull \
--target final \
--build-arg VERSION="$VERSION" \
-t "$DOCKERHUB_USER/$APP_NAME:$VERSION-alpine" \
-t "$DOCKERHUB_USER/$APP_NAME:alpine" \
-f v5/Dockerfile \
.
🤖 Prompt for AI Agents
In v5/build-distroless.sh around lines 18 to 25, the docker build command lacks
the --pull flag, which ensures the latest base image is fetched. Add the --pull
option to the docker build command to always pull the most recent Alpine base
image before building.

Comment on lines +2 to +3
set -e

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Recommend strict mode
Use set -euo pipefail (or set -euxo pipefail) instead of set -e to catch unset variables and pipeline errors.

🤖 Prompt for AI Agents
In v5/test-distroless.sh at lines 2 to 3, replace the existing 'set -e' with
'set -euo pipefail' to enable strict mode. This change will ensure the script
exits on unset variables and pipeline errors, improving robustness and error
detection.

Comment on lines +14 to +16
echo "🧹 Cleaning up existing test containers..."
docker rm -f "$CONTAINER_NAME" 2>/dev/null || true

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Ensure cleanup on exit
Add a cleanup trap so the test container is always removed:

trap 'docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1' EXIT
🤖 Prompt for AI Agents
In v5/test-distroless.sh around lines 14 to 16, add a trap command to ensure the
test container is always removed on script exit. Insert the line trap 'docker rm
-f "$CONTAINER_NAME" >/dev/null 2>&1' EXIT near the beginning of the script to
guarantee cleanup happens regardless of how the script terminates.

Comment on lines +1 to +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
```

#### 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) No newline at end of file
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Improve Markdown formatting and readability

  • Surround lists and headings with blank lines to satisfy lint rules (MD022, MD032).
  • Specify languages for all fenced code blocks (e.g., bash, yaml) for syntax highlighting.
  • Ensure the document ends with a single trailing newline (MD047).
🧰 Tools
🪛 LanguageTool

[uncategorized] ~226-~226: Use a comma before ‘but’ if it connects two independent clauses (unless they are closely connected and short).
Context: ...`` #### Missing Content If Ghost starts but content is missing: ```bash # Verify co...

(COMMA_COMPOUND_SENTENCE_2)


[style] ~246-~246: Consider a different adjective to strengthen your wording.
Context: ...fic issues 4. Use the debug variant for deeper investigation ## References - [Google...

(DEEP_PROFOUND)

🪛 markdownlint-cli2 (0.17.2)

8-8: Lists should be surrounded by blank lines
null

(MD032, blanks-around-lists)


15-15: Fenced code blocks should have a language specified
null

(MD040, fenced-code-language)


29-29: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


30-30: Lists should be surrounded by blank lines
null

(MD032, blanks-around-lists)


33-33: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


34-34: Lists should be surrounded by blank lines
null

(MD032, blanks-around-lists)


37-37: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


38-38: Lists should be surrounded by blank lines
null

(MD032, blanks-around-lists)


43-43: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


44-44: Fenced code blocks should be surrounded by blank lines
null

(MD031, blanks-around-fences)


48-48: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


49-49: Fenced code blocks should be surrounded by blank lines
null

(MD031, blanks-around-fences)


59-59: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


60-60: Fenced code blocks should be surrounded by blank lines
null

(MD031, blanks-around-fences)


64-64: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


65-65: Fenced code blocks should be surrounded by blank lines
null

(MD031, blanks-around-fences)


81-81: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


82-82: Lists should be surrounded by blank lines
null

(MD032, blanks-around-lists)


100-100: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


101-101: Fenced code blocks should be surrounded by blank lines
null

(MD031, blanks-around-fences)


121-121: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


122-122: Fenced code blocks should be surrounded by blank lines
null

(MD031, blanks-around-fences)


167-167: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


170-170: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


171-171: Fenced code blocks should be surrounded by blank lines
null

(MD031, blanks-around-fences)


177-177: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


178-178: Fenced code blocks should be surrounded by blank lines
null

(MD031, blanks-around-fences)


186-186: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


187-187: Fenced code blocks should be surrounded by blank lines
null

(MD031, blanks-around-fences)


193-193: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


195-195: Fenced code blocks should be surrounded by blank lines
null

(MD031, blanks-around-fences)


215-215: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


217-217: Fenced code blocks should be surrounded by blank lines
null

(MD031, blanks-around-fences)


225-225: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


227-227: Fenced code blocks should be surrounded by blank lines
null

(MD031, blanks-around-fences)


232-232: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


234-234: Fenced code blocks should be surrounded by blank lines
null

(MD031, blanks-around-fences)


243-243: Lists should be surrounded by blank lines
null

(MD032, blanks-around-lists)


253-253: Files should end with a single newline character
null

(MD047, single-trailing-newline)

🤖 Prompt for AI Agents
In v5/README-distroless.md from lines 1 to 253, improve markdown formatting by
adding blank lines before and after all lists and headings to comply with lint
rules MD022 and MD032. Specify the language for every fenced code block (such as
bash, yaml) to enable proper syntax highlighting. Finally, ensure the document
ends with exactly one trailing newline to satisfy MD047.

Comment on lines +218 to +224
# 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
```

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Grammar: Insert missing comma
In the “Missing Content” snippet, add a comma before “but”:

-If Ghost starts but content is missing:
+If Ghost starts, but content is missing:

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In v5/README-distroless.md around lines 218 to 224, the sentence in the "Missing
Content" snippet is missing a comma before the conjunction "but." Edit the
sentence to insert a comma immediately before "but" to correct the grammar.

@pascalandy pascalandy closed this Jun 13, 2025
@pascalandy pascalandy deleted the edge_18e branch June 13, 2025 15:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant