How I self-host my blog with Docker & Ansible

4 min read Original article ↗

I have always wanted to build a blog, but for a long time, I have been postponing it.

Well, I guess I thought it was difficult to build a blog. Turns out, even if you self-host it, it’s not that hard.

I decided to finally create it one day, and I had a few criteria:

  • No fancy admin panels. I want to write markdown and deliver it as is.

  • Without sacrificing the privacy of visitors, I should be able to collect traffic statistics, i.e. Google Analytics. No external JavaScripts.

    Just a number of visitors per article per day is enough for me.

  • Complete control over my data.

  • I would like to have comments on my website in the future.

Considered options

  • A popular blogging platform such as wordpress.com or blogspot.com.

    The choice does not provide as much flexibility as I wanted, it has privacy issues, and I don’t have any control over my data at the end of the day.

    The entire platform can be blocked in specific countries sometimes: list of contries blocking blogspot.com on Wikipedia.

  • GitHub Pages

    Almost a flawless hit.

    But it’s way too static, which means I’m not going to be able to have comments on my page unless I use an external solution.

    The shipping of something like Disqus sacrifices privacy and leads to no data control.

    Things like Disqus also have a price (or they show ads).

Self-hosting

Self-hosting has never been so easy, the only hard requirement is to have a Linux server costing $5 a month for 25GB SSD, 1GB RAM and 1vCPU.

I have selected Hugo for site generation because it is very popular, has a large supportive community, uses markdown, and has a lot of free themes.

Packaging

Packaging and distribution is done using Docker. This is how tiny my Dockerfile looks:

FROM alpine as build
RUN apk add --update --no-cache hugo
WORKDIR /srv/http/
COPY . /srv/http/
RUN hugo -e production

FROM nginx:alpine
COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=build /srv/http/public /srv/http/
WORKDIR /srv/http/

This ends up as a Docker container with Nginx and static HTML and CSS files.. The docker image is approximately 52MB in size.

Just two commands and the blog is ready for deployment:

$ docker build -t kovetskiy/my-blog .
$ docker push kovetskiy/my-blog

Deployment

A manual way of deployment and an automatic way are available.

Manual way

Manually, on the remote host, you would end up with commands like:

  • Pull the Docker image:
    $ docker pull kovetskiy/my-blog
    
  • Stop and delete existing Docker container:
  • Start a new Docker container:
    $ docker run --detach --restart=always --name my-blog -p 80:80 kovetskiy/my-blog
    

Automation with Ansible

A playbook is a list of tasks that should be executed to accomplish the desired status. In my case, it is pulling of the Docker image and starting a Docker container.

The playbook is saved as a file called my-blog.yaml:

- hosts: my-server
  tasks:
    - name: Pull image
      docker_image:
        name: kovetskiy/my-blog

    - name: Start container
      docker_container:
        name: my-blog
        detach: true
        image: kovetskiy/my-blog
        restart_policy: always
        state: started

Ansible needs a bit of project-wide configuration, it just has to understand how to connect to this my-server server.

To tell Ansible where the list of server is, put the following content into ansible.cfg:

[defaults]
inventory = hosts

Add your server into the hosts file and replace 123.123.123.123 with the host’s IP address:

[my-server]
123.123.123.123

The deployment will be done with one small command from now on:

$ ansible-playbook my-blog.yaml

Recap

How I self-host my blog:

* Static site generated with Hugo
* Packaged with Docker: docker build
* Linux host/droplet on Linode/DigitalOcean
* Ansible for the deployment of Docker containers

Which topic to expand first?

— Egor Kovetskiy (@reconquestio), November 5, 2020

I also self-host an open source comments system without sacrificing any privacy. 👇