Skip to content
Open
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
4 changes: 4 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.git
.gitignore
**/.DS_Store
**/node_modules
44 changes: 41 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,45 @@
# Ghost Docker
# Ghost Docker for Coolify

Configuration to run Ghost and its services with Docker Compose

# Copyright & License
- [docker-compose.5.yml] - Original Coolify 5 Ghost configuration available from coolify as a template (for reference / backwards compatibility)

Copyright (c) 2013-2026 Ghost Foundation - Released under the [MIT license](LICENSE).
- [docker-compose.6.local.yml] - Modifications that allow running ghost 6 (with tinybird analytics and wizard) locally. `docker compose up -f coolify/docker-compose.6.yml -f coolify/docker-compose.6.local.yml`

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Docker Compose CLI -f flag order before or after subcommand syntax

💡 Result:

In Docker Compose V2 (the docker compose CLI), global options like -f/--file come before the subcommand. The documented option syntax is docker compose [OPTIONS] COMMAND [ARG...], and examples show -f placed before commands such as run (e.g., docker compose -f compose.yaml -f compose.admin.yaml run backup_db). [1][2] So for your question (“-f flag order before or after subcommand syntax”): put -f before the subcommand (the command like up, run, ps, etc.). Also note: Compose supports specifying multiple compose files by repeating -f; later files override earlier ones, and Compose “builds the configuration in the order you supply the files.” [1]

Citations:


Fix the docker compose command option order.

In Docker Compose V2, global options like -f must come before the subcommand. The current command on line 7 won't work. Reorder to: docker compose -f coolify/docker-compose.6.yml -f coolify/docker-compose.6.local.yml up

Suggested fix
-- [docker-compose.6.local.yml] - Modifications that allow running ghost 6 (with tinybird analytics and wizard) locally. `docker compose up -f coolify/docker-compose.6.yml -f coolify/docker-compose.6.local.yml`
+- [docker-compose.6.local.yml] - Modifications that allow running ghost 6 (with tinybird analytics and wizard) locally. `docker compose -f coolify/docker-compose.6.yml -f coolify/docker-compose.6.local.yml up`
📝 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-compose.6.local.yml] - Modifications that allow running ghost 6 (with tinybird analytics and wizard) locally. `docker compose up -f coolify/docker-compose.6.yml -f coolify/docker-compose.6.local.yml`
- [docker-compose.6.local.yml] - Modifications that allow running ghost 6 (with tinybird analytics and wizard) locally. `docker compose -f coolify/docker-compose.6.yml -f coolify/docker-compose.6.local.yml up`
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` at line 7, The docker compose command in the README uses
subcommand-first option order and will fail with Compose V2; update the command
string that currently reads "docker compose up -f coolify/docker-compose.6.yml
-f coolify/docker-compose.6.local.yml" so that the global -f flags precede the
subcommand, e.g. reorder to "docker compose -f coolify/docker-compose.6.yml -f
coolify/docker-compose.6.local.yml up" wherever that command appears in the
README (the diff line with the compose command).


- [docker-compose.6.yml] - For use with coolify, see setup below.

Comment on lines +5 to +10

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Convert bracketed filenames to real Markdown links or code spans.

Line 5, Line 7, and Line 9 use [... ] without link definitions, so they won’t render as usable links.

Suggested fix
-- [docker-compose.5.yml] - Original Coolify 5 Ghost configuration available from coolify as a template (for reference / backwards compatibility)
+- [`coolify/docker-compose.5.yml`](coolify/docker-compose.5.yml) - Original Coolify 5 Ghost configuration available from coolify as a template (for reference / backwards compatibility)

-- [docker-compose.6.local.yml] - Modifications that allow running ghost 6 (with tinybird analytics and wizard) locally. `docker compose -f coolify/docker-compose.6.yml -f coolify/docker-compose.6.local.yml up`
+- [`coolify/docker-compose.6.local.yml`](coolify/docker-compose.6.local.yml) - Modifications that allow running ghost 6 (with tinybird analytics and wizard) locally. `docker compose -f coolify/docker-compose.6.yml -f coolify/docker-compose.6.local.yml up`

-- [docker-compose.6.yml] - For use with coolify, see setup below.
+- [`coolify/docker-compose.6.yml`](coolify/docker-compose.6.yml) - For use with coolify, see setup below.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` around lines 5 - 10, The three entries using bare bracketed
filenames (docker-compose.5.yml, docker-compose.6.local.yml,
docker-compose.6.yml) are not valid Markdown links; update those list items to
either use inline code spans (e.g., `docker-compose.5.yml`) or real Markdown
links pointing to the file/URL (e.g.,
[docker-compose.5.yml](./coolify/docker-compose.5.yml)) so they render
correctly; edit the three list lines (the entries referencing
docker-compose.5.yml, docker-compose.6.local.yml, and docker-compose.6.yml) to
apply the chosen format consistently and preserve the existing explanatory text.

