Sign inSign up

devmtl/ghostfire

By devmtl

•Updated 1 day ago

The docker image for Ghost 2 used on FirePress and play-with-ghost

Image
5

10M+

devmtl/ghostfire repository overview

 

FirePress ⁠

FirePress.org⁠ | play-with-ghost⁠ | GitHub⁠ | Twitter⁠

 

⁠ghostfire⁠

Release

Docker Pulls Docker Image Size Docker Image Version

Ghost Version Node Version Platform

GitHub last commit GitHub issues License GitHub stars


⁠What is this?

What is Ghost? — Ghost is an open-source software that lets you create your website with a blog. See the FAQ section⁠ for more details. This project makes it run in a Docker image.


⁠Breaking change in 5.130.6

This notice is for users of the stable and edge tags. edge moves to 5.130.6 when it is released. stable moves later, when the maintainer promotes it. From 5.130.6 on, the image runs Ghost on a distroless Node 22 runtime. It has no shell, no curl, and no gosu.

  • A healthcheck that runs curl inside the container fails, so the container turns unhealthy. An orchestrator that restarts unhealthy containers then restarts Ghost in a loop. Remove your curl healthcheck when you move to 5.130.6. The image has its own, described in Health check⁠
  • The image starts Ghost itself, with Node as its entrypoint. Remove any command or entrypoint override, such as node current/index.js: it fails with Cannot find module
  • docker exec <container> sh fails with executable file not found. Use docker cp to read files
  • The image starts as UID 1000 and GID 1000, never as root. The content volume must be writable by that user. Volumes made by earlier images already are. If you ran an earlier image as root with a volume owned by root, run chown -R 1000:1000 on the volume once
  • The image no longer sets a time zone. Mount /etc/localtime read-only to set one
  • The image is built for linux/amd64 only
  • Ghost 5 checks for updates by running npm -v through a shell. Without a shell, Ghost logs spawn /bin/sh ENOENT when the check is due, about once a day, and keeps serving

Ghost migrates the SQLite database the first time 5.130.6 starts on it. Back up the content directory before you update.


⁠Docker image features :

  • Ghost 5.130.6 on Node 22
  • Multi-stage build: Ghost is installed on node:22-trixie-slim, then copied into gcr.io/distroless/nodejs22-debian13:nonroot. Both base images are pinned by digest
  • No shell, no package manager, no curl, and no gosu
  • Non-root: runs as UID 1000 and GID 1000. The Ghost code is owned by root, so Ghost cannot change it
  • Read-only root filesystem: runs with --read-only and a writable /tmp
  • Built-in health check on the admin site API, under the path of the site url
  • SQLite by default, at /var/lib/ghost/content/data/ghost.db. MySQL works through the Ghost config
  • Native modules checked at build time: the build fails unless sqlite3 and sharp load
  • Labels based on the OpenContainer standard
  • linux/amd64 only

The image is about 620 MB.


⁠Release flow :

.github/workflows/release.yml runs only when a tag such as 5.130.6 is pushed. It runs one job on ubuntu-24.04, for linux/amd64, with Podman:

  1. It installs the tools pinned in mise.toml, then runs just check
  2. It logs in to Docker Hub and copies this README to the Docker Hub description, so the breaking-change notice is live before any tag moves
  3. It runs just push --immutable, which pushes the image that just check tested under its immutable tag, such as 5.130.6_1a2b3c4_2026-10-28_12H00. It stops when that tag already exists
  4. It signs the build provenance of that digest with actions/attest
  5. It runs just push --mutable, which points the version tag and edge at the same digest. It moves them only when git lists no release tag newer than this one, so a rerun of an older release publishes its immutable tag alone

A merge to master publishes nothing. The workflow never moves stable.

A fix to a base image ships as a rebuild. Dependabot opens one grouped PR a month for the base-image digests, and one for the GitHub Actions. After the base-image PR merges, push the next -N tag of the same Ghost version, such as 5.130.6-1, then 5.130.6-2.


