Shulker Store

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.

Updated recently 18 min read Guide

How It Works

Go Reverse Proxy
Lightweight proxy that forwards traffic to your Shulker Store, injecting your store ID automatically.
Cloudflare Tunnel
Maps your custom domain to the proxy — no open ports, no static IP needed.
DevSpace or Docker
Run the proxy on Shulker DevSpace (recommended) or your own infrastructure.

📡 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.

Shulker DevSpace (Recommended)
Managed cloud environments on Shulker infrastructure. Built-in port forwarding, no server required.
Self-hosted Docker
Run on your own VPS or local machine with Docker. Full control over resources.

📦 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.

main.go (full)
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

Terminal
# 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:

Your public endpoint (example)
in1.shulker.in:1002   # your port number will differ
1
Open DevSpace
Navigate to shulker.in/devspace and open your environment
2
Ports tab
Go to the Ports tab inside your environment settings
3
Add port forward
Click Add Port Forward and enter port 80
4
Copy public address
Copy the generated public address — it will look like in1.shulker.in:XXXX

Step 4 — Point Your Domain via Cloudflare Tunnel

Map your custom subdomain to the proxy for a clean https:// URL with no port number.

A
Create a Cloudflare Tunnel
In your Cloudflare Zero Trust dashboard, go to Networks → Tunnels → Create a tunnel. Choose "Cloudflared" and name it (e.g., shulker-store).
B
Install cloudflared & authenticate
Run the token command provided by Cloudflare in your DevSpace terminal.
C
Add a Public Hostname
Configure: Subdomain (store), Domain (yourdomain.com), Service Type (HTTP), URL (in1.shulker.in:1002).
D
Done — visit your domain
After a minute or two, visit 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

FlagDefaultDescription
--idrequiredYour Shulker Store ID (e.g., 130). Find this in your store dashboard URL.
--port80Port 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.