Setting Up a Self-Hosted Ghost Blog with Docker Compose

This article chronicles the shit-show of setting up a self-hosted Ghost blog using Docker Compose, complete with a stack of services that's about as stable as a Jenga tower in an earthquake.

In the cesspool of cloud computing and managed services, there's something uniquely masochistic about building and maintaining your own web infrastructure.

nexus_infernum
Infernal Symphony of Agony. Infrastructure description that SpeedyWeedyOps site are using

repo link

The Delusion: GhostCompose

Our project, sarcastically dubbed "GhostCompose" or better "Nexus Infernum", aims to create a personal blogging hellscape that's both powerful and a pain in the ass to manage. By leveraging Docker Compose, we're able to orchestrate a circus of services that work together about as seamlessly as a three-legged cat trying to bury turds on a frozen pond:

Oh, and a word about Nexus Infernum. What is it, you ask? Well, fuck me if I know. It could be the name of project, a secret society of masochistic self-hosters, or the Lovecraftian horror that powers our servers. Your guess is as good as mine. But it sounds cool, doesn't it?
  • Gitea: For version control and collaborative development (aka "Let's make merge conflicts great again")
  • Caddy: A modern web server acting as our reverse proxy (because Nginx was too mainstream)
  • Varnish: A caching layer to boost performance (or to add another point of failure, who knows?)
  • MySQL: The database backend for both Ghost and Gitea.
  • Ghost: Our primary blogging platform (because WordPress was just too damn easy)

The Shit-Show Architecture

