Skip to content

Add download-beta page with live build progress#555

Open
elee1766 wants to merge 4 commits into
masterfrom
download-beta
Open

Add download-beta page with live build progress#555
elee1766 wants to merge 4 commits into
masterfrom
download-beta

Conversation

@elee1766

@elee1766 elee1766 commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

New /download-beta page that provides live build progress when downloading custom Caddy builds.

Pages

/download-beta -- Plugin selection

  • Same look and feel as the existing /download page (same CSS, header, footer, package icons, download counts, version inputs, module docs previews)
  • Vanilla JS instead of jQuery (no dependencies)
  • Clicking Download posts to POST /api/build, then navigates to the build page
  • If the build is already cached, skips straight to the download

/download-beta/build/?key={hash} -- Build progress

  • Connects to GET /api/build/{key}/events via SSE
  • Shows current build step as a prominent label with a progress bar
  • Scrolling build log with live output
  • Auto-downloads the artifact on completion
  • Fallback "click here if download did not start" link
  • Error state with "Try Again" button linking back to the selection page

User flow

  1. User selects platform and plugins, clicks Download
  2. POST /api/build returns the build key
  3. Browser navigates to /download-beta/build/?key=abc123
  4. SSE streams step/log events in real time
  5. On result event with success: true, the download starts automatically
  6. If the user refreshes or shares the URL, they reconnect and get all buffered events replayed

Multiple users building the same configuration share progress -- the first request triggers the build and everyone else joins the same stream.


Local test setup

This requires three services running together: the build worker, the portal (API), and the website (frontend). You'll also need PostgreSQL.

Related PRs

  • buildworker: caddyserver/buildworker#6 (streaming endpoint)
  • portal: caddyserver/portal#10 (streaming build API, depends on caddyserver/portal#9)
  • xcaddy: elee1766/xcaddy@observable-builds (OnStep callback)

1. Build the custom Caddy binaries

Build worker binary (includes the streaming build handler):

mkdir -p /tmp/caddybuilder/buildworker && cd /tmp/caddybuilder/buildworker

cat > main.go << 'EOF'
package main

import (
    caddycmd "github.com/caddyserver/caddy/v2/cmd"
    _ "github.com/caddyserver/caddy/v2/modules/standard"
    _ "github.com/caddyserver/buildworker"
)

func main() { caddycmd.Main() }
EOF

go mod init caddybuilder/buildworker
go mod edit -replace github.com/caddyserver/buildworker=<path-to-buildworker-checkout>
go mod edit -replace github.com/caddyserver/xcaddy=github.com/elee1766/xcaddy@v0.0.0-20260708021516-c9f96ab945a0
GOPROXY=direct go mod tidy
go build -o /tmp/caddybuilder/caddy-buildworker .

Portal binary (includes the API handler):

mkdir -p /tmp/caddybuilder/portal && cd /tmp/caddybuilder/portal

cat > main.go << 'EOF'
package main

import (
    caddycmd "github.com/caddyserver/caddy/v2/cmd"
    _ "github.com/caddyserver/caddy/v2/modules/standard"
    _ "github.com/caddyserver/portal/backend"
)

func main() { caddycmd.Main() }
EOF

go mod init caddybuilder/portal
go mod edit -replace github.com/caddyserver/portal=<path-to-portal-checkout>
go mod tidy
go build -o /tmp/caddybuilder/caddy-portal .

2. PostgreSQL setup

Create the database and load the schema:

psql -h 127.0.0.1 -p 5432 -U postgres -c "CREATE DATABASE caddy"
psql -h 127.0.0.1 -p 5432 -U postgres -d caddy -f <path-to-portal-checkout>/db.sql

Seed with the top 10 plugins for testing:

-- dummy account for foreign key
INSERT INTO accounts (id, name, email, password)
VALUES ('f89c8f71-263d-445f-9bab-f18b3d91c85b', 'Test', 'test@test.local', '\x00')
ON CONFLICT DO NOTHING;

