Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 76 additions & 2 deletions v5/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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

View workflow job for this annotation

GitHub Actions / build_edge

Variables should be defined before their use

UndefinedVar: Usage of undefined variable '$BUILD_DATE' More info: https://docs.docker.com/go/dockerfile/rule/undefined-var/

Check warning on line 43 in v5/Dockerfile

View workflow job for this annotation

GitHub Actions / build_edge

Variables should be defined before their use

UndefinedVar: Usage of undefined variable '$BASE_OS' More info: https://docs.docker.com/go/dockerfile/rule/undefined-var/

Check warning on line 43 in v5/Dockerfile

View workflow job for this annotation

GitHub Actions / build_edge

Variables should be defined before their use

UndefinedVar: Usage of undefined variable '$VCS_REF' More info: https://docs.docker.com/go/dockerfile/rule/undefined-var/
org.opencontainers.image.vendor="https://firepress.org/" \
org.opencontainers.image.created="${BUILD_DATE}" \
org.opencontainers.image.revision="${VCS_REF}" \
Expand All @@ -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

View workflow job for this annotation

GitHub Actions / build_edge

Legacy key/value format with whitespace separator should not be used

LegacyKeyValueFormat: "ENV key=value" should be used instead of legacy "ENV key value" format More info: https://docs.docker.com/go/dockerfile/rule/legacy-key-value-format/
RUN set -eux; \
apk add --no-cache --virtual .gosu-deps \
ca-certificates \
Expand Down Expand Up @@ -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"; \
Comment on lines +185 to +205
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.

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

Expand All @@ -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
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.


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"]
253 changes: 253 additions & 0 deletions v5/README-distroless.md
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
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.

#### 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
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.

Loading
Loading