Privacy CAPTCHA
    Go / Docker

    Deploy a Self-Hosted ALTCHA CAPTCHA Server on a VPS

    Run a privacy-first proof-of-work CAPTCHA on a RamNode VPS — Go challenge server or GateCHA Docker image behind a TLS reverse proxy.

    ALTCHA is a privacy first, open source alternative to reCAPTCHA and hCaptcha. Instead of making users solve image puzzles or quietly shipping their behavioral data to a third party, ALTCHA uses a proof of work mechanism: the visitor's browser quietly solves a small cryptographic challenge in the background before a form is submitted. There are no cookies, no fingerprinting, and no external service calls, which makes it GDPR friendly and accessible out of the box.

    The catch is that ALTCHA is not a single download you start like a normal app. It has two halves: a front end web component (the widget), and a back end that issues HMAC signed challenges and verifies the proofs. To self host it on a RamNode VPS, you deploy that back end. This guide shows how to run a small, dedicated ALTCHA challenge server behind a reverse proxy, and how to wire the widget into a form.

    How the pieces fit

    shell
    Browser (ALTCHA widget) --GET challenge--> Your ALTCHA server --> signed challenge
    Browser solves proof of work
    Browser submits form with proof --> Your back end --verify--> ALTCHA server (or shared HMAC key)

    The security contract is an HMAC key shared between the server that issues challenges and the code that verifies them. As long as the same key signs and checks, a proof cannot be forged or replayed.

    Choosing an approach

    There are three realistic self hosted paths:

    1. A small custom server using one of the official ALTCHA libraries (Go, Node, Python, PHP, and others). This is the cleanest and most flexible. We will use the Go library because it compiles to a single static binary that is trivial to run as a systemd service.
    2. GateCHA, a community, MIT licensed server that implements the ALTCHA protocol with API keys, multi site support, replay protection, and a statistics dashboard. It uses embedded SQLite, ships as a roughly 15 MB Docker image, and is a turnkey alternative to the paid ALTCHA Sentinel.
    3. ALTCHA Sentinel, the vendor's commercial managed offering, deployed via Docker with a trial that starts on install. Worth knowing about, but not what most self hosters want.

    This guide covers options 1 and 2.

    Prerequisites

    • A RamNode VPS running Ubuntu 24.04 LTS. The smallest plan is more than enough; the challenge server is tiny.
    • Root or sudo access.
    • A subdomain such as altcha.example.com pointing at the VPS, for clean TLS.
    • Ports 80 and 443 open.

    Option 1: A custom Go challenge server

    Step 1: Install Go

    shell
    sudo apt update
    sudo apt install -y golang-go git
    go version

    Step 2: Write a minimal challenge and verify server

    Create a project directory:

    shell
    mkdir -p ~/altcha-server && cd ~/altcha-server
    go mod init altcha-server
    go get github.com/altcha-org/altcha-lib-go

    Create main.go:

    shell
    package main
    
    import (
        "encoding/json"
        "net/http"
        "os"
        "time"
    
        "github.com/altcha-org/altcha-lib-go"
    )
    
    var hmacKey = os.Getenv("ALTCHA_HMAC_KEY")
    
    func challengeHandler(w http.ResponseWriter, r *http.Request) {
        challenge, err := altcha.CreateChallenge(altcha.ChallengeOptions{
            HMACKey:    hmacKey,
            MaxNumber:  100000, // difficulty: higher means more work
            Expires:    timePtr(time.Now().Add(5 * time.Minute)),
        })
        if err != nil {
            http.Error(w, "failed to create challenge", http.StatusInternalServerError)
            return
        }
        w.Header().Set("Content-Type", "application/json")
        w.Header().Set("Access-Control-Allow-Origin", "https://example.com")
        json.NewEncoder(w).Encode(challenge)
    }
    
    func verifyHandler(w http.ResponseWriter, r *http.Request) {
        payload := r.FormValue("altcha")
        ok, err := altcha.VerifySolution(payload, hmacKey, true)
        w.Header().Set("Content-Type", "application/json")
        if err != nil || !ok {
            json.NewEncoder(w).Encode(map[string]bool{"verified": false})
            return
        }
        json.NewEncoder(w).Encode(map[string]bool{"verified": true})
    }
    
    func timePtr(t time.Time) *time.Time { return &t }
    
    func main() {
        http.HandleFunc("/challenge", challengeHandler)
        http.HandleFunc("/verify", verifyHandler)
        http.ListenAndServe("127.0.0.1:8090", nil)
    }

    Adjust MaxNumber to tune difficulty. Higher values cost spammers more CPU while staying invisible to legitimate users. Replace the Access-Control-Allow-Origin value with the real origin of the site that hosts your forms.

    Step 3: Build and run as a service

    Build the binary:

    shell
    go build -o altcha-server
    sudo mv altcha-server /usr/local/bin/altcha-server

    Generate a strong HMAC key and store it where systemd can read it:

    shell
    openssl rand -hex 32

    Create /etc/altcha.env with ALTCHA_HMAC_KEY=<your key> and lock down permissions:

    shell
    sudo chmod 600 /etc/altcha.env

    Create /etc/systemd/system/altcha.service:

    shell
    [Unit]
    Description=ALTCHA challenge server
    After=network.target
    
    [Service]
    EnvironmentFile=/etc/altcha.env
    ExecStart=/usr/local/bin/altcha-server
    DynamicUser=yes
    Restart=on-failure
    
    [Install]
    WantedBy=multi-user.target

    Start it:

    shell
    sudo systemctl daemon-reload
    sudo systemctl enable --now altcha

    Step 4: Add the widget to your form

    On the page that hosts your form, include the widget from npm or a CDN and point it at your challenge endpoint:

    shell
    <script async defer src="https://cdn.jsdelivr.net/npm/altcha/dist/altcha.min.js" type="module"></script>
    
    <form action="/submit" method="POST">
      <input type="email" name="email" required />
      <altcha-widget challengeurl="https://altcha.example.com/challenge"></altcha-widget>
      <button type="submit">Send</button>
    </form>

    The widget adds a hidden altcha field containing the solved proof. Your back end then posts that value to /verify, or verifies it inline using the same HMAC key and one of the ALTCHA server libraries. Never trust a submission that fails verification.


    Option 2: GateCHA, the turnkey route

    If you would rather not write code, GateCHA gives you an ALTCHA compatible server with a dashboard, per site API keys, and built in replay protection.

    Step 1: Install Docker

    shell
    curl -fsSL https://get.docker.com | sudo sh
    sudo usermod -aG docker $USER

    Log out and back in.

    Step 2: Run GateCHA

    shell
    docker run -d --name gatecha \
      -p 127.0.0.1:8080:8080 \
      -v gatecha_data:/app/data \
      -e GATECHA_ADMIN_PASSWORD="$(openssl rand -hex 16)" \
      --restart unless-stopped \
      ghcr.io/upellift99/gatecha:latest

    Print the password you generated from your shell history or set a known one. SQLite is embedded, so there is no database to provision. The dashboard runs on port 8080, bound to localhost so only the reverse proxy can reach it.

    Step 3: Create a site key

    Log in to the dashboard, create a new site with your domain and a difficulty setting, and copy the issued API key. You then point the official ALTCHA widget at GateCHA's challenge URL using that key, exactly as in Option 1, and verify proofs through GateCHA's verify endpoint. Consumed challenges are tracked and rejected on reuse automatically.


    Reverse proxy and TLS

    Either option needs a TLS front end. Caddy is the least effort:

    shell
    sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https
    curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
    curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list
    sudo apt update && sudo apt install -y caddy

    Set /etc/caddy/Caddyfile:

    shell
    altcha.example.com {
        # 8090 for the Go server, 8080 for GateCHA
        reverse_proxy 127.0.0.1:8090
    }

    Reload:

    shell
    sudo systemctl restart caddy

    Firewall

    shell
    sudo ufw allow OpenSSH
    sudo ufw allow 80/tcp
    sudo ufw allow 443/tcp
    sudo ufw enable

    The challenge server stays bound to localhost and is reached only through Caddy.

    Operational notes

    • Difficulty tuning. Start moderate and watch real user experience. If you face determined bot farms with GPU or ASIC hardware, ALTCHA v3 supports the memory bound algorithms Argon2id and Scrypt, which resist that acceleration. These require importing the matching widget workers separately.
    • Rotate the HMAC key carefully. Changing it invalidates any in flight challenges, but that window is only as long as your challenge expiry (five minutes above), so a quiet redeploy is low risk.
    • You own uptime. Self hosting means your challenge endpoint must stay up, or forms protected by it will fail closed. Monitor it like any other dependency.
    • Updates come from the core team. The ALTCHA project does not accept external pull requests, so track the GitHub releases feed and update the widget and server library when security patches land.

    Wrap up

    You now run your own ALTCHA back end on a RamNode VPS: either a tiny custom Go service you fully control, or GateCHA for a managed feeling dashboard, both fronted by automatic TLS. Your forms are protected by invisible proof of work, no visitor data leaves your infrastructure, and you have removed a third party CAPTCHA dependency along with its tracking and its outage risk.