## Ghost 6 with Tinybird Analytics

### New Install

Requirements: Coolify UI v4, host with at least 5GB disk space and 2GB RAM

Primary purpose was to make it easy to configure a new / existing ghost 6 instance to run from coolify, still requires tinybird at this time, but there is a useful setup tool built in (Wizard) which also functions as a gateway after configuration.

1. In Coolify UI Project: Add Resource
2. As Public Repository: https://github.com/BadPirate/ghost-docker.git
3. Select Build Pack: "Docker Compose"
4. Docker Compose Location: `/coolify/docker-compose.6.yml`
Comment on lines +20 to +22

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use the canonical repository URL in deployment steps.

Line 20 and Line 32 currently point users to a personal fork (BadPirate/ghost-docker). For official docs, this should reference TryGhost/ghost-docker to avoid supply-chain and maintenance drift.

Also applies to: 32-34

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` around lines 20 - 22, Replace the personal fork URL with the
canonical repository in the README: locate the occurrences of the string
"https://github.com/BadPirate/ghost-docker.git" (used in the "As Public
Repository" / deployment steps and the repeated block around lines referencing
Docker Compose) and change them to
"https://github.com/TryGhost/ghost-docker.git"; ensure both instances (the one
after "As Public Repository" and the duplicate near "Docker Compose Location")
are updated so all deployment instructions point to TryGhost/ghost-docker.

5. General -> Domains for wizard: https://<your coolify domain>:3989
6. Deploy
7. After deployment, visit (From links for ghost-gate or at the URL) and configure tinybird
8. Stop deployment, paste the tinybird config variables into Environment section of your deployment and relaunch
9. Profit! (?)

### Upgrade existing coolify ghost 5 template based install

1. In Coolify UI Project: Add Resource
2. As Public Repository: https://github.com/BadPirate/ghost-docker.git
3. Select Build Pack: "Docker Compose"
4. Docker Compose Location: `/coolify/docker-compose.6.yml`
5. General -> Domains for wizard: https://<your coolify domain>:3989
6. Deploy
7. Stop the deployment after it successfully opens
8. On your server, cd into docker volumes `/var/lib/docker/volumes/` find the app ID for your old version ghost blog, and the app ID for the new version, and replace new empty content with existing older content:
- `rm -rf ${NEWID}_ghost-content-data; cp -r ${OLDID}_ghost-content-data ${NEWID}_ghost-content-data`
- `rm -rf ${NEWID}_ghost-mysql-data; cp -r ${OLDID}_ghost-mysql-data ${NEWID}_ghost-mysql-data`
9. In Coolify UI, copy the environment variables (including passwords for mysql) from the old template based app and put them into your new docker compose based app
10. Deploy again, ghost-gate will migrate your database
11. After deployment, visit (From links for ghost-gate or at the URL) and configure tinybird
12. Stop deployment, paste the tinybird config variables into Environment section of your deployment and relaunch
13. Profit! (?)
45 changes: 45 additions & 0 deletions coolify/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Ghost Docker for Coolify

Configuration to run Ghost and its services with Docker Compose

- [docker-compose.5.yml] - Original Coolify 5 Ghost configuration available from coolify as a template (for reference / backwards compatibility)

- [docker-compose.6.local.yml] - Modifications that allow running ghost 6 (with tinybird analytics and wizard) locally. `docker compose up -f coolify/docker-compose.6.yml -f coolify/docker-compose.6.local.yml`

- [docker-compose.6.yml] - Ghost 6 + Tinybird for analytics + router and wizard to ease in setup. For use with coolify, see setup below.

## Ghost 6 with Tinybird Analytics

### New Install

Requirements: Coolify UI v4, host with at least 5GB disk space and 2GB RAM

Primary purpose was to make it easy to configure a new / existing ghost 6 instance to run from coolify, still requires tinybird at this time, but there is a useful setup tool built in (Wizard) which also functions as a gateway after configuration.

1. In Coolify UI Project: Add Resource
2. As Public Repository: https://github.com/BadPirate/ghost-docker.git
3. Select Build Pack: "Docker Compose"
4. Docker Compose Location: `/coolify/docker-compose.6.yml`
5. General -> Domains for wizard: https://<your coolify domain>:3989
6. Deploy
7. After deployment, visit (From links for ghost-gate or at the URL) and configure tinybird
8. Stop deployment, paste the tinybird config variables into Environment section of your deployment and relaunch
9. Profit! (?)
Comment on lines +13 to +27

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Document required SMTP/mail env vars before final deploy.

Please add a short “production checklist” note requiring MAIL_FROM and SMTP auth/host values before go-live; otherwise admin/system emails can silently fail after migration.

Based on learnings: "Email configuration is critical even without newsletter features as it is used for admin notifications".

Also applies to: 41-45

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@coolify/README.md` around lines 13 - 27, Add a short "Production checklist"
note under the "New Install" section that explicitly lists required SMTP/mail
environment variables (at minimum MAIL_FROM plus SMTP_HOST, SMTP_PORT,
SMTP_USER, SMTP_PASSWORD or equivalent auth vars) and instructs users to set
them in the Environment before final deploy to ensure admin/system emails work;
apply the same checklist note to the other deployment/production guidance block
already present elsewhere in the README so both locations warn about email
config prior to go‑live.


