# Deploying a Github Mirror Using Caddy


Strings to replace:

```
github.my-gh-proxy.com
assets.my-gh-proxy.com
avatars.my-gh-proxy.com
raw.my-gh-proxy.com
api.my-gh-proxy.com
codeload.my-gh-proxy.com
objects.my-gh-proxy.com
release-assets.my-gh-proxy.com
gist.my-gh-proxy.com
.my-gh-proxy.com
```

```caddyfile
# Basic proxy header configuration
(proxy_defaults) {
    # Spoof Host header; automatically fetches reverse proxy target hostname via upstream_hostport
    header_up Host {upstream_hostport}

    # Pass real client information
	header_up X-Real-IP {remote}

    # Critical: Disable upstream compression to allow the replace module to modify response bodies
    header_up Accept-Encoding identity
}

# Privacy and security headers
(security_headers) {
    header X-Robots-Tag "noindex, nofollow, noarchive"
    header X-Content-Type-Options "nosniff"
	# Remove CSP to prevent script blocking due to domain mismatches
    header -Content-Security-Policy
}

# 1. Primary domain: github.my-gh-proxy.com
github.my-gh-proxy.com {
    # Enable compression on proxy side to improve client loading speed
    encode zstd gzip

    import security_headers

	reverse_proxy https://github.com {
        import proxy_defaults

        # Redirect rewriting
        header_down Location https://github.com https://github.my-gh-proxy.com
        header_down Location https://objects.githubusercontent.com https://objects.my-gh-proxy.com
        header_down Location https://release-assets.githubusercontent.com https://release-assets.my-gh-proxy.com
        header_down Location https://raw.githubusercontent.com https://raw.my-gh-proxy.com

        # Cookie domain rewrite (regular expression match)
        header_down Set-Cookie "(.*)Domain=\.github\.com(.*)" "$1Domain=.my-gh-proxy.com$2"
    }

	# Content Replacement Rules
    replace {
		stream
		"https://github.com" "https://github.my-gh-proxy.com"
		"https://github.githubassets.com" "https://assets.my-gh-proxy.com"
		"https://avatars.githubusercontent.com" "https://avatars.my-gh-proxy.com"
		"https://raw.githubusercontent.com" "https://raw.my-gh-proxy.com"
		"https://api.github.com" "https://api.my-gh-proxy.com"
		"https://codeload.github.com" "https://codeload.my-gh-proxy.com"
		"https://objects.githubusercontent.com" "https://objects.my-gh-proxy.com"
		"https://release-assets.githubusercontent.com" "https://release-assets.my-gh-proxy.com"
		"https://gist.github.com" "https://gist.my-gh-proxy.com"
	}
}

# 2. Static resources: assets.my-gh-proxy.com
assets.my-gh-proxy.com {
    encode zstd gzip
    import security_headers

    reverse_proxy https://github.githubassets.com {
        import proxy_defaults
		header_down Access-Control-Allow-Origin "*"
    }
}

# 3. Avatar Service: avatars.my-gh-proxy.com
avatars.my-gh-proxy.com {
    encode zstd gzip
    import security_headers

	reverse_proxy https://avatars.githubusercontent.com {
        import proxy_defaults
        header_down Access-Control-Allow-Origin "*"
    }
}

# 4. Raw Files: raw.my-gh-proxy.com
raw.my-gh-proxy.com {
    encode zstd gzip
	import security_headers

    reverse_proxy https://raw.githubusercontent.com {
        import proxy_defaults
        header_down Access-Control-Allow-Origin "*"
    }

    replace {
        stream
        "https://github.com" "https://github.my-gh-proxy.com"
		"https://raw.githubusercontent.com" "https://raw.my-gh-proxy.com"
    }
}

# 5. API Service: api.my-gh-proxy.com
api.my-gh-proxy.com {
    encode zstd gzip
    import security_headers

	reverse_proxy https://api.github.com {
        import proxy_defaults
        # Fix pagination links
        header_down Link "https://api.github.com" "https://api.my-gh-proxy.com"
        header_down Location https://api.github.com https://api.my-gh-proxy.com
	}

    # Replace URLs in JSON responses
    replace {
        stream
        "https://api.github.com" "https://api.my-gh-proxy.com"
        "https://github.com" "https://github.my-gh-proxy.com"
		"https://raw.githubusercontent.com" "https://raw.my-gh-proxy.com"
    }
}

# 6. Code Download (Zip): codeload.my-gh-proxy.com
codeload.my-gh-proxy.com {
    encode zstd gzip
    import security_headers

	reverse_proxy https://codeload.github.com {
        import proxy_defaults
    }
}

# 7. Object Storage (LFS/Releases): objects.my-gh-proxy.com
objects.my-gh-proxy.com {
    # Disable compression to improve large file transfer efficiency (avoid CPU waste)
	# import security_headers # Object storage typically doesn't require excessive security header interference

    reverse_proxy https://objects.githubusercontent.com {
        import proxy_defaults
        # Ensure Authorization header remains unmodified, pass-through S3 signature
    }
}

# 7.5. Release Assets (moved to a separate domain by GitHub in 2026): release-assets.my-gh-proxy.com
# Without this site, release downloads get a 302 redirect to release-assets.githubusercontent.com,
# and clients will hit GitHub's official domain directly, bypassing the mirror (and the official
# domain is unreachable under restricted networks).
release-assets.my-gh-proxy.com {
	# Same as objects: disable compression to avoid CPU waste on large files
	# import security_headers

    reverse_proxy https://release-assets.githubusercontent.com {
        import proxy_defaults
    }
}

# 8. Gist Service: gist.my-gh-proxy.com
gist.my-gh-proxy.com {
    encode zstd gzip
    import security_headers

    reverse_proxy https://gist.github.com {
        import proxy_defaults
        header_down Location https://gist.github.com https://gist.my-gh-proxy.com
		header_down Set-Cookie "(.*)Domain=\.github\.com(.*)" "$1Domain=.my-gh-proxy.com$2"
    }

	replace {
        stream
        "https://gist.github.com" "https://gist.my-gh-proxy.com"
        "https://github.com" "https://github.my-gh-proxy.com"
		"https://github.githubassets.com" "https://assets.my-gh-proxy.com"
        "https://avatars.githubusercontent.com" "https://avatars.my-gh-proxy.com"
    }
}

```

