Building a Hybrid Web Station: RPi5, Docker, and Cloudflare Tunnels

Building a Hybrid Web Station: RPi5, Docker, and Cloudflare Tunnels

After getting my hands on a Raspberry Pi 5, I wanted to move beyond a simple local server. My goal was to host a multi-service web station (a Go/SQLite analytics app and various static projects) while keeping my main blog on GitLab Pages.

Here is how I navigated the world of reverse proxies, containerization, and the dreaded “Protocol Errors.”

The Architecture

The setup is a “Hybrid” model:

  • Root Domain (homecircuits.eu): Hosted on GitLab Pages.
  • App Subdomain (app.homecircuits.eu): Tunneled to my RPi5 via Cloudflare cloudflared.

1. The Container Stack

I used Docker Compose to manage three distinct services:

  1. Nginx Proxy: The gatekeeper that routes traffic based on URL paths.
  2. Static Service: An Alpine-based Nginx container serving all static frontend projects (like my Snake game).
  3. Analytics Service: A custom Go application using a CGO-free SQLite driver (modernc.org/sqlite) for data persistence.

Organizing Static Projects

To make the station future-proof, I moved the Snake game into a generic /static/ path. Now, I can access it at: https://app.homecircuits.eu/static/snake/


2. Bridging the Gap: Cloudflare Tunnels

Since I didn’t want to open ports on my home router (and deal with dynamic IPs), I used Cloudflare Tunnels.

Key Configuration

The config.yml for cloudflared was the heart of the connectivity:

tunnel: <TUNNEL_ID>
credentials-file: /etc/cloudflared/<TUNNEL_ID>.json
protocol: http2

ingress:
  - hostname: app.homecircuits.eu
    service: http://localhost:80
  - service: http_status:404

Pro Tip: Forcing protocol: http2 was necessary to prevent ERR_QUIC_PROTOCOL_ERROR caused by home routers struggling with UDP/QUIC packets.


3. The Reverse Proxy Logic

I configured the main Nginx gateway to handle the routing so I could host multiple projects on a single subdomain:

  • app.homecircuits.eu/ -> A simple 200 “Online” message.
  • app.homecircuits.eu/static/ -> Points to the internal static-service.
  • app.homecircuits.eu/analytics/ -> Points to the Go/SQLite app.
location /static/ {
    proxy_pass http://static-service:80/;
    proxy_set_header Host $host;
}

location /analytics/ {
    proxy_pass http://analytics-service:8080/;
    proxy_set_header Host $host;
}

4. Debugging the “Final Bosses”

We ran into two major hurdles during this build:

The “No Such File” Error

Even when the Go build succeeded, the container would fail with stat /app/main: no such file. The Fix: Ensuring the binary was built with CGO_ENABLED=0 to make it a truly static binary that doesn’t rely on libraries missing in the minimal Alpine image.

The DNSSEC Conflict

After setting up the tunnel, the site was still timing out or showing GitLab content. The Lesson: DNSSEC adds a layer of cryptographic signing that can conflict when you have multiple providers (GitLab + Cloudflare Tunnel) on the same domain. Turning off DNSSEC at the registrar and Cloudflare allowed the new CNAME records to propagate correctly.

The “502 Bad Gateway” Permission Trap

If your Go app crashes after adding a Docker volume, it’s likely a permission mismatch. Docker volumes often default to root ownership. Use this fix on the host machine: sudo chown -R $USER:$USER ~/my-web-station/analytics-app/data This ensures the containerized app can actually write to the SQLite database on your Pi’s disk.


Final Results

Now, my RPi5 sits quietly on my desk, serving a database-backed application to the world via a secure tunnel, while my main content remains safely on GitLab.