### Upgrade existing coolify ghost 5 template based install

1. In Coolify UI Project: Add Resource
2. As Public Repository: https://github.com/BadPirate/ghost-docker.git
3. Select Build Pack: "Docker Compose"
4. Docker Compose Location: `/coolify/docker-compose.6.yml`
5. General -> Domains for wizard: https://<your coolify domain>:3989
6. Deploy
7. Stop the deployment after it successfully opens
8. On your server, cd into docker volumes `/var/lib/docker/volumes/` find the app ID for your old version ghost blog, and the app ID for the new version, and replace new empty content with existing older content:
- `rm -rf ${NEWID}_ghost-content-data; cp -r ${OLDID}_ghost-content-data ${NEWID}_ghost-content-data`
- `rm -rf ${NEWID}_ghost-mysql-data; cp -r ${OLDID}_ghost-mysql-data ${NEWID}_ghost-mysql-data`
Comment on lines +38 to +40

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Guard destructive volume-copy commands with variable checks.

The current rm -rf ...; cp -r ... sequence can delete the wrong volume if NEWID/OLDID is empty or mistyped. Please add explicit validation and fail-fast checks before removal/copy.

Suggested safer command block
-  - `rm -rf ${NEWID}_ghost-content-data; cp -r ${OLDID}_ghost-content-data ${NEWID}_ghost-content-data`
-  - `rm -rf ${NEWID}_ghost-mysql-data; cp -r ${OLDID}_ghost-mysql-data ${NEWID}_ghost-mysql-data`
+  - `test -n "${OLDID}" && test -n "${NEWID}" || (echo "OLDID/NEWID required" && exit 1)`
+  - `test "${OLDID}" != "${NEWID}" || (echo "OLDID and NEWID must differ" && exit 1)`
+  - `rm -rf "/var/lib/docker/volumes/${NEWID}_ghost-content-data" && cp -a "/var/lib/docker/volumes/${OLDID}_ghost-content-data" "/var/lib/docker/volumes/${NEWID}_ghost-content-data"`
+  - `rm -rf "/var/lib/docker/volumes/${NEWID}_ghost-mysql-data" && cp -a "/var/lib/docker/volumes/${OLDID}_ghost-mysql-data" "/var/lib/docker/volumes/${NEWID}_ghost-mysql-data"`
📝 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
8. On your server, cd into docker volumes `/var/lib/docker/volumes/` find the app ID for your old version ghost blog, and the app ID for the new version, and replace new empty content with existing older content:
- `rm -rf ${NEWID}_ghost-content-data; cp -r ${OLDID}_ghost-content-data ${NEWID}_ghost-content-data`
- `rm -rf ${NEWID}_ghost-mysql-data; cp -r ${OLDID}_ghost-mysql-data ${NEWID}_ghost-mysql-data`
8. On your server, cd into docker volumes `/var/lib/docker/volumes/` find the app ID for your old version ghost blog, and the app ID for the new version, and replace new empty content with existing older content:
- `test -n "${OLDID}" && test -n "${NEWID}" || (echo "OLDID/NEWID required" && exit 1)`
- `test "${OLDID}" != "${NEWID}" || (echo "OLDID and NEWID must differ" && exit 1)`
- `rm -rf "/var/lib/docker/volumes/${NEWID}_ghost-content-data" && cp -a "/var/lib/docker/volumes/${OLDID}_ghost-content-data" "/var/lib/docker/volumes/${NEWID}_ghost-content-data"`
- `rm -rf "/var/lib/docker/volumes/${NEWID}_ghost-mysql-data" && cp -a "/var/lib/docker/volumes/${OLDID}_ghost-mysql-data" "/var/lib/docker/volumes/${NEWID}_ghost-mysql-data"`
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@coolify/README.md` around lines 38 - 40, The shell commands that run `rm -rf
${NEWID}_ghost-content-data; cp -r ${OLDID}_ghost-content-data
${NEWID}_ghost-content-data` are unsafe if NEWID or OLDID are empty or
identical; add explicit validation before any destructive action: check that
NEWID and OLDID are non-empty, not equal, and that the source directories
(referenced by ${OLDID}_ghost-content-data and ${OLDID}_ghost-mysql-data) exist
and are directories; fail fast with a clear error message if any check fails (do
not proceed to rm/cp), and ensure all variable expansions are quoted to avoid
globbing/word-splitting and consider adding a dry-run or confirmation step
before performing the rm -rf on the target directories.

9. In Coolify UI, copy the environment variables (including passwords for mysql) from the old template based app and put them into your new docker compose based app
10. Deploy again, ghost-gate will migrate your database
11. After deployment, visit (From links for ghost-gate or at the URL) and configure tinybird
12. Stop deployment, paste the tinybird config variables into Environment section of your deployment and relaunch
13. Profit! (?)
5 changes: 5 additions & 0 deletions coolify/deploy_local.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/usr/bin/env bash

COOLIFY_DIR=$(dirname $0)

docker compose --project-directory $COOLIFY_DIR/.. -f $COOLIFY_DIR/docker-compose.6.yml -f $COOLIFY_DIR/docker-compose.6.local.yml up $@
Comment on lines +3 to +5

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Quote path/arg expansions to avoid shell splitting bugs.

$0, $COOLIFY_DIR, and $@ should be quoted; otherwise paths/args with spaces or globs are mishandled.

Suggested patch
 #!/usr/bin/env bash
+set -euo pipefail
 
-COOLIFY_DIR=$(dirname $0)
+COOLIFY_DIR="$(dirname "$0")"
 
-docker compose --project-directory $COOLIFY_DIR/.. -f $COOLIFY_DIR/docker-compose.6.yml -f $COOLIFY_DIR/docker-compose.6.local.yml up $@
+docker compose \
+  --project-directory "$COOLIFY_DIR/.." \
+  -f "$COOLIFY_DIR/docker-compose.6.yml" \
+  -f "$COOLIFY_DIR/docker-compose.6.local.yml" \
+  up "$@"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@coolify/deploy_local.sh` around lines 3 - 5, The script fails to quote
path/arg expansions; change COOLIFY_DIR assignment to use dirname "$0" (e.g.,
COOLIFY_DIR=$(dirname "$0")) and quote usages in the docker compose invocation:
--project-directory "$COOLIFY_DIR/.." and -f "$COOLIFY_DIR/docker-compose.6.yml"
-f "$COOLIFY_DIR/docker-compose.6.local.yml" and pass arguments as "$@" so
spaces/globs are preserved; update the lines referencing COOLIFY_DIR and the
docker compose command accordingly.

