UPDATE 1 - May 11 2021: I published the second part, about deploying this to a production server for your personal website. Click here.


I'm currently writing this article from my phone. I'm using the Ghost admin of my website that I installed like a Progressive Web App. It's not a PWA yet, but that will do for now.

This is impeccable. I couldn't do this with Hexo, the previous platform I was using.

Hexo was pretty cool when I only wanted to write 2-3 articles a month. Now that I'm more comfortable writing in public, I want to reach another level.

Since Ghost provides an admin interface with a back-end that are ready for production, you don't actually need to run it in development. I did this to first test the tool out locally.

Therefore, if you simply want to get a blog started as soon as possible, I'll write a subsequent article titled "How I deployed this blog: Running Ghost in production with Docker and Caddy". I will link it here when it's ready.

Requirements

When I use Docker, no matter what the OS I'm on, I use it on Linux, for speed. I believe Docker was meant to improve Developer Experience (DX), and we need fast command execution in the terminal.

So first thing first, get a Linux distro (Ubuntu's great). You can dual boot, use a virtual machine or even replace your current OS.

Just make sure you backup your stuff somewhere else before taking any risks.

For anything else, you need to install those two:

  • Docker - at least version 20.10.6
  • Docker Compose - at least version 1.29.0

This is a high level tutorial so I'm sorry if you're a beginner, I will just fly over stuff. So I recommend you get to understand the concept of containers, Docker and Docker Compose.

I like to add links to official (or interesting) articles about things I discuss about and I highly recommend you click on them so you may benefit from them as well ;)

Note that you don't install Ghost yourself... not even Nodejs and NPM.

Running the platform locally

With all the requirements in mind. Let's see how easy it is to run Ghost locally.

We simply need two files to get started:

  • docker-compose.yml - to easily run it all with one command
  • .env - to setup private information about your data and content

The "docker-compose.yml" File

Create a new directory and use it as your base:

mkdir ~/ghost-blog
cd ~/ghost-blog

Create a new file - docker-compose.yml:

touch docker-compose.yml

Use your preferred text editor to fill it up with the following content:

version: '3.8'

services:
  ghost:
    image: ghost:4.2.1-alpine
    container_name: ghost-dev
    restart: unless-stopped
    depends_on:
      - db
    ports:
      - 2368:2368
    environment:
      # see https://ghost.org/docs/config/#configuration-options
      database__client: mysql
      database__connection__host: db
      database__connection__user: ${MYSQL_USER}
      database__connection__password: ${MYSQL_PASSWORD}
      database__connection__database: ${MYSQL_DATABASE}
      url: http://localhost:2368
      NODE_ENV: development
    volumes:
      - ghost_content:/var/lib/ghost/content

  db:
    image: mysql:8
    container_name: ghost-dev-db
    command: mysqld --default-authentication-plugin=mysql_native_password
    restart: unless-stopped
    ports:
      - 3307:3306
    environment:
      # see https://hub.docker.com/_/mysql
      MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
      MYSQL_DATABASE: ${MYSQL_DATABASE}
      MYSQL_USER: ${MYSQL_USER}
      MYSQL_PASSWORD: ${MYSQL_PASSWORD}
    volumes:
      - mysql_ghost_data:/var/lib/mysql

volumes:
  mysql_ghost_data:
  ghost_content:

Explanation

  1. As you can see, I used the compose file version 3.8.
version: '3.8'

2. Then I defined two different services:

services:
    ghost:
    	...
    db:
    	...

3. Finally, I defined two volumes that will be used by those two services:

  • mysql_ghost_data - which will point to the MySQL files
  • ghost_content - which will be point to the ghost/content directory
volumes:
  mysql_ghost_data:
  ghost_content:
The "ghost" service
  1. Within this service, I'm using the alpine variation (creates a lighter image) of Ghost version 4.2.1. Feel free to update it to a more up to date and stable one:
image: ghost:4.2.1-alpine 

