D4rk Notes
How I moved D4rk Notes from a custom Next.js/MDX site to Ghost 6 with MySQL, Docker Compose, persistent content storage, and Resend SMTP.
Moving the Blog to Ghost and MySQL
D4rk Notes has moved from the original Next.js/MDX publishing setup to Ghost backed by MySQL. I wanted a straightforward publishing workflow, a theme I can control, and persistent storage for both posts and uploads. This is how the current deployment is put together.
The stack
The site runs as two Docker Compose services:
- Ghost 6 serves the blog and admin interface.
- MySQL 8.0.44 stores Ghost’s content database.
- Traefik routes the public hostname to Ghost over the shared
proxyDocker network. - Resend SMTP handles outbound mail, including Ghost notifications.
The public URL is https://notes.d4rkcyber.com. Ghost is configured with that canonical URL so generated links and redirects use HTTPS. The Ghost container is attached to the external proxy network and a separate internal backend network. MySQL is only on backend, so it is not exposed through the reverse proxy or a published host port.
Persistent data and theme
Ghost’s content directory is bind-mounted to persistent storage. That directory contains uploaded images and other content files. The D4rk Notes theme is mounted separately into Ghost’s themes directory, which lets me edit the theme on the host without baking it into a new image.
MySQL has its own persistent data directory. The database and Ghost content uploads are separate, so both need to be included in backups. A database dump alone would not preserve uploaded images; copying only the content directory would not preserve posts and settings stored in MySQL.
Startup and health
Compose waits for MySQL’s health check before starting Ghost. The health check uses mysqladmin ping, which avoids bringing Ghost up while the database is still initializing. Both services restart unless stopped, and the database image is pinned to a specific MySQL 8.0.44 digest for repeatable deployments.
Ghost sends mail through Resend’s SMTP endpoint using STARTTLS on port 587. The sender address and SMTP credentials are configured through environment variables. The actual SMTP password/API key belongs in the server’s private .env file and should never be committed or copied into a public Compose example.
Compose file
This is the deployment structure. The .env file supplies database credentials, storage locations, the public domain, and mail settings. The proxy network must already exist. No host ports: mapping is needed for Ghost when Traefik shares that network.
services:
ghost:
image: ghost:${GHOST_VERSION:-6-alpine}
container_name: d4rknotes
restart: unless-stopped
env_file:
- .env
environment:
NODE_ENV: production
url: https://${DOMAIN}
database__client: mysql
database__connection__host: d4rknotes-ghost-mysql
database__connection__user: ${DATABASE_USER}
database__connection__password: ${DATABASE_PASSWORD}
database__connection__database: ghost
volumes:
- ${UPLOAD_LOCATION:-./data/ghost}:/var/lib/ghost/content
- ./themes/d4rknotes:/var/lib/ghost/content/themes/d4rknotes
depends_on:
db:
condition: service_healthy
networks:
- proxy
- backend
db:
image: mysql:8.0.44@sha256:f37951fc3753a6a22d6c7bf6978c5e5fefcf6f31814d98c582524f98eae52b21
container_name: ghost_db
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: ${DATABASE_ROOT_PASSWORD}
MYSQL_USER: ${DATABASE_USER}
MYSQL_PASSWORD: ${DATABASE_PASSWORD}
MYSQL_DATABASE: ghost
volumes:
- ${MYSQL_DATA_LOCATION}:/var/lib/mysql
healthcheck:
test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 -p\"$$MYSQL_ROOT_PASSWORD\""]
interval: 10s
timeout: 5s
start_period: 30s
retries: 12
networks:
backend:
aliases:
- d4rknotes-ghost-mysql
networks:
proxy:
external: true
name: proxy
backend:
internal: true
The .env file is intentionally not included. At minimum it needs values for DOMAIN, DATABASE_USER, DATABASE_PASSWORD, DATABASE_ROOT_PASSWORD, MYSQL_DATA_LOCATION, UPLOAD_LOCATION, and the Ghost mail settings. Keep strong, unique database passwords and back up both persistent data locations.
Why Ghost
The original site used a custom Next.js and MDX content pipeline. Ghost replaces that with a purpose-built publishing system and a MySQL-backed admin workflow, while the custom theme keeps the site’s presentation under my control. The result is less bespoke publishing infrastructure to maintain and a clearer separation between the application, database, uploads, and theme.