Skip to content

Commit b3da730

Browse files
fix: proxy docker-compose.yml from rustfs/rustfs via Cloudflare (#106)
* fix: host docker-compose.yml and sync it from rustfs/rustfs Serve the Compose file from public/ so rustfs.com/docker-compose.yml stops 404ing, and add a dispatch/schedule workflow that keeps it in sync with rustfs/rustfs without empty commits. Co-authored-by: RustFS <hello@rustfs.com> * fix: proxy docker-compose.yml from rustfs/rustfs via Cloudflare Drop the git-hosted copy and hourly sync workflow. The existing rustfs-com Worker now pass-throughs /docker-compose.yml from rustfs/rustfs main so curl -O keeps a rustfs.com URL and filename. Co-authored-by: RustFS <hello@rustfs.com> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com>
1 parent 5f90ec5 commit b3da730

6 files changed

Lines changed: 254 additions & 3 deletions

File tree

‎app/download/components/platforms/docker-download-section.tsx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ export default function DockerDownloadSection({ platform, release, className }:
8080
</p>
8181
</div>
8282
<a
83-
href="https://github.com/rustfs/rustfs/blob/main/docker-compose.yml"
83+
href="https://rustfs.com/docker-compose.yml"
8484
target="_blank"
8585
rel="noopener noreferrer"
8686
className="inline-flex items-center space-x-2 px-4 py-2 bg-primary text-primary-foreground rounded-lg hover:bg-primary/90 transition-colors">
@@ -91,7 +91,7 @@ export default function DockerDownloadSection({ platform, release, className }:
9191

9292
<CodeBlock
9393
code={[
94-
"wget https://raw.githubusercontent.com/rustfs/rustfs/main/docker-compose.yml",
94+
"curl -O https://rustfs.com/docker-compose.yml",
9595
"docker compose up -d",
9696
]}
9797
title={'Installation Commands'}

‎package.json‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010
"start": "next start",
1111
"lint": "eslint .",
1212
"check:homepage-metrics-worker": "pnpm run typecheck:homepage-metrics-worker && pnpm run test:homepage-metrics-worker && wrangler deploy --dry-run --config workers/homepage-metrics/wrangler.jsonc",
13+
"check:site-worker": "pnpm run typecheck:site-worker && pnpm run test:site-worker",
14+
"test:site-worker": "node --experimental-strip-types --test workers/site/src/index.test.ts",
15+
"typecheck:site-worker": "tsc --noEmit -p workers/site/tsconfig.json",
1316
"postbuild": "node scripts/generate-sitemap.js",
1417
"sitemap": "node scripts/generate-sitemap.js",
1518
"sync:blog": "node scripts/sync-wordpress-blog.mjs",

‎workers/site/src/index.test.ts‎

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import assert from "node:assert/strict";
2+
import test from "node:test";
3+
import worker, {
4+
DOCKER_COMPOSE_UPSTREAM,
5+
proxyDockerCompose,
6+
type SiteEnv,
7+
} from "./index.ts";
8+
9+
const composeBody = "services:\n rustfs:\n image: rustfs/rustfs:latest\n";
10+
11+
function assetsEnv(handler: (request: Request) => Promise<Response> | Response): SiteEnv {
12+
return {
13+
ASSETS: {
14+
fetch: async (request) => handler(request),
15+
},
16+
};
17+
}
18+
19+
test("proxies GET docker-compose.yml from rustfs/rustfs and keeps the filename", async () => {
20+
const response = await proxyDockerCompose(
21+
new Request("https://rustfs.com/docker-compose.yml"),
22+
async (input, init) => {
23+
assert.equal(input, DOCKER_COMPOSE_UPSTREAM);
24+
assert.equal(init?.method, "GET");
25+
assert.equal(
26+
(init?.headers as Record<string, string>)["User-Agent"],
27+
"rustfs.com-docker-compose-proxy",
28+
);
29+
return new Response(composeBody, {
30+
status: 200,
31+
headers: { ETag: '"abc"', "Content-Type": "text/plain" },
32+
});
33+
},
34+
);
35+
36+
assert.equal(response.status, 200);
37+
assert.equal(await response.text(), composeBody);
38+
assert.equal(response.headers.get("Content-Type"), "text/yaml; charset=utf-8");
39+
assert.equal(
40+
response.headers.get("Content-Disposition"),
41+
'inline; filename="docker-compose.yml"',
42+
);
43+
assert.equal(response.headers.get("ETag"), '"abc"');
44+
});
45+
46+
test("supports HEAD for curl -I without a body", async () => {
47+
const response = await proxyDockerCompose(
48+
new Request("https://rustfs.com/docker-compose.yml", { method: "HEAD" }),
49+
async (_input, init) => {
50+
assert.equal(init?.method, "HEAD");
51+
return new Response(composeBody, { status: 200 });
52+
},
53+
);
54+
55+
assert.equal(response.status, 200);
56+
assert.equal(await response.text(), "");
57+
assert.equal(
58+
response.headers.get("Content-Disposition"),
59+
'inline; filename="docker-compose.yml"',
60+
);
61+
});
62+
63+
test("rejects non-GET methods", async () => {
64+
const response = await proxyDockerCompose(
65+
new Request("https://rustfs.com/docker-compose.yml", { method: "POST" }),
66+
async () => {
67+
throw new Error("must not fetch upstream");
68+
},
69+
);
70+
71+
assert.equal(response.status, 405);
72+
assert.equal(response.headers.get("Allow"), "GET, HEAD");
73+
});
74+
75+
test("returns 502 when upstream is unavailable", async () => {
76+
const response = await proxyDockerCompose(
77+
new Request("https://rustfs.com/docker-compose.yml"),
78+
async () => new Response("missing", { status: 404 }),
79+
);
80+
81+
assert.equal(response.status, 502);
82+
assert.match(await response.text(), /Failed to fetch docker-compose.yml/);
83+
});
84+
85+
test("does not forward visitor cookies to GitHub", async () => {
86+
const response = await proxyDockerCompose(
87+
new Request("https://rustfs.com/docker-compose.yml", {
88+
headers: { Cookie: "session=secret" },
89+
}),
90+
async (_input, init) => {
91+
const headers = new Headers(init?.headers);
92+
assert.equal(headers.get("Cookie"), null);
93+
return new Response(composeBody, { status: 200 });
94+
},
95+
);
96+
97+
assert.equal(response.status, 200);
98+
});
99+
100+
test("routes only /docker-compose.yml through the proxy", async () => {
101+
const originalFetch = globalThis.fetch;
102+
let proxied = false;
103+
globalThis.fetch = (async () => {
104+
proxied = true;
105+
return new Response(composeBody, { status: 200 });
106+
}) as typeof fetch;
107+
108+
try {
109+
const env = assetsEnv(async (request) => new Response(`asset:${new URL(request.url).pathname}`));
110+
const compose = await worker.fetch(new Request("https://rustfs.com/docker-compose.yml"), env);
111+
const other = await worker.fetch(new Request("https://rustfs.com/install_rustfs.sh"), env);
112+
113+
assert.equal(proxied, true);
114+
assert.equal(await compose.text(), composeBody);
115+
assert.equal(await other.text(), "asset:/install_rustfs.sh");
116+
} finally {
117+
globalThis.fetch = originalFetch;
118+
}
119+
});

‎workers/site/src/index.ts‎

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
const FETCH_TIMEOUT_MS = 10_000;
2+
const FETCH_ATTEMPTS = 3;
3+
4+
export const DOCKER_COMPOSE_PATH = "/docker-compose.yml";
5+
export const DOCKER_COMPOSE_UPSTREAM =
6+
"https://raw.githubusercontent.com/rustfs/rustfs/main/docker-compose.yml";
7+
8+
const RESPONSE_HEADERS = {
9+
"Cache-Control": "public, max-age=300, stale-while-revalidate=3600",
10+
"Content-Disposition": 'inline; filename="docker-compose.yml"',
11+
"Content-Type": "text/yaml; charset=utf-8",
12+
"X-Content-Type-Options": "nosniff",
13+
};
14+
15+
export interface SiteEnv {
16+
ASSETS: {
17+
fetch(request: Request): Promise<Response>;
18+
};
19+
}
20+
21+
type FetchImpl = (
22+
input: string,
23+
init?: RequestInit,
24+
) => Promise<Response>;
25+
26+
async function fetchUpstream(
27+
method: "GET" | "HEAD",
28+
fetchImpl: FetchImpl,
29+
): Promise<Response> {
30+
let lastError: unknown;
31+
32+
for (let attempt = 1; attempt <= FETCH_ATTEMPTS; attempt += 1) {
33+
try {
34+
const response = await fetchImpl(DOCKER_COMPOSE_UPSTREAM, {
35+
method,
36+
headers: {
37+
Accept: "text/yaml, text/plain;q=0.9, */*;q=0.8",
38+
"User-Agent": "rustfs.com-docker-compose-proxy",
39+
},
40+
redirect: "follow",
41+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
42+
});
43+
44+
if (response.ok) {
45+
return response;
46+
}
47+
48+
await response.body?.cancel();
49+
lastError = new Error(`Upstream returned HTTP ${response.status}`);
50+
} catch (error) {
51+
lastError = error;
52+
}
53+
}
54+
55+
throw lastError instanceof Error ? lastError : new Error("Upstream request failed");
56+
}
57+
58+
export async function proxyDockerCompose(
59+
request: Request,
60+
fetchImpl: FetchImpl = (input, init) => globalThis.fetch(input, init),
61+
): Promise<Response> {
62+
if (request.method !== "GET" && request.method !== "HEAD") {
63+
return new Response("Method Not Allowed\n", {
64+
status: 405,
65+
headers: { Allow: "GET, HEAD" },
66+
});
67+
}
68+
69+
try {
70+
const upstream = await fetchUpstream(request.method, fetchImpl);
71+
const headers = new Headers(RESPONSE_HEADERS);
72+
const etag = upstream.headers.get("ETag");
73+
const lastModified = upstream.headers.get("Last-Modified");
74+
75+
if (etag) {
76+
headers.set("ETag", etag);
77+
}
78+
if (lastModified) {
79+
headers.set("Last-Modified", lastModified);
80+
}
81+
82+
if (request.method === "HEAD") {
83+
await upstream.body?.cancel();
84+
return new Response(null, { status: 200, headers });
85+
}
86+
87+
return new Response(upstream.body, { status: 200, headers });
88+
} catch (error) {
89+
console.error(JSON.stringify({
90+
event: "docker_compose_proxy_failed",
91+
error: error instanceof Error ? error.message : String(error),
92+
}));
93+
94+
return new Response("Failed to fetch docker-compose.yml\n", {
95+
status: 502,
96+
headers: { "Content-Type": "text/plain; charset=utf-8" },
97+
});
98+
}
99+
}
100+
101+
const worker = {
102+
async fetch(request: Request, env: SiteEnv): Promise<Response> {
103+
const url = new URL(request.url);
104+
if (url.pathname === DOCKER_COMPOSE_PATH) {
105+
return proxyDockerCompose(request);
106+
}
107+
108+
return env.ASSETS.fetch(request);
109+
},
110+
};
111+
112+
export default worker;

‎workers/site/tsconfig.json‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"extends": "../../tsconfig.json",
3+
"compilerOptions": {
4+
"incremental": false,
5+
"allowImportingTsExtensions": true
6+
},
7+
"include": [
8+
"src/**/*.ts"
9+
],
10+
"exclude": [
11+
"node_modules"
12+
]
13+
}

‎wrangler.jsonc‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@
22
"$schema": "./node_modules/wrangler/config-schema.json",
33
"name": "rustfs-com",
44
"compatibility_date": "2026-07-30",
5+
// Proxies /docker-compose.yml from rustfs/rustfs; every other path stays a static asset.
6+
"main": "workers/site/src/index.ts",
57
"assets": {
6-
"directory": "./out"
8+
"directory": "./out",
9+
"binding": "ASSETS",
10+
"run_worker_first": ["/docker-compose.yml"]
711
}
812
}

0 commit comments

Comments
 (0)