Interested in Ansible Role to install Ghost to run with podman containers / systemd / private networking?

I built an Ansible role for myself to automate installing multiple Ghost installs that will be run containerized with Podman and run as systemd services, sharing a private MySQL install.

I’m weighing how much effort to put into cleaning it up and publishing it so I thought I’d gauge interest first.

The last time I searched for “Podman” on this form, I only found my own posts…

Features

  • Ghost instances are run as systemd services, like with the ghost-cli install,
    but each in their own container for security and isolation.

  • No requirement to install node directly on the host, so there’s no conflict
    with other apps that might use different versions of Node.

  • A single YAML file for all Ghost blog configuration that directly maps to
    config.production.json structure.

  • Easier management of multiple Ghost instances.

    • Faster upgrades. ghost-cli downloads a copy of Ghost for every instance upgrade.
      Here, each version is Ghost is only downloaded once.
    • Shared configuration. Define shared configuration in one place.
    • Multiple Ghost instances can access the same MySQL database.
2 Likes

Here’s what my template for a Ghost “.container” file looks like today. Ansible will render it with template variables and then Podman will use it to generate a systemd service file like “ghost_my-blog-com.service”.

The generated service file will work like the one that Ghost itself generates, with the benefit that the service will now be containerized.

# MANAGED BY ANSIBLE
[Unit]
Description=Ghost Blog for {{ ghost_fqdn }}
After=network-online.target

# Keys are sorted in alpha-order
[Container]
# The name structure is exactly like what Ghost would create
ContainerName={{ ghost_service_name }}
Image={{ ghost_image }}
# TODO: Currently this is not setup for you and there's no option to disable it either.
# We should do one or the other.
Network={{ ghost_network_name }}.network
# Pick a value within 10.10.*.*.
IP={{ ghost_ip }}
#RunInit=true
# Map the "node" user in the container with UID 1000 inside the container to custom host user.
#UIDMap=1000:{{ ghost_mgr.uid }}
Volume={{ ghost_content_path }}:/var/lib/ghost/content:z
Volume={{ ghost_install_path }}/config.production.json:/var/lib/ghost/config.production.json:z

{#
 # systemctl enabled/disable doesn't work on the transient systemd units generated by Quadlet
 # Instead, we can add or remove this block to achieve the same effect.
#}
{% if ghost_enabled %}
[Install]
WantedBy=network-online.target
{% endif %}
# vi:ft=systemd

You can see I’m giving each Ghost blog a unique IP and adding to the same network, like “mysql.network”, so they can access a shared MySQL database.

1 Like

And here’s what part of my Ansible variable definition looks like in a playbook that uses my Ansible role. Here you can see I’ve defined some config file defaults that will apply to all my Ghost blogs, as well as some details which are specific to one blog.

You’ll also see that I do not store secrets in my Ansible repo. Those stay in my password manager, which Ansible can easily reference when I run a playbook.

ghost_config_defaults:
  mail:
    transport: SMTP
    options:
      service: Mailgun
      host: smtp.mailgun.org
      port: 465
      secure: true
ghost_blogs:
  - ghost_fqdn: sub.example.com
    ghost_ip: 10.10.23.71
    # This blog is stopped and disabled for now.
    ghost_state: stopped
    ghost_enabled: false
    ghost_config_production:
      database:
        connection:
          user: ghostmark
          password: "{{ lookup('onepassword', 'sub.example.com-mysql-ghostmark') }}"
          database: sub_example_com_ghost
      mail:
        options:
          auth:
            user: postmaster@mg.example.com
            pass: "{{ lookup('onepassword', 'mailgun-postmaster@mg.example.com') }}"
1 Like