Nick James

Self hosting Ghost on the cheap (Part 1)

If you are reading this, I will assume the time has finally come for you to start your own blog. Like you, I want to document my experiences and share them with the world. After all, sharing is caring :)

By the end of this tutorial, you should have a local copy of your own blog. If you read the entire series, you should have a copy running in a hosted environment. And truth be told, you can use a lot of these steps to host any site you like. So lets dive right in.  

Install Docker

First, you will need to install docker and docker-compose. I will start by getting the site running locally, and in the next post move it over to a server. Use what ever method makes sense for you to install docker. I run Arch Linux so I installed docker using the following command in a terminal.


sudo pacman -S docker docker-compose

From here I had to enable and start the docker service


sudo systemctl enable docker.service
sudo systemctl start docker.service

And finally add my user to the docker group.


sudo gpasswd -a your_user_name docker

Just know, at the time of writing this, any user in the docker group is root equivalent. See here

Project directory structure

Next you will want to create a folder to hold all the content for this project. I called mine blog. Inside of blog, create another folder called ghost. Finally, inside of the ghost folder, create one called content. This folders are needed for creating docker volume mounts later on. We want some of the data to live outside of docker so if the container is destroyed, all your blog post and settings don't disappear with it.

Nginx and Docker

Ok. Now for the fun part. In the blog directory, create a file named docker-compose.yml and open it in your favorite text editor. Add the following to the file


version: '3.5'

volumes:
  nginx-conf:
  nginx-vhost:
  nginx-html:

services:
  nginx:
    image: nginx:1-alpine
    container_name: nginx
    restart: always
    ports:
      - 80:80
    volumes:
      - nginx-conf:/etc/nginx/conf.d
      - nginx-vhost:/etc/nginx/vhost.d
      - nginx-html:/usr/share/nginx/html

Here we have defined an nginx service that will be used as a reverse proxy. I know, nothing too special about it yet but trust me, the magic is coming. While we are here, I want to make a comment about the use of separate named volumes. This was intentional. The volumes are needed to persist the nginx configuration and share it with other containers. If you put all the data in one volume, the wrong file types will be in folders they should not and nginx will crash. I spent a whole day learning that the heard way. Here is a quick description of what will live at each mount point

  • nginx-vhost:etc/nginx/vhost.d to change the configuration of vhosts (needed by Let's Encrypt in future post)
  • nginx-html:usr/share/nginx/html to write challenge files.

Automagically configure Nginx

Now that we have an nginx container defined, we need to configure nginx itself and tell it where to forward request. This could be done manually, but fortunately for you, I'm lazy, which means I found a better way. By the grace of the software gods, there is a tool called docker-gen which can generate reverse proxy configs, and reload the nginx container when other containers are started and stopped. You can read more about it here.

So lets take advantage of this new awesomeness to configure nginx by appending the following to the docker-compose.yml file


nginx_dockergen:
  image: jwilder/docker-gen:0.7.3
  container_name: nginx_dockergen
  restart: always
  depends_on:
    - nginx
  volumes:
    - ./nginx.tmpl:/etc/docker-gen/templates/nginx.tmpl:ro
    - /var/run/docker.sock:/tmp/docker.sock:ro
    - nginx-conf:/etc/nginx/conf.d
    - nginx-vhost:/etc/nginx/vhost.d
    - nginx-html:/usr/share/nginx/html
  command:
    "-notify-sighup nginx -watch -wait 5s:30s /etc/docker-gen/templates/nginx.tmpl /etc/nginx/conf.d/default.conf"

A few things to note here. First, we are reusing a lot of the volumes from the previous service, and secondly, we are mounting the docker socket in this container. Docker-gen needs to be able to send stop and start signals to other containers. A lot of tutorials out there will suggest using the jwilder/nginx-proxy docker image, which sets up nginx and docker-gen in the same container. Going that route means the docker socket will be bound to a publicly accessible container. I'm no security expert, but I personally did not feel comfortable doing that, which is why I split it into two separate containers.

Lastly, in order for docker-gen to create the config files for you, you will need to obtain the latest nginx template file. If you have curl installed, and are comfortable using the command line, navigate to the blog directory inside the terminal and run this command.


curl https://raw.githubusercontent.com/jwilder/nginx-proxy/master/nginx.tmpl > /path/to/nginx.tmpl

You can also just paste the above URL in a web browser, copy the text from the page and paste it into a file named nginx.tmpl.

Ghost setup

We are finally in the home stretch for this post. Now it is time to setup ghost. Once more, we will be adding an entry to the docker-compose.yml file. Append the following


ghost:
  image: ghost:2-alpine
  container_name: ghost
  restart: always
  expose:
    - '2368'
  depends_on:
    - nginx
    - nginx_dockergen
  volumes:
    - ./ghost/content:/var/lib/ghost/content
  environment:
    url: http://localhost
    NODE_ENV: development
    VIRTUAL_HOST: localhost

From the text above, we have stated that this container will be listening on port 2368, but not publish it for request from the outside world. All containers being proxied should provide this value. It will be used by the nginx_dockergen container to generate configs for nginx.

Next you will notice the volume ./ghost/content:/var/lib/ghost/content. All ghost settings, themes, images, and the sqlite database will live in this directory. You definitely want all this data to persist if the container stop and is destroyed.

Lastly, the url environment variable lets ghost know which URL is to be used to access the blog. The NODE_ENV variable sets ghost in either development or production mode. I chose development since it is currently on the local machine. And finally, in order for docker-gen to do its thing, you need to add VIRTUAL_HOST variable so it can use this value in the nginx config files. The value used here should be the same as domain to the site.

Start the blog

After all that, you should now be ready to start your new blog. Open a terminal and run this command in the blog directory.


docker-compose build

followed by


docker-compose up

If everything worked correctly, you should be able to type http://localhost into your browser and you should be greeted by your new blog!

Author image
About Nick James