To better understand how these services interact (or more likely, how they'll spectacularly fail), we created a visual representation of our stack that looks like a drunk spider's attempt at web design:

visual representation of how the different components of your infrastructure communicate with each other

This diagram illustrates the clusterfuck of traffic through our system:
1. User requests come in through the internet
2. Caddy handles SSL termination and acts as a reverse proxy
3. Varnish provides caching for improved performance (or so we tell ourselves)
4. Requests are then routed to either Ghost or Gitea
5. Both Ghost and Gitea interact with MySQL for data storage (pray for your data's soul)

Setting Up

The heart of our setup lies in the compose.yml file, which defines our services and their configurations. Here's a high-level overview of what each service does:

  • Caddy: Handles incoming HTTP/HTTPS traffic, manages SSL certificates, and routes requests to the appropriate backend services. It's like a traffic cop with ADHD.
  • Varnish: Sits between Caddy and Ghost, caching responses for improved performance. It's the equivalent of your browser's cache, but with more opportunities for things to go wrong.
  • Ghost: Our main blogging application, configured to use MySQL as its database. Because apparently, flat files were too simple.
Ghost blog in the shell. nice tits btw
  • Gitea: Provides a self-hosted Git service, useful for managing our infrastructure code and potentially for collaborative blogging. GitHub was just too reliable.
  • MySQL: Serves as the database backend for both Ghost and Gitea. Single point of failure? More like single point of "fuck it, I'm going back to pen and paper."
x-logging: &default-logging
  options:
    max-size: "10m"
    max-file: "3"

x-healthcheck: &default-healthcheck
  interval: 30s
  timeout: 10s
  retries: 3
  start_period: 5s

networks:
  web:
    external: true
  internal:
    external: false
    driver: bridge

services:
  caddy:
    image: caddy:2-alpine
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./caddy/Caddyfile:/etc/caddy/Caddyfile
      - ./caddy/data:/data
      - ./caddy/config:/config
    networks:
      - web
      - internal
    logging: *default-logging
    healthcheck:
      <<: *default-healthcheck
      test: ["CMD", "nc", "-z", "localhost", "80"]

  ghost:
    image: ghost:5.89-alpine
    restart: unless-stopped
    environment:
      - url=https://speedyweedyops.org
      - database__client=mysql
      - database__connection__host=mysql
      - database__connection__user=ghostmysqluser
      - database__connection__password=/run/secrets/ghost_mysql_password
      - database__connection__database=dbghost
    secrets:
      - ghost_mysql_password
    volumes:
      - myghostapp:/var/lib/ghost/content
    networks:
      - internal
    depends_on:
      - mysql
      - caddy
    logging: *default-logging
    healthcheck:
      <<: *default-healthcheck
      test: ["CMD", "nc", "-z", "localhost", "2368"]

  gitea:
    image: gitea/gitea:1.22
    restart: unless-stopped
    volumes:
      - giteadata:/data
    networks:
      - internal
    depends_on:
      - caddy
    logging: *default-logging
    healthcheck:
      <<: *default-healthcheck
      test: ["CMD", "nc", "-z", "localhost", "3000"]

  mysql:
    image: mysql:8.4.2
    restart: always
    environment:
      - MYSQL_RANDOM_ROOT_PASSWORD=true
      - MYSQL_DATABASE=dbghost
      - MYSQL_PASSWORD=/run/secrets/ghost_mysql_password
      - MYSQL_USER=ghostmysqluser
    secrets:
      - ghost_mysql_password
    volumes:
      - ./mysql/my.cnf:/etc/mysql/my.cnf
      - mysqldb:/var/lib/mysql
    networks:
      - internal
    logging: *default-logging
    healthcheck:
      <<: *default-healthcheck
      test: ["CMD", "bash", "-c", "cat < /dev/null > /dev/tcp/localhost/3306"]
      interval: 10s
      start_period: 30s

  varnish:
    image: varnish:7.3
    restart: unless-stopped
    volumes:
      - ./varnish/default.vcl:/etc/varnish/default.vcl:ro
    environment:
      - VARNISH_SIZE=512M
    networks:
      - internal
    depends_on:
      - ghost
    logging: *default-logging
    healthcheck:
      <<: *default-healthcheck
      test: ["CMD", "varnishadm", "ping"]
      interval: 10s

volumes:
  mysqldb:
  myghostapp:
  giteadata:

secrets:
  ghost_mysql_password:
    file: ./ghost_mysql_password

compose.yaml

Software versions used:
Docker version 27.1.2, build d01f264
Docker Compose version v2.29.1

Security Considerations

Security was a top priority in our setup, right after "making things unnecessarily complicated" and "ensuring maximum frustration." We implemented several measures:

  1. HTTPS Everywhere(except DB connections, LOL): Caddy automatically handles SSL certificate provisioning and renewal.
💡
We'll return to the topic of securing DB connections in the next article.
  1. Security Headers: We've configured Caddy to send appropriate security headers with each response.
  2. Network Isolation: Using Docker networks, we've isolated our services, reducing the attack surface.
  3. Secret Management: Sensitive data like database passwords are managed securely(no) using Docker secrets. It's as secure as hiding your diary under your mattress.

Performance Optimizations

To ensure our blog runs as smoothly as a rusty bicycle, we've implemented several performance "optimizations":

  1. Varnish Caching: Reduces load on Ghost and improves response times for frequently accessed content.
  2. Static Asset Caching: Caddy is configured to apply appropriate caching headers for static assets. Because everyone loves seeing that same broken image for weeks.
  3. Compression: Responses are compressed to reduce bandwidth usage and improve load times.

Maintenance and Monitoring

Maintaining a self-hosted infrastructure requires ongoing attention, much like a needy ex who won't stop texting you. We've set up logging for all services to output to stdout/stderr, allowing easy monitoring using Docker's built-in logging capabilities. It's like trying to find a specific drop of water in a waterfall.

Regular backups of the MySQL data and Ghost content are crucial and should be implemented as part of a comprehensive maintenance strategy. Or you could just pray to the data gods and hope for the best.

💡
We'll return to the topic of monitoring in future articles.

Conclusion

Setting up a self-hosted Ghost blog with Docker Compose has been an enlightening journey into modern web infrastructure.

While it requires more initial setup and ongoing maintenance compared to managed solutions, the knowledge gained and the satisfaction of running your own stack make it a rewarding endeavor for tech masochists and bloggers with too much free time alike.

Happy blogging, and may your words flow as smoothly as your containerized infrastructure (which is to say, not at all).


This project is open source and available at: https://git.speedyweedyops.org/igovnow/nexus_infernum.git. Clone at your own risk.

Subscribe to SpeedyWeedyOPS

Don’t miss out on the latest issues. Sign up now to get access to the library of members-only issues.
jamie@example.com
Subscribe