> ## Documentation Index
> Fetch the complete documentation index at: https://mint.skeptrune.com/llms.txt
> Use this file to discover all available pages before exploring further.

# VPS Evangelism: Deploy an LLM-over-DNS Proxy in 30 Min

> Why confidently shipping anything to the internet is a hacker's superpower — and how to build an LLM-over-DNS proxy on a bare VPS in under 30 minutes.

My most valuable skill as a hacker/entrepreneur is that I'm confident deploying arbitrary programs that work locally to the internet. Sounds simple, but it's really the core of what got me into Y-Combinator and later helped me raise a seed round. This post is about how I got there—and a concrete tutorial to prove the point by building an LLM-over-DNS proxy on a bare VPS in under 30 minutes.

## Being on the struggle bus early

When I was starting out hacking as a kid, one of the first complete things I built was a weather reply bot for Twitter. It read from the firehouse API, monitored for mentions and city names, then replied with current weather conditions when it got @'ed. My parents got me a Raspberry Pi for Christmas and I found a tutorial online. I got it working locally and then got completely stuck on deployment.

The obvious next step was using my Pi as a server, but that was a disaster. My program had bugs and would crash while I was away. Then I couldn't SSH back in because my house didn't have a static IP and Tailscale wasn't a thing yet. It only worked on and off when I was home and could babysit it.

## Skipping straight to PaaS hell

When I started building web applications, I somehow skipped VPS entirely and went straight to Platform as a Service solutions like Vercel and Render. I was googling "how do I deploy my create react app" and somehow the top answer was to deploy to some third-party service that handled build steps, managed SSL, and was incredibly complicated and time-consuming.

There was always some weird limitation: memory constraints during build, Puppeteer couldn't run because they didn't have the right apt packages. Then I was stuck configuring Docker images, and since AI wasn't a thing yet and I'd never used Docker at a real job, it was all a disaster. I wasted more time trying to deploy my crappy React app than building it.

## Getting saved by a VPS maximalist

During college, I got lucky and met a hacky startup entrepreneur who was hiring. I decided to take a chance and join, even though the whole operation seemed barely legitimate.

Going into the job, I had this assumption that the "right" way to deploy was on AWS or some other hyperscaler. But this guy's mindset was the complete opposite—he was a VPS maximalist with a beautifully simple philosophy: rent a VPS, SSH in, do the same thing you did locally (`yarn dev` or whatever), throw up a reverse proxy, and call it a day. I watched him deploy like this over and over, and eventually he walked me through it myself a few times.

It was all so small and easy to learn, but it made me exponentially more confident as a builder. I never directly thought, "I can't build this because I won't be able to deploy it," but the general insecurity definitely caused a hesitancy and procrastination that immediately went away.

## Paying it forward