⁠Live Demo

Want to try Ghost quickly? This is for you!

play-with-ghost.com⁠ is a playground to learn about Ghost. What's remarkable here, is that you have the option to log into the admin panel of each live demo available, by using dummy credentials.

In short, you can try Ghost on the spot without having to sign-up!


pwg-video-preview-e ⁠


⁠Continuous integration

See Github Actions sections⁠

At this point, this docker image has been pulled more than 11 millions of time!

docker-hub

⁠Quick Start

⁠Option #1 (basic run)
GHOSTFIRE_IMG="devmtl/ghostfire:stable"

docker run -d \
  --name ghostblog \
  -p 2368:2368 \
  -e url=http://localhost:2368 \
  ${GHOSTFIRE_IMG}
⁠Option #2 (production with persistent data)
GHOSTFIRE_IMG="devmtl/ghostfire:stable"

docker run -d \
  --name ghostblog \
  --read-only \
  --tmpfs /tmp \
  -p 2368:2368 \
  -e url=https://example.com \
  -v /myuser/localpath/ghost/content:/var/lib/ghost/content \
  -v /myuser/localpath/ghost/config.production.json:/var/lib/ghost/config.production.json:ro \
  -v /etc/localtime:/etc/localtime:ro \
  ${GHOSTFIRE_IMG}

The content directory must be writable by UID 1000. Ghost writes uploads and temporary files to /tmp, so a read-only container needs the /tmp tmpfs.

To configure the config.production.json refer the ghost docs⁠.

⁠Health check

The image has its own HEALTHCHECK. Every 30 seconds, it requests the admin site API, <path>ghost/api/admin/site/, where <path> is the path of the site url. It reads the site url, the admin url, and the port from the environment (url, admin__url, server__port), or else from the config, as Ghost does. It reports healthy only when Ghost answers 200. Read the status with:

docker inspect --format '{{.State.Health.Status}}' ghostblog

Do not add a health check that runs curl or a shell: the image has neither. To change the timing, set only --health-interval, --health-timeout, --health-retries, or --health-start-period, and keep the image's test. In a Compose file, leave out the healthcheck block.

⁠Tags

Each release pushes three tags:

devmtl/ghostfire:5.130.6_<sha7>_<date>   # never moves; pin this one
devmtl/ghostfire:5.130.6                 # the latest build of 5.130.6
devmtl/ghostfire:edge                    # the latest release

stable moves only when the maintainer runs just promote <tag> with the immutable tag that has run well on a real site. A -N rebuild pushes a new 5.130.6_<sha7>_<date> tag and moves 5.130.6 and edge.

Find the latest tags on DockerHub here: https://hub.docker.com/r/devmtl/ghostfire/tags/⁠

⁠Architecture & Security

⁠Multi-Stage Build Process

The Dockerfile has two stages:

  1. builder installs Ghost-CLI, then Ghost, on node:22-trixie-slim, with the build tools that sqlite3 needs. The build fails unless sqlite3 and sharp load
  2. The runtime stage copies /var/lib/ghost into gcr.io/distroless/nodejs22-debian13:nonroot, owned by root, with an empty content directory owned by UID 1000. It adds start.mjs and healthcheck.mjs from rootfs/

At start, start.mjs copies each missing directory and default theme from content.orig into content, then starts Ghost. It never overwrites what the volume holds. The default themes, casper and source, stay links into the image, so they update with it.

⁠Security Features
  • Non-root execution: runs as UID 1000 and GID 1000
  • Read-only code: the Ghost code is owned by root
  • Minimal attack surface: a distroless runtime, with no shell, no package manager, and no network tools
  • Vulnerability scanning: just scan runs Grype and fails on any Critical or High finding with a fix in the OS layer
  • Advisories: SECURITY.md⁠ lists the Ghost 5 advisories that have no 5.x fix, and how to report a problem
