My website and blog setup
Introduction
So I decided to redo my site infrastructure after using several different providers for my site and blog over the years. I wanted it to be all hosted in the same place and easy to maintain and deploy. I had some experience with Hugo in the past and decided to give it another try.
This blog will describe how I setup a server with Caddy as a static site hoster with hands-off TLS certificate provisioning, access logs processed by GoAccess behind oauth2 authentication and auto deployment through a Github Action that builds and deploys my site when needed.
So far I am happy with it, it’s hazzle free and I can focus on writing instead of struggling with 3rd party providers or sites to get things published. It also gives me full control over the source and content of my site.
I decided to write this to mainly document it for myself but also for other people to use and/or have opinions about.
Hosting
It needs to be hosted somewhere and I did not want to host it on my homelab server(s). I decided to rent a VPS. Use whatever you see fit.
Hugo - site/blog
I am not much for any dynamic content, I write static text so any tool that can generate a website for me with some non-fancy theme so I can focus on writing content (in Markdown…) works the best for me. Hugo provides that for me.
Hugo provides a static binary that can generate my website, offers basic themes and can spin-up a development server to check my content before publishing. Install Hugo on your local development machine, pick a theme and start writing content. Run through the quick-start guide here to get a feel for how Hugo works.
You do not need Hugo on your server as we will use Github Actions to generate the site and push the static html to the server. I will explain how to set that up further down.
I use the simple monochrome theme and my hugo.toml
file looks like this.
baseURL = "https://hacktobeer.eu/"
languageCode = "en-us"
title = "hacktobeer's website"
theme = "hugo-theme-monochrome"
[params]
enable_zooming_js = false
footer = "© 2023 by hacktobeer"
color_scheme = "dark"
[params.list_layout]
enable_group_by_year = false
[param.syntax_highlight]
lib = "builtin"
[param.syntax_highlight.builtin]
enable_code_copy = true
# For code blocks highlighting
pygmentsUseClasses=true
pygmentsCodeFences=true
[[menu.navbar]]
identifier = "about"
name = "about"
title = "about"
url = "/about/"
weight = 100
[[menu.navbar]]
identifier = "blog"
name = "blog"
title = "blog"
url = "/posts/"
weight = 100
Caddy - webserver
If you want to host a site yourself, you need a webserver. I have used both apache and nginx in the past and they are powerful web servers but after thinking about my requirements I came to another conclusion. My main requirements boil down to 2 things: easy SSL/TLS certificate management and static file hosting. So I decided to use Caddy. Easy to setup and manage and enough modules for future expansion if needed.
Download and install Caddy from here and make sure to select the following 2 modules so they are included in your Caddy binary:
- caddy.logging.encoders.formatted - for the Apache compatible combined log output
- security - the greenpau/caddy-security module for oauth2 support
My configuration can be seen here, replace [domainname]
with your own domain:
[domainname] {
tls [address]@[domainname]
encode gzip zstd
file_server
root * /var/www/html/[domainname]/public
log {
output file /var/log/caddy/[domainname]/access.log
format transform `{request>remote_ip} - {request>user_id} [{ts}] "{request>method} {request>uri} {request>proto}" {status} {size} "{request>headers>Referer>[0]}" "{request>headers>User-Agent>[0]}"` {
time_format "02/Jan/2006:15:04:05 -0700"
}
}
}
My full configuration contains definitions and oauth2 authentication entries for the logs server as well which will be explained below.
Point the [domainname]
to the public IP address of your server in your DNS zone and Caddy wil automatically request, configure (and renew) Let’s Encrypt TLS certificates for your domain. It can not get easier for self-hosting I think.
A simple sudo systemctl reload caddy
will reconfigure your server after changing the configuration. Should things go wrong have a look at the journal logs with sudo journalctl --no-pager -u caddy
.
GoAccess - accesslogs
So you have your server setup and want to have some graphs for your access logs. I decided to go with GoAccess. Standalone binary and easy to configure. The time of Webalizer and awstats is behind us.
Download and install GoAccess according to the installation documentation. I decided to created a systemd configuration file that runs GoAccess on my logs every 5 minutes and host the output on my logs.
subdomain.
Here is my GoAccess systemd file. Create it with systemctl edit --full goaccess
:
[Unit]
Description=GoAccess Live Log Analyzer
After=caddy.service
[Service]
Type=simple
ExecStart=/usr/bin/goaccess [path_to_access_log] -o /var/www/html/[domainname]/logs/index.html --log-format=COMBINED
RestartSec=300
User=caddy
Group=caddy
Restart=always
[Install]
WantedBy=multi-user.target
Combined with another Caddy subdomain denifition in /etc/caddy/Caddyfile
.
logs.[domainname] {
tls [address]@[domainname]
encode gzip zstd
file_server
root * /var/www/html/[domainname]/logs
}
This will give you a nice 5 minute overview of your access logs for your viewing pleasure. But it is not locked down, everybody can reach it over the internet. I decided to lock it down with oauth2. Feel free to skip this ‘complicated’ oauth2 configuration and opt for basic-auth as explained here.
Caddy - oauth2 (optional)
Caddy can lock down (sub)domains with oauth2 so I decided to try that for the GoAccess logs. I use Google OAuth2 OpenID which gives me the option to authenticate with my Google Account.
Create an auth
subdomain in your DNS zone file that points to your public IP address of your server. This endpoint will serve the oauth2 flow on your server.
I created a client_id and client_secret as described here. Make sure that the ‘Authorized redirect URI’ is set to point to your auth
subdomain as shows in the configuration below.
After getting the Google oauth2 id and secret, add them to your caddy systemd startup file (systemctl edit caddy
).
[Service]
Environment="JWT_SHARED_KEY=[random-string]"
Environment="GOOGLE_CLIENT_ID=[your-client-id]"
Environment="GOOGLE_CLIENT_SECRET=[your-client-secret]"
Have configured the Google side of your oauth2 flow, we now need to configure Caddy to lock down the log subdomain. Add the following to your Caddy configuration file.
{
order authenticate before respond
order authorize before basicauth
security {
oauth identity provider google {
realm google
driver google
client_id {env.GOOGLE_CLIENT_ID}
client_secret {env.GOOGLE_CLIENT_SECRET}
scopes openid email profile
}
authentication portal authportal {
crypto default token lifetime 3600
crypto key sign-verify {env.JWT_SHARED_KEY}
enable identity provider google
cookie domain [domainname]
transform user {
match realm google
match email [gmail-address-you-added-as-Test-User-in-Google]
action add role authp/admin
}
}
authorization policy authpolicy {
set auth url https://auth.[domainname]/oauth2/google/authorization-code-callback
crypto key verify {env.JWT_SHARED_KEY}
allow roles authp/admin
validate bearer header
inject headers with claims
}
}
}
auth.hacktobeer.eu {
authenticate with authportal
}
Again, issue a sudo servicectl reload caddy
to reload the Caddy configuration.
You should now be asked for oauth2 authentication when trying to access https://logs.[domainname]/
.
Github Action - publish
I manage my site in a private Github repository so I can have some control over the release process and easy access from everywhere (git clone
or Github Codespace). When I have written a new blog post and have verified it locally by running hugo serve -D
I change the post draft status to false and push the change to my Github repository. Each push triggers a Github Action that builds the website with Hugo and pushes it to my server with rsync. The configuration for this flows is stored in .github\workflows\deploy.yml
.
name: Deploy Blog
on:
push:
branches: [master]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
submodules: 'true'
- name: Setup Hugo
uses: peaceiris/actions-hugo@v2
with:
hugo-version: "0.121.1"
extended: true
- name: Hugo build
run: |
cd $GITHUB_WORKSPACE
hugo --environment production --minify
- name: Rsync Deployments Action
uses: Burnett01/rsync-deployments@5.2
with:
switches: -avzr --delete
path: public/
remote_path: /var/www/html/[domainname]/public
remote_host: ${{ secrets.SSH_HOST }}
remote_user: ${{ secrets.SSH_USER }}
remote_key: ${{ secrets.SSH_KEY }}
The ssh host, user and key are stored as Secrets in the repository as explained here.
Note: feel free to add a command
configuration to your authorized_keys
file on your server to lockdown the access for that key.
With this in place I can focus on writing my blog posts, set the draft
status to true
to prevent publishing, push any changes to Github to not loose work and auto-publish whenever I am ready by changing the draft
status for that post.
Conclusion
I think I have found the sweet spot between self-hosting my site/blog without the maintenance burden and an easy publish flow so I can focus on writing. So far it makes me happy!
Relevant configuration files
Caddy
/etc/caddy/Caddyfile
{
order authenticate before respond
order authorize before basicauth
security {
oauth identity provider google {
realm google
driver google
client_id {env.GOOGLE_CLIENT_ID}
client_secret {env.GOOGLE_CLIENT_SECRET}
scopes openid email profile
}
authentication portal authportal {
crypto default token lifetime 3600
crypto key sign-verify {env.JWT_SHARED_KEY}
enable identity provider google
cookie domain [domainname]
transform user {
match realm google
match email [gmail-address-you-added-as-Test-User-in-Google]
action add role authp/admin
}
}
authorization policy authpolicy {
set auth url https://auth.[domainname]/oauth2/google/authorization-code-callback
crypto key verify {env.JWT_SHARED_KEY}
allow roles authp/admin
validate bearer header
inject headers with claims
}
}
}
[domainname] {
tls [address]@[domainnam]
encode gzip zstd
file_server
root * /var/www/html/[domainname]/public
log {
output file /var/log/caddy/[domainname].access.log
format transform `{request>remote_ip} - {request>user_id} [{ts}] "{request>method} {request>uri} {request>proto}" {status} {size} "{request>headers>Referer>[0]}" "{request>headers>User-Agent>[0]}"` {
time_format "02/Jan/2006:15:04:05 -0700"
}
}
}
logs.[domainname] {
authorize with authpolicy
header x-forwarded-proto https
tls [address]@[domainname]
encode gzip zstd
file_server
root * /var/www/html/[domainname]/logs
}
auth.[domainname] {
authenticate with authportal
}
Caddy oauth2
systemctl edit caddy
[Service]
Environment="JWT_SHARED_KEY=[random-string]"
Environment="GOOGLE_CLIENT_ID=[your-client-id]"
Environment="GOOGLE_CLIENT_SECRET=[your-client-secret]"
GoAccess
systemctl edit --full goaccess
[Unit]
Description=GoAccess Live Log Analyzer
After=caddy.service
[Service]
Type=simple
ExecStart=/usr/bin/goaccess [path_to_access_log] -o /var/www/html/[domainname]/logs/index.html --log-format=COMBINED
RestartSec=300
User=caddy
Group=caddy
Restart=always
[Install]
WantedBy=multi-user.target
Hugo TOML
hugo.toml
baseURL = "https://hacktobeer.eu/"
languageCode = "en-us"
title = "hacktobeer's website"
theme = "hugo-theme-monochrome"
[params]
enable_zooming_js = false
footer = "© 2023 by hacktobeer"
color_scheme = "dark"
[params.list_layout]
enable_group_by_year = false
[param.syntax_highlight]
lib = "builtin"
[param.syntax_highlight.builtin]
enable_code_copy = true
# For code blocks highlighting
pygmentsUseClasses=true
pygmentsCodeFences=true
[[menu.navbar]]
identifier = "about"
name = "about"
title = "about"
url = "/about/"
weight = 100
[[menu.navbar]]
identifier = "blog"
name = "blog"
title = "blog"
url = "/posts/"
weight = 100