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
•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.