No description
  • CSS 64.4%
  • Handlebars 35.6%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-07-27 07:26:03 -04:00
theme Initial Commit 2026-07-27 07:26:03 -04:00
.env.example Initial Commit 2026-07-27 07:26:03 -04:00
.gitignore Initial Commit 2026-07-27 07:26:03 -04:00
docker-compose.yml Initial Commit 2026-07-27 07:26:03 -04:00
README.md Initial Commit 2026-07-27 07:26:03 -04:00

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:

  1. Ghost Admin -> Settings -> Design and theme
  2. 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.