-- top 10 plugins by download count
INSERT INTO packages (id, account_id, path, listed, available, downloads) VALUES ('1fab5617-a93f-4617-b886-cfc8e44253ae', 'f89c8f71-263d-445f-9bab-f18b3d91c85b', 'github.com/greenpau/caddy-security', true, true, 1574081) ON CONFLICT DO NOTHING;
INSERT INTO packages (id, account_id, path, listed, available, downloads) VALUES ('7cdc6c32-a6c4-40f0-93d1-4fb470915528', 'f89c8f71-263d-445f-9bab-f18b3d91c85b', 'github.com/caddy-dns/route53', true, true, 1553610) ON CONFLICT DO NOTHING;
INSERT INTO packages (id, account_id, path, listed, available, downloads) VALUES ('e97af045-03cf-427c-96f5-5607516080e7', 'f89c8f71-263d-445f-9bab-f18b3d91c85b', 'github.com/caddy-dns/cloudflare', true, true, 395614) ON CONFLICT DO NOTHING;
INSERT INTO packages (id, account_id, path, listed, available, downloads) VALUES ('f567fc49-b0d5-4340-99a4-e6f02703af65', 'f89c8f71-263d-445f-9bab-f18b3d91c85b', 'github.com/dunglas/vulcain/caddy', true, true, 204568) ON CONFLICT DO NOTHING;
INSERT INTO packages (id, account_id, path, listed, available, downloads) VALUES ('20925127-9a0c-4b73-8fc9-ece394d54833', 'f89c8f71-263d-445f-9bab-f18b3d91c85b', 'github.com/dunglas/mercure/caddy', true, true, 203633) ON CONFLICT DO NOTHING;
INSERT INTO packages (id, account_id, path, listed, available, downloads) VALUES ('093c4864-5c74-4d2d-b75e-9756ba00278d', 'f89c8f71-263d-445f-9bab-f18b3d91c85b', 'github.com/caddyserver/replace-response', true, true, 117680) ON CONFLICT DO NOTHING;
INSERT INTO packages (id, account_id, path, listed, available, downloads) VALUES ('1de15c74-1a7e-4465-8279-ecfe9e5b3765', 'f89c8f71-263d-445f-9bab-f18b3d91c85b', 'github.com/sjtug/caddy2-filter', true, true, 88317) ON CONFLICT DO NOTHING;
INSERT INTO packages (id, account_id, path, listed, available, downloads) VALUES ('ecead391-831c-4f52-a99a-0eb403e07c9e', 'f89c8f71-263d-445f-9bab-f18b3d91c85b', 'github.com/caddyserver/transform-encoder', true, true, 65432) ON CONFLICT DO NOTHING;
INSERT INTO packages (id, account_id, path, listed, available, downloads) VALUES ('cae8ce32-6e4c-4a26-ae98-e5bc33cfd0a1', 'f89c8f71-263d-445f-9bab-f18b3d91c85b', 'github.com/mastercactapus/caddy2-proxyprotocol', true, true, 45868) ON CONFLICT DO NOTHING;
INSERT INTO packages (id, account_id, path, listed, available, downloads) VALUES ('7d996720-ae83-4a79-8684-cba7f1fa682a', 'f89c8f71-263d-445f-9bab-f18b3d91c85b', 'github.com/caddy-dns/duckdns', true, true, 43306) ON CONFLICT DO NOTHING;

3. Config files

Build worker (buildworker.json) -- serves on :8888:

{
  "admin": {"disabled": true},
  "apps": {
    "http": {
      "servers": {
        "worker": {
          "listen": ["127.0.0.1:8888"],
          "routes": [
            {
              "match": [{"path": ["/streaming-build"]}],
              "handle": [{"handler": "caddy_builder_streaming", "timeout": "10m"}]
            },
            {
              "match": [{"path": ["/build"]}],
              "handle": [{"handler": "caddy_builder", "timeout": "10m"}]
            }
          ]
        }
      }
    }
  }
}

Portal (Caddyfile.portal) -- API on :4444, proxies to worker:

{
    order portal last
    admin off
}

:4444 {
    portal {
        dsn "postgresql://postgres:postgres@127.0.0.1:5432/caddy?sslmode=disable"
        build_worker_endpoint 127.0.0.1:5555
        streaming_build_worker_endpoint 127.0.0.1:5556
    }
}

# legacy build worker proxy
:5555 {
    bind 127.0.0.1
    reverse_proxy 127.0.0.1:8888 {
        rewrite /build
    }
}

# streaming build worker proxy
:5556 {
    bind 127.0.0.1
    reverse_proxy 127.0.0.1:8888 {
        rewrite /streaming-build
    }
}

Website (Caddyfile.website) -- frontend on :2015:

{
    admin off
}

:2015 {
    root * <path-to-website-checkout>/src
    file_server
    templates
    encode zstd gzip
    try_files {path}.html {path}
    reverse_proxy /api/* 127.0.0.1:4444
}

4. Run

In three terminals:

# Terminal 1: build worker
./caddy-buildworker run --config buildworker.json

# Terminal 2: portal
./caddy-portal run --config Caddyfile.portal --adapter caddyfile

# Terminal 3: website (stock caddy binary works)
caddy run --config Caddyfile.website --adapter caddyfile

Then open http://localhost:2015/download-beta

Supervisor script

#!/bin/bash
set -e
DIR="$(cd "$(dirname "$0")" && pwd)"

cleanup() { kill $PID1 $PID2 $PID3 2>/dev/null; wait 2>/dev/null; }
trap cleanup EXIT INT TERM

"$DIR/caddy-buildworker" run --config "$DIR/buildworker.json" &
PID1=$!; sleep 1

"$DIR/caddy-portal" run --config "$DIR/Caddyfile.portal" --adapter caddyfile &
PID2=$!; sleep 1

caddy run --config "$DIR/Caddyfile.website" --adapter caddyfile &
PID3=$!; sleep 1

echo "Ready: http://localhost:2015/download-beta"
wait

elee1766 added 4 commits July 7, 2026 22:43
New /download-beta page that uses the streaming build API:

- Same look and feel as the existing /download page (same CSS,
  header, footer, package rendering with icons, download counts,
  version inputs, module docs previews)
- Vanilla JS instead of jQuery
- Clicking Download posts to /api/build, then navigates to
  /download-beta/build/?key={hash} for live progress
- Build page shows current step label, progress bar, and
  scrolling build log via SSE
- Auto-downloads the artifact on completion with a fallback
  'click here' link
- If the build is already cached, skips straight to download
When the build server retries on a different worker (503 from a
draining node), the SSE stream sends a 'reset' event. The frontend
clears the build log, resets the progress bar, and shows a message
that the build is retrying on another server.
The download-beta page now has a collapsible PGO Optimization section:
- None (default): no PGO, same as before
- Default: selects a pre-configured server-side profile
- Upload: lets users upload their own .pprof file

When a named profile is selected, it's sent as a ?pgo= query param.
When a custom profile is uploaded, it's base64-encoded and sent in
the POST body. A badge shows 'enabled' when PGO is active.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant