Host your blog in a Cloud VM using Docker

A summary of my experience migrating this blog from Basecamp’s Kamal to Docker

February 28, 2025

#docker #kamal

For many months, I hosted this blog in a Cloud VM using Basecamp’s kamal. kamal is a CLI tool that deploys a project to Docker containers in 1 or more servers. Earlier this year, when I struggled to upgrade to Kamal 2, I decided that its an unnecessary abstraction (as it didn’t provide commensurate value) and discarded it in favour of plain Docker.

To my pleasant surprise, I found that starting a Docker container is easy. I ran the command below to start my blog in a Docker container

mkdir -p /opt/blog-content/
docker run -d -p 80:1313 -v /opt/blog-content:/opt/blog/content --restart unless-stopped nisanth074/blog:latest

Just running that command brought my blog online and made it publicly on http://

HTTPS

To make my blog available on https://, I turned on Cloudflare’s Proxy feature for my blog domain’s DNS A record in Cloudflare’s dashboard. Doing that causes Cloudflare to issue an SSL certificate and serve my blog over https. While the traffic between Cloudflare and my blog is still unencrypted, that’s an acceptable tradeoff for me.

If you don’t use Cloudflare or dislike the approach above, you can easily install and configure Caddy. Caddy will automatically obtain an SSL certificate for your blog’s domain and serve it over https://

Ancillary Docker containers

As I write my blog posts in Notion, I also have to run hugo-notion (a CLI tool that I developed in Go to sync blog posts from Notion to Hugo’s content dir) in a different Docker container.

Fortunately, starting ancillary docker containers is straightforward.

mkdir -p /opt/blog

touch /opt/blog/.env
echo "CONTENT_NOTION_URL=https://www.notion.so/blog_content-0f1b55769779411a95df1ee9b4b070c9" >> /opt/blog/.env
echo "NOTION_TOKEN=my_notion_access_token" >> /opt/blog/.env
echo "GOPROXY=direct" >> /opt/blog/.env

docker run -d -v /opt/blog-content:/opt/blog/content --env-file /opt/blog/.env --restart unless-stopped nisanth074/hugo-notion 

Deployments

To restart/update my blog and hugo-notion with ease, I simply added a docker-compose.production.yml file

services:
  sinatra:
    image: nisanth074/blog:latest
    volumes:
      - /opt/blog-content:/opt/blog/content/
    ports:
      - "80:4567"
    restart: unless-stopped
  hugo-notion:
    image: nisanth074/hugo-notion
    volumes:
      - /opt/blog-content:/opt/blog/content
    env_file: /opt/blog/.env
    working_dir: /opt/blog

and then added a deploy.sh bash script to copy the docker compose file to my CloudVM and to restart Docker containers

npm run docker:push:x86
scp .env root@1.2.3.4:/opt/blog/.env
scp docker-compose.production.yml root@150.136.2.200:/opt/blog/docker-compose.yml
ssh root@150.136.2.200 "cd /opt/blog && docker compose down"
ssh root@150.136.2.200 "cd /opt/blog && docker compose up -d --pull always"

Takeaway

This move reinforced my opinion that every abstraction introduced to a project (kamal in this scenario) should provide value that significantly surpasses the learning curve and complexity the abstraction adds. Otherwise, that abstraction should be discarded. While kamal is a great alternative to Kubernetes, its a total overkill when hosting simple projects (like this blog).


I hope this post helps you host your blog or other hobby projects using Docker.

Source Code