Blog engine in Go using standard net/http package

Fri May 8 17:22:49 2026
This blog was written in Go completely from scratch. This idea came to me when I was thinking about what to do with my Go knowledge and I decided why not — it was an interesting task. Please excuse me, this post should have appeared a long time ago, but better late than never. Hosting your own blog engine on your own PC can provide you with freedom that you can't get from standard platforms like Medium, and it can be challenging enough to actually learn something new. Under the hood it uses the regular net/http package for serving webpages with a middleware approach for authentication. I also created several endpoints to serve static files like css & js, added OAuth to be able to authenticate using a GitHub account, logging capabilities for events similar to what a reverse proxy like nginx provides, and finally the autocert library for fetching valid certificates. Go was chosen because it is very fast, portable across platforms, and compiles to a single binary — not counting static files like css & js. As a database I chose SQLite. When I first started writing this blog engine, there was a library called libsqlite which heavily used cgo. This means that Go needs to import not Go libraries but C libraries, and we have a problem when, for example, our builder machine is x86 and we are compiling code for arm64. Even though Go can easily cross-compile Go code across different platforms, when dealing with C libraries you need to install the target architecture's dev packages. The solution I used was Debian as the build machine — Debian has good multiarch support, so you can enable the target architecture with dpkg --add-architecture arm64 and then install the ARM64 variants of the required dev packages via apt. You also need a cross-compiler like gcc-aarch64-linux-gnu so that the C code gets compiled for the right target. Once those are in place, a single command is enough to produce a working ARM64 binary:
CC=aarch64-linux-gnu-gcc GOARCH=arm64 GOOS=linux CGO_ENABLED=1 go build
Later I rewrote the engine and switched to modernc.org/sqlite, which is a pure Go port of SQLite with no cgo dependency at all. With that library the cross-compilation problem disappears completely — a plain go build with just GOARCH=arm64 GOOS=linux is all you need. The Orange Pi was chosen because it is a cheap single-board PC on ARM architecture that can run for a long time. For example here is the status of the systemd unit for blog engine as you already see.

sudo systemctl status blog
● blog.service - My Blog engine written on Go
   Loaded: loaded (/etc/systemd/system/blog.service; enabled; vendor preset: enabled)
   Active: active (running) since Sat 2026-04-11 14:32:03 UTC; 3 weeks 6 days ago
 Main PID: 1006 (webserver)
    Tasks: 12 (limit: 2069)
   CGroup: /system.slice/blog.service
           └─1006 /usr/local/bin/webserver

May 08 16:35:25 orangepipc webserver[1006]: 2026/05/08 16:35:25 http: TLS handshake error from 86.105.57.128:53315: tls: unsupported SSLv2 handshake received
May 08 16:36:58 orangepipc webserver[1006]: 2026/05/08 16:36:58 http: TLS handshake error from 3.143.162.210:39060: acme/autocert: missing server name
May 08 16:37:47 orangepipc webserver[1006]: 2026/05/08 16:37:47 http: TLS handshake error from 3.143.162.210:39252: read tcp 192.168.100.100:443->3.143.162.210:39252: i/o timeout
May 08 16:38:06 orangepipc webserver[1006]: 2026/05/08 16:38:06 http: TLS handshake error from 3.143.162.210:49188: acme/autocert: missing server name
May 08 16:38:11 orangepipc webserver[1006]: 2026/05/08 16:38:11 http: TLS handshake error from 77.83.39.232:46238: acme/autocert: missing server name
May 08 16:45:24 orangepipc webserver[1006]: Fri May  8 16:45:24 2026 200 144.76.32.242:36200 GET /robots.txt
May 08 16:45:24 orangepipc webserver[1006]: Fri May  8 16:45:24 2026 302 144.76.32.242:36204 GET /
May 08 16:45:24 orangepipc webserver[1006]: Fri May  8 16:45:24 2026 200 144.76.32.242:36204 GET /page?p=0
May 08 16:45:24 orangepipc webserver[1006]: Fri May  8 16:45:24 2026 302 144.76.32.242:36216 GET /
May 08 16:45:25 orangepipc webserver[1006]: Fri May  8 16:45:25 2026 200 144.76.32.242:36218 GET /page?p=0
I created a separate user in that case here is the systemd unit itself

cat /etc/systemd/system/blog.service 
[Unit]
Description=My Blog engine written on Go
After=network.target

[Service]
User=blog
Group=blog
Type=simple
WorkingDirectory=/home/blog
ExecStart=/usr/local/bin/webserver
Restart=on-failure
EnvironmentFile=/home/blog/env/blog.env

[Install]
WantedBy=multi-user.target
Here is the exact file structure on the server

tree /home/blog/
/home/blog/
├── cert
│   ├── acme_account+key
│   ├── srelog.dev
│   └── srelog.dev+rsa
├── conf.d
│   └── conf.json
├── database
│   └── database.sqlite
├── env
│   └── blog.env
├── log
│   └── access.log
├── public
│   ├── assets
│   │   └── avatar.jpg
│   └── css
│       ├── custom.css
│       ├── github-prettify-theme.css
│       ├── normalize.css
│       └── skeleton.css
├── templates
│   ├── about.gohtml
│   ├── courses.gohtml
│   ├── create.gohtml
│   ├── footer.gohtml
│   ├── header.gohtml
│   ├── links.gohtml
│   ├── login.gohtml
│   ├── post.gohtml
│   ├── posts.gohtml
│   ├── seo_header.gohtml
│   └── update.gohtml
└── uploads
the configuration looks like something like this

 cat /home/blog/conf.d/conf.json 
{
        "server" : {
                "addr":":8080",
                "saddr":":8443"
        }, 
        "database" : {
                "dbpath":"database/database.sqlite"
        },
        "log" : {
                "log":"log/access.log"
        },
        "template" : {
                "tmpath":"templates/*.gohtml"
        },
        "cert" : {
                "domain":"srelog.dev"
        },
        "oauth" : {
                "githubauthorizeurl":"https://github.com/login/oauth/authorize",
                "githubtokenurl":"https://github.com/login/oauth/access_token",
                "redirecturl":"",
                "clientid":"CLIENT_ID",
                "clientsecret":"CLIENT_SECRET"
        }
}
The main challenge I had was configuring port forwarding on my home router. If you have a very cheap router, even if it has DNAT rules, it may simply not work. For example, forwarding everything coming in on the public interface to a private static IP within the local network just didn't work, so I decided to go the easy route: PUBLIC_IP:443 -> PRIVATE_IP:443 and the same for port 80. To do this on Linux you need to set capabilities on your binary, something like this: sudo setcap 'cap_net_bind_service=+ep' /usr/local/bin/webserver Final thoughts it's not the end for sure in the era of the LLM you can easily refactor the application, add new features so maybe I will update the post or will create the new one.

Comments
To leave a comment please login via github

Powered by Golang net/http package