- CSS 64.4%
- Handlebars 35.6%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| theme | ||
| .env.example | ||
| .gitignore | ||
| docker-compose.yml | ||
| README.md | ||
Ghost blog deployment
Note: This deployment configuration and theme were built with AI assistance (Claude). Review it yourself before relying on it - Ghost itself is not AI-generated, only the config/theme wrapping it here is.
A self-hosted Ghost instance, deployed via Docker Compose (Ghost's own recommended method), with a custom theme matching the aircraft selector / site portal design system.
Important, stated plainly: I have no Docker/Node.js runtime
available to actually run Ghost or render this theme while building
it. The docker-compose.yml matches Ghost's documented deployment
requirements, and the theme follows Ghost's documented Theme API
(Handlebars helpers, required files, package.json shape) - but
neither has been booted and visually checked against a live Ghost
instance. Test it before relying on it; if a template throws an error
on first boot, check it against
https://ghost.org/docs/themes/ first - the helper names/behavior here
are correct as documented, but Ghost's theme engine (and gscan, its
theme validator) is the actual authority, not this file.
Why a separate (sub)domain, not a sub-path
Ghost expects to own its whole domain/URL space - its own url config
setting is baked into how it builds links, RSS feeds, and canonical
URLs. Serving it at sirisyntax.com/blog (a sub-path of the portal)
needs non-trivial reverse-proxy path-rewriting and isn't how Ghost's
own docs recommend running it. Serving it at its own subdomain -
blog.sirisyntax.com, matching how acselect.sirisyntax.com already
works for the aircraft app - avoids all of that. This is what
GHOST_URL in .env.example assumes.
Deploy
mkdir -p /var/www/ghost-deploy
# copy docker-compose.yml, .env.example, and theme/ into that directory
cd /var/www/ghost-deploy
cp .env.example .env
# fill in GHOST_DB_ROOT_PASSWORD, GHOST_DB_PASSWORD (generate with:
# openssl rand -base64 24), TAILSCALE_IP (tailscale ip -4), and
# GHOST_URL
docker compose up -d
docker compose logs -f ghost # watch it come up
First boot takes a minute or two (Ghost runs its own DB migrations
against the fresh MySQL database). Once it's up, go to
https://blog.sirisyntax.com/ghost/ to create the first admin account
- this is Ghost's own login, entirely separate from the aircraft app/portal's shared accounts (see the main conversation for why that separation is real and not a shortcut I skipped).
Activate the custom theme
The theme is mounted into Ghost's content volume automatically (see
the volumes: section of docker-compose.yml), but Ghost doesn't
auto-activate a newly-appeared theme - do it manually once:
- Ghost Admin -> Settings -> Design and theme
- Find "instrument-panel" in the list, click Activate
Nginx Proxy Manager setup
Same pattern as the other two services:
- Domain Names:
blog.sirisyntax.com - Forward Hostname/IP: your Tailscale IP
- Forward Port:
2368 - SSL tab: request a new Let's Encrypt certificate, Force SSL on
- Advanced tab: same header block as the other two Proxy Hosts:
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
Don't forget the DNS A record for blog.sirisyntax.com pointing at
the droplet's public IP, same as last time.
Linking it into the portal
No code needed for the simplest version - the portal's editable menu
bar (built earlier) already does this: Admin -> Menu bar -> Add a
menu item, label "Blog", URL https://blog.sirisyntax.com. It'll
show up as a tile on the hub for whichever visibility level you pick.
Theme structure
theme/
package.json required Ghost theme metadata
default.hbs base layout - nav, footer, {{ghost_head}}/{{ghost_foot}}
index.hbs post list / homepage
post.hbs single post
error.hbs 404/error page
partials/
post-card.hbs one card in the post list
assets/
css/screen.css design tokens copied from the Flask apps' style.css
images/ flag favicon/nav icon/OG image, same files as the other two apps
Design tokens (assets/css/screen.css) are a manual copy of the
custom properties in the Flask apps' static/css/style.css - if that
palette ever changes, this file needs the same edit made by hand
alongside it. There's no build step or shared package tying them
together; that's a real maintenance seam, not an oversight, since
Ghost themes and Flask static assets don't have a natural way to share
a single source of truth without a lot more tooling than is worth it
here.
Dark/light mode follows the reader's OS-level preference
(prefers-color-scheme) only - Ghost doesn't share a JS runtime with
the other two apps, so there's no manual toggle wired up here the way
there is on those.