I've become an evangelist for this approach and wanted to write about it for a long time, but didn't know how to frame it entertainingly. Then on X, I got inspiration when levelsio posted a tweet about [deploying a DNS server on Hetzner that lets you talk to an LLM](https://x.com/levelsio/status/1952861177731793324).

Want to see it in action? Try this:

```bash theme={null}
dig @llm.skeptrune.com "what is the meaning of life?" TXT +short
```

Getting that setup is probably more interesting than my rambling. Here's how to deploy your own LLM-over-DNS proxy on a VPS in less than half an hour with nothing other than a rented server.

***

## Tutorial: LLM-over-DNS on a bare VPS

The architecture is simple: a Python DNS server that listens on port 53, treats incoming DNS query names as LLM prompts, calls the OpenRouter API, and returns the response as a TXT record. The client is just `dig`.

<Note>
  You'll need a VPS with a public IP address (Hetzner, DigitalOcean, Linode, or similar), an [OpenRouter](https://openrouter.ai) API key, and Python 3 on the server. Any Linux image works.
</Note>

<Steps>
  <Step title="Access your VPS">
    After purchasing your VPS, you'll receive an IP address and login credentials (usually via email). Connect to your server:

    ```bash theme={null}
    ssh root@<your-vps-ip>
    ```

    Replace `<your-vps-ip>` with your actual server IP address.
  </Step>

  <Step title="Clear existing DNS services">
    Many VPS images come with `systemd-resolved` or `bind9` pre-installed. These will conflict with a DNS server running on port 53. Remove or disable them first:

    ```bash theme={null}
    # Check for running DNS services
    systemctl list-units --type=service | grep -E 'bind|dns|systemd-resolved'

    # Stop and disable systemd-resolved (if present)
    systemctl stop systemd-resolved
    systemctl disable systemd-resolved

    # Remove bind9 (if present)
    apt-get remove --purge bind9 -y
    ```
  </Step>

  <Step title="Install Python dependencies">
    Install the required Python packages:

    ```bash theme={null}
    pip install dnslib requests
    ```
  </Step>

  <Step title="Create the DNS-to-LLM proxy script">
    Create a file called `llm_dns.py` with the following content. The script listens for DNS queries, extracts the query name as a prompt, sends it to the OpenRouter API, and returns the response in one or more TXT records:

    ```python theme={null}
    from dnslib.server import DNSServer, BaseResolver
    from dnslib import RR, QTYPE, TXT
    import requests
    import codecs

    OPENROUTER_API_KEY = ""  # Add your OpenRouter API key here
    LLM_API_URL = "https://openrouter.ai/api/v1/chat/completions"

    class LLMResolver(BaseResolver):
        def resolve(self, request, handler):
            qname = request.q.qname
            qtype = QTYPE[request.q.qtype]
            prompt = str(qname).rstrip('.')

            # Forward prompt to LLM
            try:
                response = requests.post(
                    LLM_API_URL,
                    headers={
                        "Authorization": f"Bearer {OPENROUTER_API_KEY}",
                        "Content-Type": "application/json"
                    },
                    json={
                        "model": "openai/gpt-3.5-turbo",
                        "messages": [{"role": "user", "content": prompt}]
                    },
                    timeout=10
                )
                response.raise_for_status()
                raw_answer = response.json()["choices"][0]["message"]["content"]
            except Exception as e:
                raw_answer = f"Error: {str(e)}"

            try:
                answer = codecs.decode(raw_answer.encode('utf-8'), 'unicode_escape')
            except Exception:
                answer = raw_answer.replace('\\010', '\n').replace('\\n', '\n')

            reply = request.reply()
            if qtype == "TXT":
                # Split long responses into chunks of 200 chars (safe limit)
                chunk_size = 200
                if len(answer) > chunk_size:
                    chunks = [answer[i:i+chunk_size] for i in range(0, len(answer), chunk_size)]
                    for i, chunk in enumerate(chunks):
                        reply.add_answer(RR(qname, QTYPE.TXT, rdata=TXT(f"[{i+1}/{len(chunks)}] {chunk}")))
                else:
                    reply.add_answer(RR(qname, QTYPE.TXT, rdata=TXT(answer)))
            return reply

    if __name__ == "__main__":
        resolver = LLMResolver()
        server = DNSServer(resolver, port=53, address="0.0.0.0")
        server.start_thread()
        import time
        while True:
            time.sleep(1)
    ```

    Before running, paste your OpenRouter API key into the `OPENROUTER_API_KEY` variable. For anything more serious than a demo, use an environment variable to keep your key out of the source code.

    <Warning>
      This is a proof-of-concept. For production use you'd want proper process management (systemd), structured logging, rate limiting, and to avoid storing API keys in plaintext.
    </Warning>
  </Step>

  <Step title="Run the DNS-LLM proxy">
    Start the DNS server. Port 53 requires root privileges:

    ```bash theme={null}
    sudo python3 llm_dns.py
    ```
  </Step>

  <Step title="Test your service">
    From another machine, send a DNS TXT query directly to your server's IP:

    ```bash theme={null}
    dig @<your-vps-ip> "what is the meaning of life" TXT +short
    ```

    The LLM's response should appear in the terminal output.
  </Step>

  <Step title="Secure your setup (recommended)">
    Use UFW (Uncomplicated Firewall) to restrict access. Allow SSH so you don't lock yourself out, allow port 53 for DNS queries, and block everything else:

    ```bash theme={null}
    ufw allow ssh
    ufw allow 53
    ufw enable
    ```

    <Info>
      UFW is literally called "uncomplicated" because that's what a VPS is—uncomplicated. This setup runs as root and stores your API key in plaintext. For anything beyond experimentation, use environment variables, a non-root user, and a process manager like systemd.
    </Info>
  </Step>
</Steps>

## Troubleshooting

<AccordionGroup>
  <Accordion title="Permission denied">
    Make sure you're running with `sudo`. Port 53 requires root privileges.
  </Accordion>

  <Accordion title="Connection timeout">
    Check your VPS firewall settings and ensure port 53 (UDP and TCP) is open. Most VPS providers have a cloud-level firewall in addition to the OS-level one—check both.
  </Accordion>

  <Accordion title="API errors">
    Verify your OpenRouter API key and confirm your account has credits. You can test the API directly with `curl` before running the DNS server.
  </Accordion>

  <Accordion title="No response from the server">
    Run `systemctl status systemd-resolved` to confirm it's actually stopped. If it's still running, it will bind port 53 and your script will fail to start (or silently fail to receive queries).
  </Accordion>
</AccordionGroup>

## References

* [dnslib documentation](https://dnslib.readthedocs.io/en/latest/)
* [OpenRouter API docs](https://openrouter.ai/)

That's it. You now have your own LLM-over-DNS proxy running on a simple VPS. No complex infrastructure needed—just SSH in, install dependencies, and run your code. This is the beauty of keeping things simple.
