šŸš€ From Zero to Newsletter Hero: Building and Hosting a Full-Stack Publishing Platform with Ghost, Docker, Kubernetes, and ngrok

Kartik YadavKartik Yadav
5 min read

Submission for the Newsletter Challenge — Built with šŸ’», powered by curiosity, and deployed like production.


✨Why You Should Read This

If you’ve ever wondered:

  • How can I run my own newsletter without paying a monthly fee?

  • Can I customize it like a pro?

  • Can I deploy it using modern DevOps tools like Docker and Kubernetes?

...then you’re exactly where you need to be. In this challenge, I took a raw idea and turned it into a fully functional, self-hosted newsletter system, built from scratch using:

  • šŸ“° Ghost CMS

  • 🐳 Docker & Docker Compose

  • ā˜øļø Kubernetes with Kind

  • 🌐 ngrok for instant HTTPS tunneling

Let’s dive into the story.


šŸ” Challenge Breakdown: What Was Required

Here's what the challenge asked for:

  1. āœ… Use a popular newsletter platform (Substack, Beehiiv, or MailerLite), post content, and export data

  2. āœ… Set up a self-hosted Ghost CMS site (locally or on cloud), import data from step 1

  3. āœ… Customize Ghost’s theme and settings

  4. āœ…šŸ”§ Bonus: Run with Docker, then with Kubernetes (Minikube/Kind), and document everything

I didn't just complete the challenge. I engineered it like a real production system.


āœ‰ļø Step 1: Exploring Substack

I chose substack because I used to write on it a while ago.
I then added a few email ids of mine, and then exported the data.


šŸ—ļø Step 2: Running Ghost CMS with Docker

Here's the docker-compose.yaml file I used to run Ghost CMS and MySQL locally:

services:
  ghost:
    image: ghost:5
    container_name: ghost
    ports:
      - "2368:2368"
    volumes:
      - ghost_data:/var/lib/ghost/content
    environment:
      url: http://localhost:2368
      database__client: mysql
      database__connection__host: db
      database__connection__user: ghost
      database__connection__password: ghostpass
      database__connection__database: ghost
    depends_on:
      db:
        condition: service_healthy
    restart: always

  db:
    image: mysql:8
    container_name: ghost-db
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: -/-/-/
      MYSQL_DATABASE: ghost
      MYSQL_USER: ghost
      MYSQL_PASSWORD: ghostpass
    volumes:
      - ghost_mysql:/var/lib/mysql
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 10s
      timeout: 5s
      retries: 5

volumes:
  ghost_data:
  ghost_mysql:

Now run this command in the same directory:

docker-compose up -d

Once all the containers are healthy and running, we can visit http://localhost:2368 and we should be able to see something like this:

Now go to http://localhost:2368/ghost to create the newsletter:


🌈 Theme Customization

I chose the Edition theme because it resembles Substack’s layout, and picked the Manrope font—because it’s clean and modern (and honestly, my favorite).

Now, I just have to import my blogs and email list and then I can finally see how will my page will look like.

A common issue I faced was that Ghost requires the site to be hosted publicly to access certain features like newsletter imports. Since I didn’t want to pay to host my website, so I had to figure out a way to host my site for free(coz that’s what Indians are expert inšŸ˜…). So, after searching the internet, I found a way that I can host the site using ngrok or cloudflare tunnel temporarily.

After downloading ngrok and setting it up with my auth token, I ran the below command to forward the port 2368. But this alone wouldn’t solve the problem, because I also had to update the Docker Compose file and recreate the containers which will ensure that ghost generate internal links correctly with ngrok’s domain.

ngrok http 2368

something like this will be shown, now I just have to edit the url in the compose file and then restart container.

docker-compose down
docker-compose up -d

Even after doing this, Ghost was redirecting incorrectly. I expected to be prompted for setup, but it redirected me back to the sign-in page—turns out I needed to wipe the previous volume data. After a while, I got the issue. I then started everything fresh and removed all the old volumes.

Now, finally we are allowed the import from substack.

Finally, after setting up website for a while, I got this result, containing all my blogs from substack.

And that’s how you host your newsletter using Ghost CMS and Docker—for free! 😁.


āš™ļø Step 3: Ghost + Kubernetes Using Kind

Now, I wanted to run the ghost cms using kubernetes and kind (Since I’ve used Minikube in the past, I decided to try out Kind for fun).

I organized my project into several kubernetes manifest files:

  • ghost-deployment.yaml : defines the Ghost CMS deployment

  • ghost-pvc.yaml : Persistent Volume Claim for Ghost content

  • mysql-deployment.yaml : MySQL database deployment

  • mysql-service.yaml : MySQL service configuration

  • ghost-service.yaml: Ghost service configuration

After writing the code for all the files, run these commands:

kubectl apply -f ghost-pvc.yaml && kubectl apply -f mysql-deployment.yaml && kubectl apply -f mysql-service.yaml && kubectl apply -f ghost-deployment.yaml && kubectl apply -f ghost-service.yaml

Generate the ngrok url and edit the url in ghost deployment file and reapply it.

But got the ngrok 8012 error:

struggled a lot with the error and came to know that this command was missing.

Basically request was forwarding from ngrok to my localhost, but still after that, I needed to forward it to my ghost service.

After running the command—boom—it started working on the ngrok URL!


🧠Learnings

  • How to organise your thoughts to present them in a structured way bcoz there is a lot I can write → how to hit the perfect spot so that reader doesn’t get bored and still gain some value.

  • Learning backwards → It's not necessary these days to learn everything upfront. You can learn on the go—and that’s how I approach most projects nowadays.

  • When starting out, I wondered—why go through all this if I could just keep writing on Substack?( turns out ghost cms is a lot customizable)

  • I was curious how ngrok allows access to a site running locally. So, as any curious person would do, I asked ChatGPT and got this. Maybe this can also be useful to you.


šŸ’¬ Let’s Connect

If you enjoyed reading this blog and gained some value out of it, let’s connect on LinkedIn.
Here is the link to github repo!


1
Subscribe to my newsletter

Read articles from Kartik Yadav directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Kartik Yadav
Kartik Yadav