54 changes: 54 additions & 0 deletions coolify/docker-compose.5.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
---
# Legacy Ghost 5 Coolify template, kept for migrations from the old built-in Ghost
# service in Coolify. No analytics, no ActivityPub, no ghost-gate. For a new install
# prefer coolify/docker-compose.6.yml. See coolify/README.md.
services:
ghost:
image: 'ghost:5'
volumes:
- 'ghost-content-data:/var/lib/ghost/content'
environment:
- SERVICE_URL_GHOST_2368
- url=$SERVICE_URL_GHOST
- database__client=mysql
- database__connection__host=mysql
- database__connection__user=$SERVICE_USER_MYSQL
- database__connection__password=$SERVICE_PASSWORD_MYSQL
- 'database__connection__database=${MYSQL_DATABASE-ghost}'
- mail__transport=SMTP
- 'mail__options__auth__pass=${MAIL_OPTIONS_AUTH_PASS}'
- 'mail__options__auth__user=${MAIL_OPTIONS_AUTH_USER}'
- 'mail__options__secure=${MAIL_OPTIONS_SECURE:-true}'
- 'mail__options__port=${MAIL_OPTIONS_PORT:-465}'
- 'mail__options__service=${MAIL_OPTIONS_SERVICE:-Mailgun}'
- 'mail__options__host=${MAIL_OPTIONS_HOST}'
depends_on:
mysql:
condition: service_healthy
healthcheck:
test:
- CMD
- echo
- ok
Comment on lines +28 to +32

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Replace placeholder Ghost healthcheck with a real probe.

