This is a guide on how to selfhost your own blog. It is a step by step guide, or perhaps my own documentation, because this is how I am hosting this blog.

Note: This has no warranty, and I am not responsible for any damages that may occur from following this guide. This is a guide for educational purposes only.

Step 0: Decide to blog

Not a technical step I know, but if you do not currently have writing material to post, perhaps draft some blog titles. Find some topics that you are passionate about, because that will fuel your motivation. It will also help guide the technical and design decisions you will make in will designing your blog.

Step 1: Choose a static site framework

Static site frameworks are tools that take your content and generate a static website. This makes the actual hosting of the website light weight. It also will aid in adding features, such as an RSS feed, themes, comments etc.

I chose to use Hugo. I like that it is written in Go, and it can process articals written in markdown.

Other options include:

For Hugo:

1 Install Hugo On debian based systems:

sudo apt install hugo

2 Setup your environment

hugo new site myblog cd myblog

3 Choose a theme

I am using the re-terminal theme for my blog. I like the the simple retro look. Feel free to pick your own. Follow the steps on their page for best practices on how to install. I chose to use the git submodule setup, but you can also download the theme and place it in the themes folder of your Hugo project.

4 Create the content

I made the following direcotry structure for my blog:

  • content/posts/ This is where my blog posts are stored
  • static/posts/ARTICLE_NAME/ This is where I store images for my blog posts
hugo new posts/my-first-post.md

Now you can edit the markdown file to add your content.

Add a image to the post by placing it in the static/posts/ARTICLE_NAME/ folder. Then reference it in the markdown file like so:

[]: # ![Image Caption](/posts/ARTICLE_NAME/image.png)

5 View the site locally

hugo server -D

Great! Now you have the shape of the blog setup. If you want you can stop here and host the site locally for your local network to see. But if you want to share your blog with the world, you will need to host it.

Step 3: Containerize your blog and CI/CD

If you want to, you can host your blog by running hugo server -D on your server, and move on. That is fine. There is no shame. I want to containerize my blog so that I can put it in my Docker environment running on my server. I also want to setup a CI/CD pipeline so that I can publish any changes to my blog with a simple git push to my hugo repository.

I am using the hugomods nginx image to host my blog. It only takes a few lines to setup. Hugo Mods docker container documentation

Dockerfile:

FROM hugomods/hugo:exts AS builder

# Can be overwriten by passing --build-arg HUGO_BASEURL= to docker build ARG
HUGO_BASEURL=

ENV HUGO_BASEURL=${HUGO_BASEURL}

# Copy site
COPY . /src

# Build the site
RUN hugo --minify --enableGitInfo

# Pull in the nginx image to serve the site
FROM hugomods/hugo:nginx

# Copy the generated files to keep the image as small as possible.
COPY --from=builder /src/public /site

This docker file can be built with the following command:

docker build --HUGO_BASEURL=http://localhost:1313 -t myblog .

And started with the following command:

docker run -p 1313:80 myblog

-p 1313:80 will map port 1313 on your host to port 80 on the container. You can change this to whatever you want.

Step 4: Setup a CI/CD pipeline

Again this can be skipped if you want to manually build and deploy your blog.

First I will write a docker compose file that will host my blog.

docker-compose.yml:

name: blog
  services:
  sourcetosink:
    image: source-to-sink:latest
    container_name: source-to-sink
    restart: unless-stopped
    ports:
      - 1313:80

This docker compose file will start my blog on port 1313.

Next a github action file to build and deploy the blog. I have a self-hosted runner on the serer that will run the action. I also have portinaer running on the server to manage the docker containers.

.github/workflows/deploy.yml:

name: CI

on:
  push:
    branches:
      - main  # or the branch you want to trigger the CI on

jobs:
  build:
    runs-on: edith

    steps:
      - name: Checkout code uses: actions/checkout@v2

      - name: Submodule update for theme
        run: git submodule update --init --recursive

      - name: Build Docker image
        run: |
          docker build --build-arg HUGO_BASEURL="http://sourcetosink.duckdns.org:80" \
                 -t source-to-sink .
          docker tag source-to-sink source-to-sink:latest          

      - name: Trigger Portainer Stack redeploy
        run: | 
          curl --insecure -X POST ${{ secrets.EDITH_BLOG_STACK_WEBHOOK }} 

This action will build the docker image, and then trigger a webhook to redeploy the stack in portainer. The url for the webhook is stored in my github secrets. That will redeploy the blog with the latest changes, after building the docker image.

Check in the docker-compose.yml to a repository that you can access from your server.

Next run portainer on your server. To start portainer:

docker run -d -p 9000:9000 -v /var/run/docker.sock:/var/run/docker.sock portainer/portainer

Setup portainer, and create a stack for the blog.

Copy the webhook url from the portainer stack, and add it to the github secrets.

Step 6: Choose a DNS

A DNS or Domain Name Service is a system that converts a human readable site name (ie. “duckdns.org”) to a machine understandable IP address. For folks to access your page, they will need to know the IP address of your server.

To begin, I will use duckdns.org. It is a free dynamic DNS service that will allow folks to access my blog by using the link ‘sourcetosink`.duckdns.org’. This is good enough to get off the ground, but in the future I will be looking at buying a domain and setting up dynamic dns.

Sign up for an account at DuckDNS, and follow the instructions to setup your domain.

DuckDNS

On my OpenWrt router, setup the Dynamic DNS service to automatically determine my public IP address, and update the DNS record on DuckDNS. This way, the DuckDNS record stays up to date in case my ISP changes my IP address. If you have a static IP address, you can skip this step.

  1. Copy the token from the DuckDNS website for your new domain.
  2. On the OpenWrt router, navigate to Network -> Dynamic DNS
  3. Click Add
  4. Fill in the following:
    • lookup hostname: (Your DuckDNS domain)
    • Domain: (Your DuckDNS domain)
    • DDNS Service Provider: duckdns.org
    • Username: (Your DuckDNS username)
    • Password: (Your DuckDNS token)
  5. Click Save and Apply

Now your domain will be updated with your public IP address.

You can try using the ping command to see if your domain is pointing to your public IP address.

If you don’t know your public IP address, you can use the command: curl ipinfo.io/ip

Step 7: Setup your router

You will need to setup port forwarding on your router to allow folks to access your blog from the internet. Your home router uses Network Address Translation (NAT) to allow multiple devices to share your public IP address. I have a R4PiS running OpenWRT, so I will show you how to setup port forwarding on that.

  1. Navigate to Netowrk -> Firewall -> Port Forwards
  2. Click Add
  3. Fill in the following:
    • Protocol: TCP
    • External Zone: WAN
    • External Port: 80
    • Internal Zone: LAN
    • Internal IP: (Select your server)
  4. Click Save and Apply

NOTE please be careful when opening ports on your router. Only open the ports you need, and close them when you are done. This will help keep your network secure.

Step 8: Setup your blog

Now if you push changes to your hugo repository, the github action will triger a build of the docker image. Then call the webhook to redeploy the stack in portainer.

Every time you push changes it should automatically update your blog in a few minutes.

Congratulations! You have setup your own blog. 🎉

Conclusion

There are many like it, but this is my blog. I hope you learned something about web frameworks, CI/CD, containerization, and networking. This is a great way to learn about these topics, and I hope you have fun setting up your own blog.

I will in the future go into more detail about my homelab setup. Github actions, portainer, and Docker all deserve their own series. I will also post any more tips and tricks I find with the hugo framework. I will keep the tech stack fluid as I find better ways to self host, and share them with you. Thanks for reading! 😁

-E