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
curlcommand
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
- Build your artifacts
- 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.htmlto 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