⁠Volume Management

Critical paths for data persistence:

/var/lib/ghost/content              # All Ghost content, themes, uploads
/var/lib/ghost/config.production.json  # Runtime configuration, mounted read-only
/var/lib/ghost/content/logs         # Application logs
/var/lib/ghost/content/data/ghost.db    # SQLite database
/tmp                                # Uploads and imports in progress; a tmpfs with --read-only

⁠DevOps best practices

Let's understand our processes. In this post « How we update hundreds of Ghost's websites on Docker Swarm?⁠ », we explain how we deploy Ghost in production and which best practices we do follow.

⁠Test the image locally

Install the pinned tools with mise install, then run just check. It lints the repo, builds the image with Podman, boots it on SQLite, and scans it with Grype. AGENTS.md⁠ lists every recipe.


⁠Developing Ghost themes locally

I open-sourced my setup here⁠. It’s a workflow to run Ghost locally within a Docker container. Once your local paths are defined, it’s enjoyable and easy to work between many themes.


⁠Configuration & Database

⁠Database Support
  • SQLite (default): Zero-configuration, file-based database at /var/lib/ghost/content/data/ghost.db
  • MySQL: Full support with connection configuration in config.production.json
⁠Configuration Management

The image's config.production.json sets the url to http://localhost:2368, listens on port 2368, uses SQLite, and keeps the content in /var/lib/ghost/content. To change it, mount your own config.production.json read-only over it.

Ghost reads environment variables before config.production.json. A variable such as url or database__client wins over the mounted config.

⁠Version Information

Current Stack:

  • Ghost: v5.130.6
  • Node.js: 22, on gcr.io/distroless/nodejs22-debian13
  • Ghost CLI: v1.34.0, in the build stage only

Check versions in a running container:

docker inspect --format '{{ index .Config.Labels "org.opencontainers.image.version" }}' <container-id>
docker exec <container-id> /nodejs/bin/node --version
⁠Migration Notes

Path consistency across versions:

- Ghost 5.x.x: /var/lib/ghost/content ✅ (current)
- Ghost 4.x.x: /var/lib/ghost/content
- Ghost 3.x.x: /var/lib/ghost/content
- Ghost 2.x.x: /var/lib/ghost/content
- Ghost 1.x.x: /var/lib/ghost/content
- Ghost 0.11.x: /var/lib/ghost (deprecated)

From an earlier image to 5.130.6, read Breaking change in 5.130.6⁠.


⁠FirePress Hosting

At FirePress we empower entrepreneurs and small organizations to create their websites on top of Ghost⁠.

At the moment, our pricing for hosting one Ghost website is $15 (Canadian dollars). This price will be only available for our first 100 new clients, starting May 1st, 2019 🙌. See our pricing section⁠ for details.

More details about this announcement⁠ on Ghost's forum.


⁠Contributing

The power of communities pull request and forks means that 1 + 1 = 3. You can help to make this repo a better one! Here is how:

  1. Fork it
  2. Create your feature branch: git checkout -b my-new-feature
  3. Commit your changes: git commit -am 'Add some feature'
  4. Push to the branch: git push origin my-new-feature
  5. Submit a pull request

Check this post for more details: Contributing to our Github project⁠. Also, by contributing you agree to the Contributor Code of Conduct on GitHub⁠. It's plain common sense really.


⁠License


⁠Sources & Fork


⁠Why all this work?

Our mission⁠ is to empower freelancers and small organizations to build an outstanding mobile-first website.

Because we believe your website should speak up in your name, we consider our mission completed once your site has become your impresario.

For more info about the man behind the startup, check out my now page⁠. You can also follow me on Twitter @askpascalandy⁠.

— The FirePress Team 🔥📰

Tag summary

Content type

Image

Digest

sha256:fe6acd0e4…

Size

166.8 MB

Last updated

1 day ago

docker pull devmtl/ghostfire:edge