Custom Domain Setup Go Proxy + Cloudflare
Connect your own domain to your Shulker Store. This guide walks you through hosting a lightweight Go reverse proxy, forwarding traffic, and routing your domain with Cloudflare Tunnel.
How It Works
📡 Architecture Flow: Visitor → store.yourdomain.com → Cloudflare Tunnel → Go Proxy :80 → shulker.in/store/?id=YOUR_ID
Step 1 — Host the Proxy Container
Choose between Shulker DevSpace (recommended) or your own Docker environment.
📦 Using DevSpace — Navigate to shulker.in/devspace and create a new environment with the golang:1.22 image.
Step 2 — Add & Run the Proxy Script
Copy the script below into a file called main.go inside your DevSpace or Docker container. This is the complete, production-ready reverse proxy.
package main import ( "compress/gzip" "crypto/tls" "flag" "fmt" "io" "log" "net/http" "net/url" "regexp" "strings" "time" ) type storeProxy struct { storeID string targetBase string client *http.Client } func newStoreProxy(storeID string) *storeProxy { transport := &http.Transport{ TLSClientConfig: &tls.Config{InsecureSkipVerify: false}, MaxIdleConns: 100, IdleConnTimeout: 90 * time.Second, TLSHandshakeTimeout: 10 * time.Second, ExpectContinueTimeout: 1 * time.Second, } client := &http.Client{ Transport: transport, Timeout: 30 * time.Second, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, } return &storeProxy{ storeID: storeID, targetBase: "https://shulker.in", client: client, } } // buildUpstreamURL maps local paths → shulker.in paths and injects store id. func (p *storeProxy) buildUpstreamURL(r *http.Request) string { path := r.URL.Path params, _ := url.ParseQuery(r.URL.RawQuery) params.Set("id", p.storeID) switch { case path == "/" || path == "": // Root → store listing path = "/store/" case strings.HasPrefix(path, "/purchase/") || path == "/purchase": // Purchase links that escaped without /store/ prefix path = "/store" + path // /store/... and /cdn-cgi/... pass through unchanged } return fmt.Sprintf("%s%s?%s", p.targetBase, path, params.Encode()) } // rewriteBody rewrites shulker.in absolute URLs to local relative URLs // so all clicks stay inside the proxy. func (p *storeProxy) rewriteBody(body []byte, contentType string) []byte { if !strings.Contains(contentType, "text/html") && !strings.Contains(contentType, "text/css") && !strings.Contains(contentType, "application/javascript") && !strings.Contains(contentType, "text/javascript") { return body } s := string(body) rewriteURL := func(raw string) string { u, err := url.Parse(raw) if err != nil { return raw } q := u.Query() q.Set("id", p.storeID) u.RawQuery = q.Encode() // Make relative so it hits our proxy u.Scheme = "" u.Host = "" return u.String() } // https://shulker.in/... → /... reAbsolute := regexp.MustCompile(`https://shulker\.in(/[^"' \)>]*)`) s = reAbsolute.ReplaceAllStringFunc(s, func(match string) string { sub := reAbsolute.FindStringSubmatch(match) if len(sub) < 2 { return match } return rewriteURL(sub[1]) }) // //shulker.in/... → /... reProtoRelative := regexp.MustCompile(`//shulker\.in(/[^"' \)>]*)`) s = reProtoRelative.ReplaceAllStringFunc(s, func(match string) string { sub := reProtoRelative.FindStringSubmatch(match) if len(sub) < 2 { return match } return rewriteURL(sub[1]) }) return []byte(s) } // decodeBody decompresses gzip responses. Other encodings are returned as-is. func decodeBody(r io.Reader, encoding string) ([]byte, error) { switch strings.ToLower(strings.TrimSpace(encoding)) { case "gzip": gz, err := gzip.NewReader(r) if err != nil { return nil, fmt.Errorf("gzip reader: %w", err) } defer gz.Close() return io.ReadAll(gz) default: return io.ReadAll(r) } } var hopByHopHeaders = []string{ "Connection", "Keep-Alive", "Proxy-Authenticate", "Proxy-Authorization", "TE", "Trailers", "Transfer-Encoding", "Upgrade", } func removeHopByHop(h http.Header) { for _, hh := range hopByHopHeaders { h.Del(hh) } } // humanHeaders mimics Chrome to avoid Cloudflare blocks. // No brotli — Go stdlib has no built-in br decoder. var humanHeaders = map[string]string{ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8", "Accept-Language": "en-US,en;q=0.9", "Accept-Encoding": "gzip, deflate", "Sec-CH-UA": `"Chromium";v="124", "Google Chrome";v="124", "Not-A.Brand";v="99"`, "Sec-CH-UA-Mobile": "?0", "Sec-CH-UA-Platform": `"Windows"`, "Sec-Fetch-Dest": "document", "Sec-Fetch-Mode": "navigate", "Sec-Fetch-Site": "none", "Sec-Fetch-User": "?1", "Upgrade-Insecure-Requests": "1", "Cache-Control": "max-age=0", } var skipResponseHeaders = map[string]bool{ "content-security-policy": true, "x-frame-options": true, "content-encoding": true, // we decoded it "content-length": true, // body changed size after rewrite "transfer-encoding": true, } func (p *storeProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { upstreamURL := p.buildUpstreamURL(r) log.Printf("[PROXY] %s %s → %s", r.Method, r.URL.String(), upstreamURL) upReq, err := http.NewRequest(r.Method, upstreamURL, r.Body) if err != nil { http.Error(w, "upstream request build failed: "+err.Error(), http.StatusBadGateway) return } // Copy incoming headers then override with browser fingerprint for key, vals := range r.Header { for _, v := range vals { upReq.Header.Add(key, v) } } removeHopByHop(upReq.Header) for k, v := range humanHeaders { upReq.Header.Set(k, v) } upReq.Header.Set("Host", "shulker.in") upReq.Header.Set("Origin", "https://shulker.in") upReq.Header.Set("Referer", fmt.Sprintf("https://shulker.in/store/?id=%s", p.storeID)) for _, cookie := range r.Cookies() { upReq.AddCookie(cookie) } resp, err := p.client.Do(upReq) if err != nil { http.Error(w, "upstream request failed: "+err.Error(), http.StatusBadGateway) return } defer resp.Body.Close() // Rewrite redirect Location headers if loc := resp.Header.Get("Location"); loc != "" { parsed, err := url.Parse(loc) if err == nil && parsed.Host == "shulker.in" { q := parsed.Query() q.Set("id", p.storeID) parsed.RawQuery = q.Encode() parsed.Scheme = "" parsed.Host = "" resp.Header.Set("Location", parsed.String()) } } // Decompress body encoding := resp.Header.Get("Content-Encoding") bodyBytes, err := decodeBody(resp.Body, encoding) if err != nil { log.Printf("[WARN] decompression failed (%s): %v", encoding, err) bodyBytes = []byte{} } contentType := resp.Header.Get("Content-Type") bodyBytes = p.rewriteBody(bodyBytes, contentType) // Forward safe response headers for key, vals := range resp.Header { if skipResponseHeaders[strings.ToLower(key)] { continue } for _, v := range vals { w.Header().Add(key, v) } } w.Header().Set("Content-Type", contentType) w.WriteHeader(resp.StatusCode) w.Write(bodyBytes) } func main() { storeID := flag.String("id", "", "Your Shulker store ID (e.g. 130)") port := flag.Int("port", 80, "Port to listen on") flag.Parse() if *storeID == "" { fmt.Print("Enter your Shulker store ID: ") fmt.Scan(storeID) } if *storeID == "" { log.Fatal("Store ID is required. Use -id=<your_store_id>") } proxy := newStoreProxy(*storeID) addr := fmt.Sprintf(":%d", *port) fmt.Printf("\n") fmt.Printf("╔══════════════════════════════════════════════════╗\n") fmt.Printf("║ Shulker Store Reverse Proxy ║\n") fmt.Printf("╠══════════════════════════════════════════════════╣\n") fmt.Printf("║ Store ID : %-35s║\n", *storeID) fmt.Printf("║ Target : https://shulker.in/store/?id=%-5s║\n", *storeID) fmt.Printf("║ Listening : http://localhost%s%-19s║\n", addr, "") fmt.Printf("╚══════════════════════════════════════════════════╝\n\n") server := &http.Server{ Addr: addr, Handler: proxy, ReadTimeout: 30 * time.Second, WriteTimeout: 60 * time.Second, IdleTimeout: 120 * time.Second, } log.Printf("Server starting on %s", addr) if err := server.ListenAndServe(); err != nil { log.Fatalf("Server error: %v", err) } }
Run the script
# Replace 130 with your actual Shulker Store ID go run main.go --id 130 # Or with a custom port go run main.go --id 130 --port 8080
💡 Set startup command in DevSpace — To have the proxy start automatically when your DevSpace boots, set the Startup Command in DevSpace settings to: go run /path/to/main.go --id YOUR_STORE_ID
Step 3 — Forward the Port via DevSpace
Once your proxy is running on port 80, use DevSpace's port forwarding feature to expose it publicly. DevSpace assigns you a public URL like:
in1.shulker.in:1002 # your port number will differ
80in1.shulker.in:XXXXStep 4 — Point Your Domain via Cloudflare Tunnel
Map your custom subdomain to the proxy for a clean https:// URL with no port number.
shulker-store).store), Domain (yourdomain.com), Service Type (HTTP), URL (in1.shulker.in:1002).https://store.yourdomain.com to see your Shulker Store.✅ SSL is automatic — Cloudflare Tunnel handles HTTPS and SSL certificates for you. No extra configuration needed.
Script Flags Reference
| Flag | Default | Description |
|---|---|---|
| --id | required | Your Shulker Store ID (e.g., 130). Find this in your store dashboard URL. |
| --port | 80 | Port for the proxy to listen on. Change if 80 is taken (e.g., 8080). |
Troubleshooting
🌐 Domain shows Cloudflare error — Make sure your DevSpace proxy is still running (go run main.go --id YOUR_ID) and the environment hasn't been paused.
🔌 Port 80 permission denied — Run with --port 8080 and update your Cloudflare Tunnel target URL accordingly.
🛍️ Store shows wrong content — Double-check your store ID. Find it in the URL: shulker.in/store/?id=YOUR_ID.
🔑 Tunnel token not found — Make sure cloudflared is running with the correct token. The tunnel token is single-use per session.