I thought it may be appropriate that the topic of my first article here be about how I set up Ghost. As I'm sure you'll find via a web search, a number of people have written about how they deployed Ghost.

As they say, there is more than one way to make a cup of chai. They don't really say that, but I really like a good cup of chai! :)

Why Ghost

There are a number of publishing platforms out there. WordPress, Medium, Joomla, Blogger, and the list goes on.

Because I operate other servers, I preferred a solution I could host myself. I also wanted it to be secure, simple to run, and also simple to use. And Ghost met all of these requirements. I specially liked that it allows you to write markdown!

Building Blocks

At least at this time, I didn't see the need to dedicate a server to my blog. I also wanted to make it easy to move my blog if traffic ever warrants a dedicated server. By server, I don't mean a physical server. I've done my share of running my own physical servers in colocation facilities back in the day. I feel grateful for cloud providers like DigitalOcean (this is a referral link – I appreciate the support if you find this article useful, but at the same time respect if you'd like a non-referral link), AWS, Google Cloud, and Azure, that make it super easy to deploy resources across the globe as you need them. Tools like Terraform from HashiCorp make it easy to provision resources in a consistent manner and at scale across these cloud providers.


I decided I preferred a containerized installation. Docker comes with its benefits and tradeoffs. The benefits include ease and consistency of installation, and ease of relocation when the time comes. Tradeoffs include the complexity of running in a container when you need to troubleshoot something. I have other applications already running in containers, and decided the benefits outweighed the tradeoffs.


I wanted to be able to front my blog with Caddy. Caddy is an open source web server written in go that provides effortless, automated TLS encryption using LetsEncrypt out of the box.


Ghost requires MySQL for a production install, and since I already had a MySQL server running, I created a database for Ghost.



We won't go into the installation of Docker in this blog post. I had used this guide as the basis for installing Docker.


We won't go into the installation of Caddy in this blog post. I had used the Caddy Documentation as the basis for installing Caddy.

Let's add an entry to our Caddyfile for our Ghost blog.

<fqdn> {
  proxy / {
  log / /var/log/caddy/<fqdn>.access.log "{combined}"
  errors /var/log/caddy/<fqdn>.error.log
  tls <email address>

Below is my Caddy configuration for my Ghost blog that is running on the same server as Caddy.

blog.example.io {
  proxy / {
  log / /var/log/caddy/blog.example.io.access.log "{combined}"
  errors /var/log/caddy/blog.example.io.error.log
  tls [email protected]

In the first line, <fqdn> indicates the fully qualified domain name of your blog server. You should have a DNS record that resolves to the IP address of the server on which you are running Caddy.

In the second line, proxy / indicates that we are going to proxy all requests to, the address and port where Ghost will be listening.

In the third line, transparent tells Caddy to pass the host information from the original request.

In he fifth and sixth lines, log and errors tells Caddy where to write the access log and error log, respectively. Note that these are the logs for Caddy, which are separate from the log for Ghost.

In the seventh line, tls <email address> tells Caddy the email address to use for requesting the TLS certificate from LetsEncrypt.


We also won't go into the installation of MySQL, but you want to make sure that the Ghost container / server is able to connect to your MySQL database server.

Let's create a database for our Ghost blog.

create database blogdb;

Let's add a user for ghost to our MySQL database server.

create user ghost@'%' identified by '<password>';

Let's grant that user all privileges on the database we just created.

grant all on blogdb.* to 'ghost'@'%';

Note that % allows the user Ghost to connect from any IP address. Please feel free to adjust this further if you prefer.


Let's create a directory for our Ghost data.You may want mount a volume at this path. You may also want to create a Docker volume for this purpose.

mkdir -p /data/ghost/content

Let's create a script called /data/ghost/run.sh to run Ghost.


docker run \
  -d \
  --name ghost \
  --network=host \
  -v /data/ghost/content:/var/lib/ghost/content \
  -e url=https://blog.example.io/ \
  -e database__client=mysql \
  -e database__connection__host= \
  -e database__connection__user=ghost \
  -e database__connection__password='<password>' \
  -e database__connection__database=blogdb \
  -e NODE_ENV=production \
  --restart=always \

The -d flag indicates that we should detach from the container once it starts.

The --name ghost flag specifies the name of the container, making it easier to interact with afterward.

The --network=host flag specifies to use the host networking driver. This is not strictly a requirement, but how I prefer to run Ghost. An alternative approach for exposing the port on which the container listens is to use the -p flag. For example: -p

The -v /data/ghost/content:/var/lib/ghost/content flag specifies to map the /data/ghost/content directory on the host to /var/lib/ghost/content in the container.

The -e flag specify environment variables for the container.

The --restart=always flag specifies to always restart the container.

Finally, ghost:2.21.0 specifies the docker image to run.

Let's make this script executable.

chmod 0755 /data/ghost/run.sh

Let's run this script to start the container.


Let's check the logs for our container.

docker logs -f ghost

Let's try connecting to our blog to configure ghost.


From here, follow the prompts to configure your blog.

Thank you so much for reading, and have a most wonderful day! :)