echo ok always passes, so unhealthy Ghost instances are still marked healthy.

Suggested patch
     healthcheck:
       test:
-        - CMD
-        - echo
-        - ok
+        - CMD-SHELL
+        - wget -q -S -T 3 --spider http://127.0.0.1:2368/ 2>&1 | grep -q 'HTTP/'
       interval: 5s
       timeout: 20s
       retries: 10
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@coolify/docker-compose.5.yml` around lines 24 - 28, The healthcheck currently
uses the placeholder command shown in the healthcheck -> test -> CMD -> echo ok,
which always returns success; replace that test with a real HTTP probe against
Ghost (e.g., curl --fail http://localhost:2368/ or the admin endpoint /ghost/)
and add sensible timing settings. Update the healthcheck.test to use CMD curl
--fail --silent --show-error http://localhost:2368/ || exit 1 (or wget
--spider), and add healthcheck.interval, healthcheck.timeout, and
healthcheck.retries entries to ensure the container is marked unhealthy on real
failures.

interval: 5s
timeout: 20s
retries: 10
mysql:
image: 'mysql:8.0'
volumes:
- 'ghost-mysql-data:/var/lib/mysql'
environment:
- 'MYSQL_USER=${SERVICE_USER_MYSQL}'
- 'MYSQL_PASSWORD=${SERVICE_PASSWORD_MYSQL}'
- 'MYSQL_DATABASE=${MYSQL_DATABASE}'
- 'MYSQL_ROOT_PASSWORD=${SERVICE_PASSWORD_MYSQLROOT}'
healthcheck:
test:
- CMD
- mysqladmin
- ping
- '-h'
- 127.0.0.1
interval: 5s
timeout: 20s
retries: 10
10 changes: 10 additions & 0 deletions coolify/docker-compose.6.local.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
services:
ghost-gate:
ports:
- "3989:3989"
environment:
SERVICE_URL_GHOST_GATE_3989:
SERVICE_URL_GHOST_GATE: http://localhost:3989
MYSQL_USER: ${SERVICE_USER_MYSQL:-localuser}
MYSQL_PASSWORD: ${SERVICE_PASSWORD_MYSQL:-changeme}
MYSQL_ROOT_PASSWORD: ${SERVICE_PASSWORD_MYSQLROOT:-changemeroot}
196 changes: 196 additions & 0 deletions coolify/docker-compose.6.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
services:
ghost-gate:
build:
context: .
dockerfile: ghost-gate/Dockerfile
args:
GHOST_VERSION: ${GHOST_VERSION:-6-alpine}
depends_on:
mysql:
condition: service_healthy
environment:
SERVICE_URL_GHOST_GATE_3989:
SERVICE_URL_GHOST_GATE:
PROXY_GHOST_UPSTREAM: http://ghost:2368
PROXY_ANALYTICS_UPSTREAM: http://traffic-analytics:3000
PROXY_ACTIVITYPUB_UPSTREAM: http://activitypub:8080
MYSQL_HOST: mysql
MYSQL_PORT: "3306"
MYSQL_ROOT_PASSWORD: ${SERVICE_PASSWORD_MYSQLROOT}
MYSQL_USER: ${SERVICE_USER_MYSQL}
MYSQL_PASSWORD: ${SERVICE_PASSWORD_MYSQL}
MYSQL_MULTIPLE_DATABASES: ${MYSQL_MULTIPLE_DATABASES:-activitypub}
MAIL_OPTIONS_AUTH_PASS: ${MAIL_OPTIONS_AUTH_PASS:-mailgun_smtp_user_password}
MAIL_OPTIONS_AUTH_USER: ${MAIL_OPTIONS_AUTH_USER:-local@localhost}
MAIL_OPTIONS_PORT: ${MAIL_OPTIONS_PORT:-465}
MAIL_OPTIONS_SECURE: ${MAIL_OPTIONS_SECURE:-true}
MAIL_OPTIONS_SERVICE: ${MAIL_OPTIONS_SERVICE:-Mailgun}
MAIL_OPTIONS_HOST: ${MAIL_OPTIONS_HOST:-smtp.mailgun.org}
# ghost-gate serves the setup page until all four TINYBIRD_* vars are set; after
# that it switches to reverse-proxy mode on next start.
TINYBIRD_API_URL: ${TINYBIRD_API_URL:-}
TINYBIRD_WORKSPACE_ID: ${TINYBIRD_WORKSPACE_ID:-}
TINYBIRD_ADMIN_TOKEN: ${TINYBIRD_ADMIN_TOKEN:-}
TINYBIRD_TRACKER_TOKEN: ${TINYBIRD_TRACKER_TOKEN:-}
expose:
- "3989"
restart: unless-stopped
healthcheck:
test:
- "CMD-SHELL"
- "wget -q -S -T 3 --spider http://127.0.0.1:3989/__ghost_gate/health 2>&1 | grep -q 'HTTP/1.1 200'"
interval: 5s
timeout: 5s
retries: 24
start_period: 30s

ghost:
image: ghost:${GHOST_VERSION:-6-alpine}
environment:
url: ${SERVICE_URL_GHOST_GATE}
NODE_ENV: production
admin__url: ${SERVICE_URL_GHOST_GATE}
database__client: mysql
database__connection__host: mysql
database__connection__user: ${SERVICE_USER_MYSQL}
database__connection__password: ${SERVICE_PASSWORD_MYSQL}
database__connection__database: ${MYSQL_DATABASE-ghost}
mail__transport: SMTP
mail__from: ${MAIL_FROM}
mail__options__auth__pass: ${MAIL_OPTIONS_AUTH_PASS}
mail__options__auth__user: ${MAIL_OPTIONS_AUTH_USER}
mail__options__secure: ${MAIL_OPTIONS_SECURE:-true}
mail__options__port: ${MAIL_OPTIONS_PORT:-465}
mail__options__service: ${MAIL_OPTIONS_SERVICE:-Mailgun}
mail__options__host: ${MAIL_OPTIONS_HOST}
Comment on lines +59 to +65

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Make MAIL_FROM explicit for production deployments.

mail__from is currently optional/empty. Add a required expansion (${MAIL_FROM:?...}) or document it as mandatory so admin/system email flows are not silently broken.

Based on learnings: "Email configuration is critical even without newsletter features as it is used for admin notifications".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@coolify/docker-compose.6.yml` around lines 58 - 64, The mail__from setting is
currently optional and can be empty, so update the docker-compose variable
expansion for mail__from to make MAIL_FROM required (e.g., use a required
expansion pattern) and/or add an explicit validation/documentation step so
deployments fail fast when MAIL_FROM is unset; target the mail__from key and
related env var names (MAIL_FROM, mail__options__auth__user,
mail__options__auth__pass) and ensure the compose config enforces MAIL_FROM
presence or the README/startup checks clearly mark MAIL_FROM as mandatory for
production admin email flows.

