DEV Community

Cover image for Building My Personal Website: From Idea to Automated Deployment (Part 1)
Danylo Mikula
Danylo Mikula

Posted on • Originally published at mikula.dev

Building My Personal Website: From Idea to Automated Deployment (Part 1)

The idea of creating my own personal website—a place where I could share projects I'm working on and document my technical journey—has been on my mind for a long time. But as with many personal projects, it kept getting pushed aside. Finally, I found the time, and here it is: mikula.dev. In this post, I want to share how I built it, what tools I chose, and why.

Choosing the Right Static Site Generator

When looking for a site generator, I had a few requirements in mind: it needed to be simple yet flexible, fast, and shouldn't require hours of configuration just to get started. After evaluating several options, I settled on Hugo.

Hugo is one of the fastest static site generators out there. Written in Go, it can build thousands of pages in seconds. But speed isn't the only advantage—it generates pure static HTML files, which makes hosting incredibly straightforward. No databases, no server-side processing, no complex runtime dependencies. Just files that can be served by any web server.

The fact that Hugo outputs static files also brings security benefits—there's simply no dynamic attack surface. Combined with its extensive templating capabilities and active community, it was an easy choice.

Finding the Perfect Theme

I didn't want to spend weeks developing my own theme from scratch. Instead, I looked for something that matched my aesthetic preferences and could be customized easily. I found Terminal by Radek Kozieł, and it was exactly what I was looking for.

The theme has a clean, retro terminal-inspired look with beautiful syntax highlighting powered by Chroma. It uses Fira Code as the default monospace font, is fully responsive, and supports customizable color schemes. While it covered most of my needs out of the box, I did extend it with some additional functionality—like better post organization and a dedicated resume page.

Where to Host?

Since Hugo generates static files, I had several hosting options to consider: GitHub Pages, AWS S3 with CloudFront, or a small cloud server. Each has its merits, but I went with a dedicated server on Hetzner Cloud.

Why? Flexibility. While GitHub Pages and S3 are excellent for simple static hosting, having my own server gives me complete control over the infrastructure. I can configure custom caching rules, set up rate limiting, add custom header and run additional services if needed. Plus, Hetzner offers excellent performance at very competitive prices.

Caddy: The Modern Web Server

For the web server, I evaluated a few options—nginx, Apache, and Caddy. I chose Caddy for several compelling reasons.

First, automatic HTTPS. Caddy handles SSL certificate provisioning and renewal through Let's Encrypt completely automatically. No more manual certificate management, no cron jobs for renewal, no forgetting to renew and having your site go down. It just works.

Second, simplicity. Caddy's configuration format (the Caddyfile) is remarkably straightforward compared to nginx or Apache configurations. A basic site configuration can be just a few lines, yet it still offers powerful customization options when you need them.

I'm also using a custom Caddy build with additional plugins: caddy-dns/cloudflare for DNS-01 ACME challenges (so I can get certificates even before DNS propagation completes) and caddy-ratelimit to protect against bots and abuse.

DNS and Security with Cloudflare

For DNS management, I'm using Cloudflare. But it's not just about DNS—I have Cloudflare Proxy enabled, which means all traffic to my site goes through Cloudflare's network first. This provides several benefits: DDoS protection, CDN caching, and most importantly, it hides my server's real IP address from the public.

To take security a step further, I configured firewall rules directly in Hetzner Cloud to only allow incoming HTTP/HTTPS traffic from Cloudflare's IP ranges. This means even if someone discovers my server's actual IP address, they can't connect to the web server directly—all requests must go through Cloudflare. This setup effectively creates an additional security layer and ensures that all traffic benefits from Cloudflare's protection.

Cloudflare publishes their IP ranges publicly, so keeping the firewall rules updated is straightforward. Combined with Caddy's rate limiting, this gives me a solid defense-in-depth approach without adding complexity to the daily operations.

Infrastructure as Code with Terraform

As someone who believes in automating everything, I needed proper Infrastructure as Code for my cloud setup. I looked for existing Terraform modules for Hetzner Cloud but didn't find anything that met my standards for flexibility and maintainability. So I built my own.

I created a set of reusable Terraform modules that cover the essential Hetzner Cloud resources:

These modules are designed to work together seamlessly while remaining flexible enough for various use cases. They're all open source and available on both GitHub and the Terraform Registry.

Configuration Management with Ansible

With infrastructure sorted out, I needed a way to automate the actual server configuration and site deployment. For this, I created an Ansible collection: ansible-hugo-deploy.

This collection handles the complete deployment pipeline:

  • Installing and configuring Caddy with custom builds (including the Cloudflare DNS and rate limiting plugins)
  • Generating SSH deploy keys for secure repository access
  • Cloning the Hugo site from GitHub
  • Building the site with Hugo
  • Obtaining and managing SSL certificates
  • Configuring rate limiting and security headers
  • Setting up automated content updates via systemd timer

The systemd timer runs daily, pulling the latest changes from the repository and rebuilding the site. This means I can just push a new post to GitHub, and within a day (or I can trigger it manually), the site updates automatically. No SSH-ing into the server, no manual deployments.

The Complete Picture

Here's how everything fits together:

  1. Terraform provisions the infrastructure on Hetzner Cloud—server, network, firewall rules (allowing only Cloudflare IPs), and SSH keys
  2. Ansible configures the server—installs Caddy, Hugo, sets up the deployment pipeline
  3. Hugo generates the static site from Markdown content
  4. Caddy serves the site with automatic HTTPS, compression, and rate limiting
  5. Cloudflare handles DNS, proxies all traffic, provides DDoS protection and CDN caching
  6. systemd timer keeps the content automatically updated

The entire setup—from bare server to fully functional website—takes about 15 minutes. And once it's running, I never need to touch the server for content updates. Write a post in Markdown, push to GitHub, and the site updates itself.

What's Next?

In the second part of this series, I'll dive deeper into the technical details of both the Terraform modules and the Ansible collection. I'll walk through the code, explain the design decisions, and show how you can use these tools for your own projects.

All the code is open source and available on my GitHub:

Feel free to use them, contribute, or just take inspiration for your own automation journey!

Top comments (0)