Webux Pages

Using Nginx + WebDav to create a simple artifacts and static website hosting.

Requirements

  • Private Network (Strongly recommended)
  • Public Network
  • Domain Name
  • Port 8081 and 8082
  • Any CI will work, you need the curl command

The Idea

Having a centralized system to upload my build artifacts securely from the CI server and also host my static websites.

Something like:

[public] artifacts.webux.dev => /<project-name>/<semver and /latest/>/**/**
[private] upload.webux.dev => Using Webdav to upload artifacts and website files, secured using Basic Auth.
[public] Any Domains => /sites/<domain>/**/*

CI

  1. Build your artifacts
  2. Using curl, upload all files to the project directory, e.g. /cerveau/v.1.4.10/<the 4 binaries> and /cerveau/latest/<the 4 binaries> or /sites/example.com/

Example (Woodpecker CI):

when:
  - event: tag

steps:
  release-build:
    image: golang:1.26
    commands:
      - mkdir -p dist
      - GOOS=darwin  GOARCH=amd64 go build -ldflags "-X main.Version=${CI_COMMIT_TAG}" -o dist/cerveau-darwin-amd64  ./cmd/cerveau/
      - GOOS=darwin  GOARCH=arm64 go build -ldflags "-X main.Version=${CI_COMMIT_TAG}" -o dist/cerveau-darwin-arm64  ./cmd/cerveau/
      - GOOS=linux   GOARCH=amd64 go build -ldflags "-X main.Version=${CI_COMMIT_TAG}" -o dist/cerveau-linux-amd64   ./cmd/cerveau/
      - GOOS=linux   GOARCH=arm64 go build -ldflags "-X main.Version=${CI_COMMIT_TAG}" -o dist/cerveau-linux-arm64   ./cmd/cerveau/

  release-upload:
    image: curlimages/curl
    environment:
      WEBUX_PAGES_UPLOAD_PASSWORD:
        from_secret: webux_pages_upload_password
      WEBUX_PAGES_UPLOAD_URL:
        from_secret: webux_pages_upload_url
    commands:
      - |
        for f in dist/*; do
          name=$(basename "$f")
          curl -sf -u "ci-uploader:$${WEBUX_PAGES_UPLOAD_PASSWORD}" -T "$f" \
            "$${WEBUX_PAGES_UPLOAD_URL}/artifacts/cerveau/${CI_COMMIT_TAG}/$${name}"
          curl -sf -u "ci-uploader:$${WEBUX_PAGES_UPLOAD_PASSWORD}" -T "$f" \
            "$${WEBUX_PAGES_UPLOAD_URL}/artifacts/cerveau/latest/$${name}"
        done

Network / Gateway

My setup uses an HAProxy in front of the K3s Cluster, so I redirect from that HAProxy using the sni to Envoy Gateway (which serves some of my publicly accessible services) hosted on the cluster.

Internet -> artifacts.webux.dev -> HAProxy -> Envoy Gateway -> Webux Pages Pod [Port 8081] -> Nginx
Internal Network (VPN) -> upload.webux.dev -> HAProxy -> Gateway API -> Webux Pages Pod [Port 8082] -> Nginx -> Basic Auth

Nginx + WebDav Configuration

I used that image: nginxinc/nginx-unprivileged:1.30.3-alpine3.23-otel

You can use whatever nginx image or version as long as it supports webdav.

load_module modules/ngx_otel_module.so;

user  nginx;
worker_processes  auto;

error_log  /var/log/nginx/error.log notice;
pid        /tmp/nginx.pid;

events {
  worker_connections  1024;
}

http {
  include       /etc/nginx/mime.types;
  default_type  application/octet-stream;
  sendfile      on;
  keepalive_timeout 65;

  limit_req_zone $binary_remote_addr zone=webux_pages_write:10m rate=5r/s;

  map $host $safe_host {
     default "";
     "~^(?<h>[a-zA-Z0-9.-]+)$" $h;
  }

  otel_exporter {
     endpoint monitoring-alloy.monitoring.svc.cluster.local:4317;
  }
  otel_service_name webux-pages;

  server {
     listen 8081;
     server_name artifacts.webux.dev;
     server_tokens off;

     otel_trace on;
     otel_trace_context propagate;

     add_header X-Content-Type-Options nosniff always;
     add_header X-Frame-Options DENY always;

     location / {
         limit_except GET HEAD {
             deny all;
         }

         alias /data/artifacts/;
         autoindex on;
     }
  }

  server {
     listen 8081 default_server;
     server_name _;
     server_tokens off;

     otel_trace on;
     otel_trace_context propagate;

     add_header X-Content-Type-Options nosniff always;
     add_header X-Frame-Options DENY always;

     if ($safe_host = "") {
         return 400;
     }

     root /data/sites/$safe_host;
     index index.html;

     location / {
         limit_except GET HEAD {
             deny all;
         }

         try_files $uri $uri/ =404;
     }
  }

  server {
     listen 8082;
     server_tokens off;

     otel_trace on;
     otel_trace_context propagate;

     location / {
         return 403;
     }

     location /artifacts/ {
         limit_req zone=webux_pages_write burst=10 nodelay;

         auth_basic "webux-pages upload";
         auth_basic_user_file /etc/nginx/secrets/.htpasswd;

         client_max_body_size 500m;

         dav_methods PUT DELETE;
         create_full_put_path on;
         client_body_temp_path /tmp/nginx_dav;

         root /data;
     }

     location /sites/ {
         limit_req zone=webux_pages_write burst=10 nodelay;

         auth_basic "webux-pages upload";
         auth_basic_user_file /etc/nginx/secrets/.htpasswd;

         client_max_body_size 500m;

         dav_methods PUT DELETE;
         create_full_put_path on;
         client_body_temp_path /tmp/nginx_dav;

         root /data;
     }
  }
}
  • This configuration forces you to define at least an index.html to get your website to load.
  • The artifacts will automatically list all the content, so be careful when you upload your artifacts.

The .htpasswd

CI_PASSWORD=$(openssl rand -base64 24)
echo "Save this password now: $CI_PASSWORD"

htpasswd -Bnb ci-uploader "$CI_PASSWORD" > .htpasswd

The container

I won't publish my actual kubernetes setup as it is specific for my cluster anyway. If you use Cilium, you're gonna have to review your policies to allow OTEL, and external/internal traffic to the different networks, etc.

Ports:

  • 8081:8081
  • 8082:8082

Volumes:

  • [PVC:50GB]: /data
  • ./nginx.conf:/etc/nginx/nginx.conf (The config above)
  • ./.htpasswd:/etc/nginx/secrets/.htpasswd

Static Website Example

curl -u ci-uploader:<password> -X DELETE https://upload.webux.dev/sites/example.com/
curl -u ci-uploader:<password> -T ./index.html https://upload.webux.dev/sites/example.com/index.html