## Advanced: Basic Auth and Same-Origin Setup

Mirror sites usually shouldn't be public. The simplest protection is adding basic auth to each site block:

```caddyfile
github.my-gh-proxy.com {
	basic_auth {
		user $2a$14$xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
	}
	# ...rest of the config unchanged
}
```

Generate the hash with `caddy hash-password --plaintext 'your-password'`.

But multi-subdomain + basic auth has a pitfall: **browsers store credentials per hostname**. Once you enter the password on `github.my-gh-proxy.com`, the browser won't automatically send those credentials to `assets.my-gh-proxy.com` — so the homepage loads but all cross-origin resources (JS/CSS/avatars) return 401, leaving a bare skeleton. You'd have to enter the password on every subdomain, and again after switching devices or clearing the cache.

The fix is **same-origin setup**: keep only the primary domain and route every upstream by path with `handle_path`, so a single credential works everywhere:

```caddyfile
github.my-gh-proxy.com {
	basic_auth {
		user $2a$14$xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
	}

	# Static resources: github.githubassets.com
	handle_path /ghassets/* {
		encode zstd gzip
		reverse_proxy https://github.githubassets.com {
			import proxy_defaults
			header_down Access-Control-Allow-Origin "*"
		}
	}

	# Avatars: avatars.githubusercontent.com
	handle_path /ghavatars/* {
		encode zstd gzip
		reverse_proxy https://avatars.githubusercontent.com {
			import proxy_defaults
			header_down Access-Control-Allow-Origin "*"
		}
	}

	# Raw files: raw.githubusercontent.com
	handle_path /ghraw/* {
		encode zstd gzip
		reverse_proxy https://raw.githubusercontent.com {
			import proxy_defaults
			header_down Access-Control-Allow-Origin "*"
		}
	}

	# API: api.github.com
	handle_path /ghapi/* {
		encode zstd gzip
		reverse_proxy https://api.github.com {
			import proxy_defaults
			header_down Link "https://api.github.com" "https://github.my-gh-proxy.com/ghapi"
			header_down Location https://api.github.com https://github.my-gh-proxy.com/ghapi
		}
	}

	# Code download (Zip): codeload.github.com
	handle_path /ghcodeload/* {
		encode zstd gzip
		reverse_proxy https://codeload.github.com {
			import proxy_defaults
		}
	}

	# Object storage (LFS): objects.githubusercontent.com
	handle_path /ghobjects/* {
		reverse_proxy https://objects.githubusercontent.com {
			import proxy_defaults
		}
	}

	# Release assets: release-assets.githubusercontent.com
	handle_path /ghreleaseassets/* {
		reverse_proxy https://release-assets.githubusercontent.com {
			import proxy_defaults
		}
	}

	# Gist: gist.github.com
	handle_path /ghgist/* {
		encode zstd gzip
		reverse_proxy https://gist.github.com {
			import proxy_defaults
			header_down Location https://gist.github.com https://github.my-gh-proxy.com/ghgist
			header_down Set-Cookie "(.*)Domain=\.github\.com(.*)" "$1Domain=.my-gh-proxy.com$2"
		}
	}

	# Primary site: github.com (fallback, matches every other path)
	handle {
		encode zstd gzip
		import security_headers

		reverse_proxy https://github.com {
			import proxy_defaults

			header_down Location https://github.com https://github.my-gh-proxy.com
			header_down Location https://objects.githubusercontent.com https://github.my-gh-proxy.com/ghobjects
			header_down Location https://release-assets.githubusercontent.com https://github.my-gh-proxy.com/ghreleaseassets
			header_down Location https://raw.githubusercontent.com https://github.my-gh-proxy.com/ghraw
			header_down Location https://codeload.github.com https://github.my-gh-proxy.com/ghcodeload

			header_down Set-Cookie "(.*)Domain=\.github\.com(.*)" "$1Domain=.my-gh-proxy.com$2"
		}

		replace {
			stream
			"https://github.com" "https://github.my-gh-proxy.com"
			"https://github.githubassets.com" "https://github.my-gh-proxy.com/ghassets"
			"https://avatars.githubusercontent.com" "https://github.my-gh-proxy.com/ghavatars"
			"https://raw.githubusercontent.com" "https://github.my-gh-proxy.com/ghraw"
			"https://api.github.com" "https://github.my-gh-proxy.com/ghapi"
			"https://codeload.github.com" "https://github.my-gh-proxy.com/ghcodeload"
			"https://objects.githubusercontent.com" "https://github.my-gh-proxy.com/ghobjects"
			"https://release-assets.githubusercontent.com" "https://github.my-gh-proxy.com/ghreleaseassets"
			"https://gist.github.com" "https://github.my-gh-proxy.com/ghgist"
		}
	}
}
```

Key points:

- `handle_path` **strips the prefix before forwarding**: `/ghassets/assets/x.css` → upstream receives `/assets/x.css`, no manual rewrite needed.
- The primary site uses a matcher-less `handle` as fallback, so every non-split path (login, repository pages, etc.) still goes to github.com.
- The per-upstream `replace` rules merge into the primary site block, with replacement targets pointing at same-origin paths.
- With this setup the `assets/avatars/raw/api/codeload/objects/release-assets/gist` subdomains no longer need DNS records or certificates — all traffic goes through one domain.


---

> Author: Nite  
> URL: https://www.nite07.com/en/posts/github-mirror-caddy/  