# Same-origin tracker (no CORS). ghost-gate strips /.ghost/analytics and forwards
# to traffic-analytics:3000/api/v1/page_hit.
tinybird__tracker__endpoint: ${SERVICE_URL_GHOST_GATE}/.ghost/analytics/api/v1/page_hit
labs__publicAPI: "true"
tinybird__adminToken: ${TINYBIRD_ADMIN_TOKEN}
tinybird__workspaceId: ${TINYBIRD_WORKSPACE_ID}
tinybird__tracker__datasource: analytics_events
# Admin/stats read from Tinybird API directly (not via traffic-analytics).
tinybird__stats__endpoint: ${TINYBIRD_API_URL:-https://api.tinybird.co}
volumes:
- 'ghost-content-data:/var/lib/ghost/content'
depends_on:
mysql:
condition: service_healthy
ghost-gate:
condition: service_healthy
activitypub:
condition: service_started
required: false
# Ghost redirects `/` to its public URL, so `wget --spider` follows the redirect
# out of the container. Instead, treat "server answered with any HTTP status line"
# as alive (busybox wget prints `HTTP/1.1 …` on stderr with -S even for redirects).
healthcheck:
test:
- "CMD-SHELL"
- "wget -q -S -T 3 --spider http://127.0.0.1:2368/ 2>&1 | grep -q 'HTTP/'"
interval: 10s
timeout: 5s
retries: 18
start_period: 120s

mysql:
image: mysql:8.0.44@sha256:f37951fc3753a6a22d6c7bf6978c5e5fefcf6f31814d98c582524f98eae52b21
restart: always
environment:
MYSQL_USER: ${SERVICE_USER_MYSQL}
MYSQL_PASSWORD: ${SERVICE_PASSWORD_MYSQL}
MYSQL_DATABASE: ${MYSQL_DATABASE}
MYSQL_ROOT_PASSWORD: ${SERVICE_PASSWORD_MYSQLROOT}
volumes:
- 'ghost-mysql-data:/var/lib/mysql'
healthcheck:
test: mysqladmin ping -p$$MYSQL_ROOT_PASSWORD -h 127.0.0.1
interval: 1s
start_period: 30s
start_interval: 5s
timeout: 20s
retries: 10

traffic-analytics:
image: ghost/traffic-analytics:1.0.196@sha256:f93abf4134db41d94d90bb0ef6e4fcc67dfd91162f6ad15423c51e3c49f9c3a7
restart: always
depends_on:
ghost-gate:
condition: service_healthy
expose:
- "3000"
volumes:
- traffic_analytics_data:/data
environment:
NODE_ENV: production
# Behind Traefik/Coolify; client IP from X-Forwarded-For (see TryGhost/TrafficAnalytics).
TRUST_PROXY: "true"
PROXY_TARGET: ${TINYBIRD_API_URL:-https://api.tinybird.co}/v0/events
SALT_STORE_TYPE: ${SALT_STORE_TYPE:-file}
SALT_STORE_FILE_PATH: /data/salts.json
TINYBIRD_TRACKER_TOKEN: ${TINYBIRD_TRACKER_TOKEN:-}
LOG_LEVEL: debug
healthcheck:
test:
- "CMD-SHELL"
- "wget -q -S -T 3 --spider http://127.0.0.1:3000/ 2>&1 | grep -q 'HTTP/'"
interval: 10s
timeout: 5s
retries: 6
start_period: 15s

activitypub:
image: ghcr.io/tryghost/activitypub:1.2.2@sha256:128f0d08d872930b4ab37c9fc1fe8042fefd44622316b05f3885bd068be7cc43
restart: always
expose:
- "8080"
volumes:
- 'ghost-content-data:/opt/activitypub/content'
environment:
NODE_ENV: production
MYSQL_HOST: mysql
MYSQL_USER: ${SERVICE_USER_MYSQL}
MYSQL_PASSWORD: ${SERVICE_PASSWORD_MYSQL}
MYSQL_DATABASE: activitypub
LOCAL_STORAGE_PATH: /opt/activitypub/content/images/activitypub
LOCAL_STORAGE_HOSTING_URL: ${SERVICE_URL_GHOST_GATE}/content/images/activitypub
depends_on:
mysql:
condition: service_healthy
activitypub-migrate:
condition: service_completed_successfully
# activitypub (alpine + BusyBox wget) has no documented health URL; accept any HTTP
# response on :8080 as "process listening".
healthcheck:
test:
- "CMD-SHELL"
- "wget -q -S -T 3 --spider http://127.0.0.1:8080/ 2>&1 | grep -q 'HTTP/'"
interval: 10s
timeout: 5s
retries: 6
start_period: 30s

activitypub-migrate:
image: ghcr.io/tryghost/activitypub-migrations:1.2.2@sha256:2af8a0726ac4362cdcab59c308ed612140478d43011ec8d3475bb2634b96d108
environment:
MYSQL_DB: mysql://${SERVICE_USER_MYSQL}:${SERVICE_PASSWORD_MYSQL}@tcp(mysql:3306)/activitypub
depends_on:
mysql:
condition: service_healthy
ghost-gate:
condition: service_healthy
restart: no
# One-shot job with no HTTP server; report healthy unconditionally. Downstream
# services gate on service_completed_successfully, not service_healthy.
healthcheck:
test: ["CMD-SHELL", "true"]
interval: 10s
timeout: 3s
retries: 1
start_period: 5s

volumes:
ghost-content-data:
ghost-mysql-data:
traffic_analytics_data:
2 changes: 2 additions & 0 deletions ghost-gate/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node_modules
npm-debug.log
Loading