Ghost

Publishing platform for blogs, newsletters and memberships

Ghost on Cubeship

Ghost is an open-source publishing platform: a website, a blog and a newsletter in one, with paid memberships built in.

This template installs it on a Cubeship instance with the managed MySQL it needs, and a volume for everything it keeps in files.

What it creates

  • ghost — Ghost, from ghost:6.63.0, answering on the domain you choose, with a volume at /var/lib/ghost/content: uploaded images and files, themes, and the routes and redirects files.
  • ghost-db — a managed MySQL 8.0 database, attached to the app. Posts, members, staff and settings are in it.

It needs Cubeship 0.7.0 or newer.

What you are asked

InputWhat to give
Where the site answersA domain you control, pointed at your instance.
Your SMTP serverThe host your mail provider gives you, like smtp.mailgun.org.
Its TLS port465 unless your provider says otherwise.
The SMTP usernameFrom your mail provider.
The SMTP passwordFrom your mail provider.
The address Ghost sends fromAn address your provider lets you send as.

Mail is not optional. Ghost emails a code whenever a staff member signs in from a device it has not seen, and the same mail carries staff invites and password resets. Without it, the first sign-in works and the next one on a new browser does not.

Ghost connects to port 465 with TLS from the start. A provider that only offers STARTTLS on 587: set the port to 587, then change mail__options__secure to false on the ghost app and redeploy.

This is transactional mail only. Newsletters go out through Mailgun, configured separately in Ghost's settings.

After installing

  1. Open https://<your domain>/ghost straight away. Ghost has no default account: the first person to open it creates the owner. Until you do, that is anyone who finds the domain.
  2. Before signing out, sign in again from a private window. Ghost emails a code to the owner's address; if it never arrives, fix the SMTP settings while you still have a session.

The SMTP settings are variables on the ghost app. Change them and redeploy.

What does not work here

  • Social web (ActivityPub). Ghost expects /.ghost/activitypub/ on its domain to reach a separate service, and a domain on Cubeship sends every path to the one app. It is on by default: turn Social web off in Ghost's settings so Ghost stops trying.
  • Web analytics. Ghost's built-in traffic analytics need a Tinybird account and a tracking service beside Ghost. Without them Ghost leaves them off.

The volume

The app runs as one copy on the machine its volume is on, and a deploy stops it for a few seconds. Back up both the volume, for images and themes, and the database, for everything else: one without the other is a site with broken images or no posts.

Resources

The app is limited to 1 CPU and 1 GiB of memory. Raise limits in template.yaml for a busy site.

What this creates

ghost

ghost:6.63.0

ghost-db

MySQL 8.0

/var/lib/ghost/content

Volume of ghost

template.yaml
# yaml-language-server: $schema=https://cubeship.dev/schema/template/v1.json
version: 1
# The first release that keeps a volume's data across deploys.
minCubeship: "0.7.0"
project: ghost

inputs:
  - key: domain
    type: domain
    label: Where the site answers
  - key: smtpHost
    type: text
    label: Your SMTP server
    help: Ghost emails a code to sign in from a new device, so without working mail nobody can sign in twice.
  - key: smtpPort
    type: number
    label: Its TLS port
    help: Ghost connects with TLS from the first byte, which providers offer on 465.
    default: 465
    min: 1
    max: 65535
  - key: smtpUser
    type: text
    label: The SMTP username
  - key: smtpPassword
    type: secret
    label: The SMTP password
  - key: mailFrom
    type: text
    label: The address Ghost sends from
    help: One your SMTP provider lets you send as, like hello@example.com.
    pattern: ^[^@\s]+@[^@\s]+\.[^@\s]+$

databases:
  - key: db
    name: ghost-db
    # Ghost supports MySQL 8 only, and tests against 8.0.
    engine: mysql
    version: "8.0"
    database: ghost

apps:
  - key: web
    name: ghost
    image: ghost
    tag: "6.63.0"
    port: 2368
    # Every other route redirects a probe that did not arrive over HTTPS;
    # this one is outside that check and answers 200 without signing in.
    health: /ghost/.well-known/jwks.json
    domains:
      - host: ${input.domain}
    attach:
      - database: db
    volumes:
      - path: /var/lib/ghost/content
    limits: { cpu: 1, memory: 1Gi }
    env:
      url: https://${input.domain}
      # Ghost reads its connection piece by piece, not from DATABASE_URL.
      database__client: mysql
      database__connection__host: ${db.db.host}
      database__connection__port: ${db.db.port}
      database__connection__user: ${db.db.user}
      database__connection__password: ${db.db.password}
      database__connection__database: ${db.db.name}
      mail__transport: SMTP
      mail__options__host: ${input.smtpHost}
      mail__options__port: ${input.smtpPort}
      mail__options__secure: "true"
      mail__options__auth__user: ${input.smtpUser}
      mail__options__auth__pass: ${input.smtpPassword}
      mail__from: ${input.mailFrom}