2. I like to be explicit about my container names, so I call this one ghost-dev:

container_name: ghost-dev

3. I want it to only restart if it were explicitly stopped. So in case of failure, it will attempt a restart:

restart: unless-stopped

4. I make sure the db service runs successfully first because Ghost needs a storage location for your dynamic data:

depends_on:
  - db

5. This Ghost image runs on port 2368 by default, so I open this container port to my computer:

ports:
  - 2368:2368

6. The official Ghost image accepts specific environment variables so it can automatically create the Ghost configuration file for you. They do a good enough job at explaining the necessary variables on the image documentation.

I say "good enough" because it takes two different pages/websites to know which environment variables to use.

I only configure the database when in development:

environment:
  # see https://ghost.org/docs/config/#configuration-options
  database__client: mysql
  database__connection__host: db
  database__connection__user: ${MYSQL_USER}
  database__connection__password: ${MYSQL_PASSWORD}
  database__connection__database: ${MYSQL_DATABASE}
  url: http://localhost:2368
  # contrary to the default mentioned in the linked documentation, this image defaults to NODE_ENV=production (so development mode needs to be explicitly specified if desired)
  NODE_ENV: development

Note that three of those configuration properties are using environment variables, not direct input. We will be setting those variables in the .env file.

Also note that we're using the name of the db service as the database host so that it can easily access the MySQL default port running under this service.

7. Finally, I set a volume so I can easily have access to my Ghost content and so that Ghost can be able to reuse them when the container restarts

volumes:
  - ghost_content:/var/lib/ghost/content
The "db" service
  1. Within this service, I'm using the default image of MySQL version 8. Feel free to update it to a more up to date and stable one:
image: mysql:8

2. I set my own name for the resulting container:

container_name: ghost-dev-db

3. I set a custom command for this image so I can use a different authentication plugin for MySQL. Because this is how Ghost connects to MySQL.

command: mysqld --default-authentication-plugin=mysql_native_password

4. I make sure it only stops when it's explicitly stopped:

restart: unless-stopped

5. I open the default MySQL port (3306) to my laptop through the port 3307 because on this computer I was already running another MySQL engine on port 3306. This step was not necessary at all, I only wanted to connect with the container database directly so I could understand how Ghost stores data:

ports:
  - 3307:3306

6. I define the required environment variables so MySQL can configure itself:

environment:
  # see https://hub.docker.com/_/mysql
  MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
  MYSQL_DATABASE: ${MYSQL_DATABASE}
  MYSQL_USER: ${MYSQL_USER}
  MYSQL_PASSWORD: ${MYSQL_PASSWORD}

Note that here again, we will be using the variables from a .env file to setup a container.

7. I setup a volume that points to the MySQL files so that I don't lose my data on container restart

volumes:
  - mysql_ghost_data:/var/lib/mysql

The ".env" File

I hope that you understand how the Compose file is structured by now. It's expecting a few environment variables. We can pass them to Compose through a .env file that's located in the same directory as the Compose file.

Within the same ~/ghost-blog directory, create a new file:

touch .env

Then fill it up with the following content:

MYSQL_ROOT_PASSWORD=the-password-for-root-user
MYSQL_DATABASE=ghost
MYSQL_USER=ghost
MYSQL_PASSWORD=the-password-for-ghost-user

Make sure to update the values to your own needs.

Running it all

Now that we're all setup, here's how to run Ghost locally:

docker-compose up -d

That's it. It will install MySQL, configure it, then run it, then it will install Ghost, configure it, and run it.

Once it says "done" for both services, you're ready to check out your Ghost blog at http://localhost:2368, and your Ghost admin at http://localhost:2368/ghost so you can configure it.

To stop all services, run

docker-compose stop

Conclusion

I hope that was detailed enough. Here are some ideas of things you can do from there:

  • Configure Mailgun SMTP to test out the transactional mail service (user registration and login... etc)
  • Make your Compose file more configurable with the .env file