In 2025, building your own website can be as easy or hard as you want it to be. There are plenty of platforms out there, of varying prices, morals and ethics, that can make you a website in minutes. For people who want slightly more control, there are a variety of static site generators in the programming language of your choice, as well as easy hosting options such as Github pages. Or you could go with a dedicated blogging platform such as Bear Blog that provides a relatively cheap all-in-one service with some degree of autonomy.
My approach to building this website was not only to produce a site that I could maintain completely independently, following the IndieWeb key principles, but also to learn more web development. In particular, becoming more confident hooking up all the interlocking pieces of software needed to host a website end-to-end. Here's how I did it.
Structure
If you're new to building software, even if you have some coding experience, it can be overwhelming learning which software does what and interlacing all their dependencies in the most optimal and safest manner. Personally, I find this the more difficult task when approaching new software than using the software itself. So at a high level, this is what is going on to produce this web page:
- A backend web-framework written in Rust, which builds the HTML files from templates, handles all the URL routing, CSS styling, and serves the site on the local web server of the machine running the code.
- The Rust code is packaged into a
Docker container,
meaning all the code can be served using a simple
call to
docker compose up
. - The code is hosted at a
Github repository and
Github Actions are used to build the Docker image and copy it to a
Hetzner
virtual private
server connected to the internet and remotely
accessible with SSH.
The builds are currently only triggered on
merged pull requests to the
main
branch, but another possibility is to set up automated builds at a cadence that makes sense. - The virtual server's IP address points to
my domain name
cmgoold.com
, meaning that I can usehttp://cmgoold.com:<port>
to access my website rather thanhttp://<long-ip-address>:<port>
. - Finally, I use
nginx
as a reverse proxy to route
http://cmgoold.com:<port>
to bothhttp://cmgoold.com
and it's safer default varianthttps://cmgoold.com
. Of course, you can just typecmgoold.com
and it will take you to the HTTPS-enabled site directly, all thanks to nginx.
Building a website using Rust
This website's tooling is no doubt overkill for what could have been made via any of the static site generators out there. But I've been dappling with Rust for a little while now, and wanted a more involved project to build some Rust muscle memory. I would also like to maintain this site in the long term and its functionality is still open-ended. The current setup provides me autonomy and flexibility.
I am using the Rust crate
actix_web
as the web framework. Rust has a number of
crates for web development, although none
quite as expansive as something like
Ruby on Rails
or Python's
Django
or PHP's Laravel.
actix_web
appeared to be relatively mature
compared to some other Rust crates, and has features
such as asynchronous handlers.
Here's the 'hello world' equivalent for
web development in actix_web
.
Assuming you have Rust installed, run:
cargo new my-site && cd my-site
and edit the Cargo.toml
file to include:
[dependencies]
actix-web = "4" # change to the most recent version
In src/main.rs
add the following code:
use actix_web::{get, web, App, HttpServer, Responder};
#[get("/")]
async fn index() -> impl Responder {
"Hello, World!"
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(|| App::new().service(index))
.bind(("127.0.0.1", 8080))?
.run()
.await
}
Enter http://127.0.0.1:8080
into your web browser
and you should see "Hello, World!" on a blank page.
The code for this small example is available in this post's directory at the Github repository.
I initially followed the blog by
Morten Vistisen to create this website,
but the above bare-bones
approach is really all you need to get started.
You can expand by defining additional service functions
(like index()
above) in a separate file, like the
handlers.rs
file I use, and adding in HTML templates using
Tera, to which you
can layer on CSS styling or other frameworks of your choice.
Writing posts in HTML
I'm not the biggest lover of mark-up languages like Markdown or reStructuredText. I'm perhaps one of the minority that like to retain the control of the lower-level language and not add an additional package to the workflow. I've also run into issues with mark-up languages where, at some point, a feature is not available without esoteric workarounds, or with static site generators where certain HTML classes are not clearly accessible, and other odd bugs.
A first version of this site used Markdown to write posts, but I've since changed to using raw HTML. I ran into issues rendering LaTeX math environments, and that is a necessary feature here. While raw HTML is slightly more bloated, I'm quite used to writing in e.g. LaTeX, and so it doesn't pose too much of an inconvenience.
The post metadata is stored in a metadata.toml
TOML file.
This idea was taken from
Morten Vistisen's blog post.
Choosing Hetzner
I deliberated for a quite a while on how to actually host this site. I wanted to go with a more local company that used renewable energy to power their own servers, like Krystal, but I also wanted control over how I deployed my website. I wanted to spin up a Docker image directly, and I couldn't quite see how that was possible with some of my preferred vendors. Hetzner is a well-known name in the area with servers in Europe that are well-protected by data protection laws. They have a pretty good sustainability statement that mentions renewable energy, although I found it slightly more difficult to find than some other options. Take from that what you will, I'm definitely not an expert on this topic.
In terms of cost, I chose the cheapest plan at about \$5 per month, which is a shared vCPU with 4 GB of RAM and 40 GB of inbuilt storage. I think this will be fine for my needs, and I wouldn't want to spend anything more than \$5 a month. They have plenty of additional plans should you need it, or I in the future, however.
The Hetzner setup was pretty smooth sailing if you're comfortable with basic server terminology and logging in remotely by, ideally, SSH.
nginx reverse proxy
To clean up the site URLs, I used nginx
as a reverse proxy, along with using
Let's Encrypt
and
Certbot
to generate SSL certificates to enable HTTPS.
I added nginx into my
compose-prod.yaml
file.
You can see my
nginx.conf
file if you need the details, and how it
interacts with with the compose.prod.yaml
workflow.
One simple tip if you are going this route: use
docker compose up
rather than docker compose up -d
to build your site when developing, as you can more
clearly see the logs. I couldn't get the reverse proxy
to work for quite a while due to not seeing an erroneous
path that was assumed in the nginx.conf
file but
didn't exist in the Docker container. Running
docker compose up -d
swallowed the
warning/error message, and I was left
with just the nginx server running but no re-routing
of my host URIs.
Adding tests
I still need to add unit and integration tests for this site. I have a development Docker compose file that is built as a Github action, but that's only a placeholder for more specific end-to-end tests.