<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Ahmet Ozan GAZİ</title>
    <description>The latest articles on DEV Community by Ahmet Ozan GAZİ (@ozy31).</description>
    <link>https://dev.to/ozy31</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1315268%2F67e138c9-df75-4d0f-917d-8a963e6c4bc7.jpeg</url>
      <title>DEV Community: Ahmet Ozan GAZİ</title>
      <link>https://dev.to/ozy31</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ozy31"/>
    <language>en</language>
    <item>
      <title>I Built a Self-Hosted ChatGPT on My Laptop. Here's What Broke.</title>
      <dc:creator>Ahmet Ozan GAZİ</dc:creator>
      <pubDate>Mon, 02 Nov 2026 07:57:12 +0000</pubDate>
      <link>https://dev.to/ozy31/i-built-a-self-hosted-chatgpt-on-my-laptop-heres-what-broke-2gci</link>
      <guid>https://dev.to/ozy31/i-built-a-self-hosted-chatgpt-on-my-laptop-heres-what-broke-2gci</guid>
      <description>&lt;p&gt;&lt;em&gt;From DevOps to AI Platform Engineering — Part 1&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I'm a DevOps engineer. I've spent years running Kubernetes clusters, writing Terraform and keeping production alive. Lately, every job interview has a new question in it: &lt;em&gt;"What have you done with AI?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Calling the OpenAI API from a script didn't feel like a good answer. So I set myself a different goal: &lt;strong&gt;run an open-weight LLM myself and serve it to other people like a real platform&lt;/strong&gt;, with API keys, rate limits, usage tracking and numbers I measured myself.&lt;/p&gt;

&lt;p&gt;This is part 1 of a 3-part series. Everything here runs on one laptop, a MacBook Pro M1 Pro with 16 GB of RAM. Part 2 covers observability and load testing. Part 3 moves the same platform to GPUs on Amazon EKS.&lt;/p&gt;

&lt;p&gt;I call the platform &lt;strong&gt;OzAI&lt;/strong&gt;. To be clear from the start: OzAI is &lt;em&gt;not&lt;/em&gt; a model I trained. It's a self-hosted platform built on open-weight models (Qwen3 8B and Qwen2.5-Coder 3B). The engineering is in serving them.&lt;/p&gt;




&lt;h2&gt;
  
  
  The architecture
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnkhborf5dgf9oecqowyo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnkhborf5dgf9oecqowyo.png" alt="OzAI architecture: browser, VS Code and scripts talk to a LiteLLM gateway in Docker Compose, which calls Ollama running natively on the macOS host" width="799" height="453"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ollama&lt;/strong&gt; runs the models. It runs natively on macOS, not in Docker (more on that below).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LiteLLM&lt;/strong&gt; is the gateway: an OpenAI-compatible API with per-user virtual keys, model allow-lists, rate limits and a request log in Postgres.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Open WebUI&lt;/strong&gt; is a ChatGPT-style UI that talks only to the gateway.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SearXNG&lt;/strong&gt; is a self-hosted search engine, so the model can answer questions about recent events.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Grafana&lt;/strong&gt; shows dashboards built on the gateway's request log (part 2).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything except Ollama runs in Docker Compose and binds to &lt;code&gt;127.0.0.1&lt;/code&gt; only. Nothing is exposed to the internet yet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Decision #1: Ollama runs on the host, not in a container.&lt;/strong&gt; Docker on macOS runs containers inside a Linux VM with no access to the Apple GPU. Ollama in a container would run on the CPU only. On the host it uses Metal, and the containers reach it at &lt;code&gt;host.docker.internal:11434&lt;/code&gt;. It looks less tidy in a compose file, but it's the difference between a usable system and an unusable one.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 1: How fast is a laptop, really?
&lt;/h2&gt;

&lt;p&gt;Before building anything, I measured. Here is one request per model, 200 max tokens, no concurrency:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Model                    Disk     Cold load   Prompt eval   Generation
qwen2.5-coder:3b (Q4)    1.9 GB   2.1 s       120 tok/s     53.3 tok/s
qwen3:8b (Q4_K_M)        5.2 GB   3.9 s        87 tok/s     22.4 tok/s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things surprised me:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The 8B model is less than half as fast as the 3B one.&lt;/strong&gt; Generating tokens is limited by memory bandwidth, not compute. For every token, the model's weights have to be read from memory again. A model 2.7× bigger is roughly 2.4× slower.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;22 tokens per second is fine for one person.&lt;/strong&gt; That's faster than most people read. The real question is what happens with more than one user, which comes later.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Quantization (&lt;code&gt;Q4_K_M&lt;/code&gt; = roughly 4 bits per weight) is what makes an 8B model fit in 5.2 GB at all. Without it, the same model needs around 16 GB just for its weights, which is all the RAM in this laptop.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 2: A gateway, not just a model
&lt;/h2&gt;

&lt;p&gt;Running a model is easy. Letting &lt;em&gt;other people&lt;/em&gt; use it safely is the platform engineering part, and that's where LiteLLM comes in.&lt;/p&gt;

&lt;p&gt;The model list maps friendly names to real models:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;model_list&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;model_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ozai&lt;/span&gt;            &lt;span class="c1"&gt;# fast chat&lt;/span&gt;
    &lt;span class="na"&gt;litellm_params&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ollama_chat/qwen3:8b&lt;/span&gt;
      &lt;span class="na"&gt;api_base&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://host.docker.internal:11434&lt;/span&gt;
      &lt;span class="na"&gt;reasoning_effort&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;none&lt;/span&gt;
      &lt;span class="na"&gt;num_ctx&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8192&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;model_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ozai-think&lt;/span&gt;      &lt;span class="c1"&gt;# same model, thinking on&lt;/span&gt;
    &lt;span class="na"&gt;litellm_params&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ollama_chat/qwen3:8b&lt;/span&gt;
      &lt;span class="na"&gt;api_base&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://host.docker.internal:11434&lt;/span&gt;
      &lt;span class="na"&gt;num_ctx&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8192&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;model_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ozai-coder&lt;/span&gt;
    &lt;span class="na"&gt;litellm_params&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ollama_chat/qwen2.5-coder:3b&lt;/span&gt;
      &lt;span class="na"&gt;api_base&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://host.docker.internal:11434&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then every client gets its own virtual key with its own limits:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl localhost:4000/key/generate &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$LITELLM_MASTER_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"key_alias":"demo-friend","models":["ozai"],"rpm_limit":10}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I tested it the way I'd test any API gateway:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;No key                              -&amp;gt; 401
Friend key asking for ozai-coder    -&amp;gt; 403 (not on its allow-list)
12 requests in a minute, limit 10   -&amp;gt; 9 x 200, then 429
                                       (one earlier request counted in the same window)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Any tool that speaks the OpenAI API can now use OzAI by changing two settings: the base URL and the key. That turned out to matter a lot for the IDE integration below.&lt;/p&gt;




&lt;h2&gt;
  
  
  What broke (the useful part)
&lt;/h2&gt;

&lt;p&gt;Nothing below threw an error. Every one of these problems showed up as "it works, but badly."&lt;/p&gt;

&lt;h3&gt;
  
  
  1. A "thinking" model made every answer take two minutes
&lt;/h3&gt;

&lt;p&gt;Qwen3 is a reasoning model. Before it answers, it writes a hidden chain of thought. One simple question produced over 600 streamed reasoning chunks before the first word of the actual answer. The full response took about two minutes, partly because it was queued behind another request on the same GPU.&lt;/p&gt;

&lt;p&gt;There was a second, quieter problem. With a small &lt;code&gt;max_tokens&lt;/code&gt;, the model spent the whole budget on thinking and &lt;strong&gt;returned an empty answer&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; two aliases for the same weights. &lt;code&gt;ozai&lt;/code&gt; has thinking turned off (&lt;code&gt;reasoning_effort: none&lt;/code&gt;) and is the default. &lt;code&gt;ozai-think&lt;/code&gt; is there for hard questions. Users choose speed or depth by picking a model name. Nobody has to know about prompt flags.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. A $0 budget blocked everything
&lt;/h3&gt;

&lt;p&gt;I gave a test key &lt;code&gt;max_budget: 0&lt;/code&gt;, assuming that meant "no spending allowed." Local models cost $0, so I expected every request to pass. Instead, every request was rejected:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Budget has been exceeded! Current cost: 0.0, Max budget: 0.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The check is &lt;code&gt;spend &amp;gt;= budget&lt;/code&gt;, and 0 ≥ 0. For self-hosted models, either leave the budget unset or set a custom per-token price so the budget means something.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. The UI was quietly competing with its own users
&lt;/h3&gt;

&lt;p&gt;The chat UI felt slow even with a single user. The gateway log explained why. After every answer, Open WebUI sends &lt;em&gt;extra&lt;/em&gt; requests to the same model: one to generate a chat title, one for tags, one for follow-up suggestions.&lt;/p&gt;

&lt;p&gt;On one GPU, those requests queue in front of the user's next question.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; In Open WebUI's admin settings, point background tasks at the small 3B model (&lt;code&gt;ozai-coder&lt;/code&gt;) and turn off tags and follow-ups. After that, answers started almost immediately again.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. More users don't get more throughput
&lt;/h3&gt;

&lt;p&gt;I wrote a small load generator (Python threads streaming from the gateway) and measured time to first token (TTFT) and throughput at different concurrency levels. Each request asked for 128 tokens:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Users   TTFT p50   TTFT max   Per-user tok/s   Total tok/s
1         0.3 s      0.3 s        19.5            19.5
2         3.4 s      6.7 s        14.5            19.7
4         9.3 s     18.6 s         8.7            20.8
8        21.3 s     43.3 s         4.7            20.7
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Total throughput stays flat at about 20 tokens per second.&lt;/strong&gt; More users just split the same output into smaller slices. The worst TTFT is roughly &lt;em&gt;your place in the queue × one answer&lt;/em&gt;. In practice, requests are processed one at a time.&lt;/p&gt;

&lt;p&gt;For a DevOps engineer this was the key lesson: &lt;strong&gt;an LLM server does not scale like a web server.&lt;/strong&gt; Adding users doesn't use idle capacity, because the hardware is already working at full speed for one request. Production inference servers like vLLM fix this with &lt;em&gt;continuous batching&lt;/em&gt;, which runs many requests through the model in the same pass. That's part 3.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. The model is confidently wrong, and has no idea what day it is
&lt;/h3&gt;

&lt;p&gt;An 8B model has to compress a lot of world knowledge into 5 GB, and it shows. Ask it a precise technical question and it answers with total confidence, even when it's wrong. The KEDA Kafka scaler's default &lt;code&gt;lagThreshold&lt;/code&gt; is &lt;strong&gt;10&lt;/strong&gt;. OzAI says &lt;strong&gt;1000&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fypj0e3jpjky2v1o2eb2a.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fypj0e3jpjky2v1o2eb2a.png" alt="OzAI confidently answering that KEDA's default Kafka lagThreshold is 1000 (the docs say 10), then saying its knowledge cutoff is October 2023 and it has no real-time F1 data" width="800" height="357"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The second answer shows the other limit. The model only knows what was in its training data, and it reports a knowledge cutoff of October 2023. The current F1 season is out of reach.&lt;/p&gt;

&lt;p&gt;Both problems have the same cure: stop relying on what the model remembers, and give it the facts at question time. Later in this series I add document RAG and measure it. On 10 KEDA questions like this one, accuracy goes from 1/10 to 8/10.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix (part one of RAG):&lt;/strong&gt; web search through a &lt;strong&gt;self-hosted SearXNG&lt;/strong&gt; instance. Open WebUI searches, pulls in the relevant parts of the top results and gives them to the model as context. No search API keys, and no queries sent to a third-party AI provider.&lt;/p&gt;

&lt;p&gt;It still didn't work. That led to the best bug of the project.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. The bug with no error: silent prompt truncation
&lt;/h3&gt;

&lt;p&gt;Web search was on and SearXNG was returning results. The model still said "I don't have access to live data."&lt;/p&gt;

&lt;p&gt;The gateway's request log had the clue. &lt;strong&gt;Every Open WebUI request had exactly 2,050 prompt tokens.&lt;/strong&gt; It didn't matter whether the question was short or long, or whether search was on or off.&lt;/p&gt;

&lt;p&gt;So I ran a test. I sent about 5,000 tokens of filler text with a secret word at the very end:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Sent:   ~5,000 tokens, secret word at the END
Got:    prompt_tokens: 2050, answer: BANANA
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The model found the word, but the gateway reported only 2,050 prompt tokens. About 3,000 tokens had disappeared, and since the end survived, they came from the beginning. Ollama was silently cutting long prompts to fit its context window. It &lt;strong&gt;kept the end and dropped the beginning&lt;/strong&gt;, with no error and no warning. Open WebUI puts the search results at the &lt;em&gt;start&lt;/em&gt; of the prompt, so they were cut every single time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; one line, &lt;code&gt;num_ctx: 8192&lt;/code&gt;, on the gateway model. Then I moved the secret word to the &lt;em&gt;start&lt;/em&gt; of the prompt and repeated the test:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Sent:   ~5,000 tokens, secret word at the START
Got:    prompt_tokens: 5430, answer: BANANA
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The whole prompt now reached the model. Memory use went from 5.6 GB to 6.2 GB.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy8esubav0glr82ok863a.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy8esubav0glr82ok863a.png" alt="OzAI calling the search_web tool and answering a question about the 2026 Formula 1 season from search results" width="799" height="170"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is the lesson I'd put on a poster for anyone moving from DevOps to AI infrastructure: &lt;strong&gt;in LLM serving, "no error" doesn't mean "correct."&lt;/strong&gt; A web server that drops half a request fails loudly. A model server that drops half a prompt just gives a worse answer. Watch your token counts.&lt;/p&gt;

&lt;h3&gt;
  
  
  7. IDE integration: the gateway was the wrong path for autocomplete
&lt;/h3&gt;

&lt;p&gt;Because the gateway speaks the OpenAI API, connecting VS Code (with the Continue extension) took one config block. Chat and code edits go through the gateway with their own key, so that usage shows up in the logs.&lt;/p&gt;

&lt;p&gt;Autocomplete was different. Code completion uses &lt;em&gt;fill-in-the-middle&lt;/em&gt; (FIM): the model gets the code before and after the cursor and fills the gap. Through the gateway, the coder model answered like a chatbot instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"Certainly! The Fibonacci sequence is a series of numbers where..."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The gateway's completions endpoint was dropping the FIM parameters (&lt;code&gt;suffix&lt;/code&gt;, &lt;code&gt;raw&lt;/code&gt;) on the way to Ollama. Called directly, Ollama completed the function correctly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;fibonacci&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;fibonacci&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;fibonacci&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; chat goes through the gateway, and autocomplete goes straight to Ollama. That's the dashed line in the architecture diagram. It's a trade-off: autocomplete requests don't appear in the usage logs. For a local setup, I'll take working completions over complete logs.&lt;/p&gt;

&lt;p&gt;Cursor is another story. It calls custom models from its own servers, so it can't reach &lt;code&gt;localhost&lt;/code&gt;. That needs a public, authenticated endpoint, which is coming later with Cloudflare Tunnel and Access.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I'd tell another DevOps engineer
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The gateway is where your skills transfer.&lt;/strong&gt; Keys, quotas, rate limits, routing, request logs: this is API platform work you already know. The model is just another upstream.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Measure before you optimize.&lt;/strong&gt; One table of tokens per second at 1, 2, 4 and 8 users explained more than any blog post I read.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LLMs fail quietly.&lt;/strong&gt; Truncated prompts, empty answers caused by thinking, confident hallucinations. None of them return a 500. Your monitoring has to look at token counts and answer quality, not just status codes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Small local models are fast and cheap, but not reliable as a source of facts.&lt;/strong&gt; Give them context (search, documents) instead of trusting what they remember.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Part 2: observability and load testing.&lt;/strong&gt; A Grafana dashboard for TTFT, latency percentiles and tokens per second, built from the gateway's request log. Plus why LiteLLM's Prometheus metrics weren't an option.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Part 3: from laptop to production.&lt;/strong&gt; vLLM on Amazon EKS with GPU nodes, autoscaling on queue depth with KEDA, and the cost of self-hosting compared with paying per token.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All configs, benchmarks and notes are in the repo: &lt;strong&gt;&lt;a href="https://github.com/ozy31/self-hosted-llm-platform" rel="noopener noreferrer"&gt;github.com/ozy31/self-hosted-llm-platform&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;If you're making the same move from DevOps into AI infrastructure, I'd like to hear what surprised you.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>devops</category>
      <category>llm</category>
      <category>selfhosted</category>
    </item>
    <item>
      <title>AWS Site-to-Site VPN Connection and Distrubution over AWS Transit Gateway.</title>
      <dc:creator>Ahmet Ozan GAZİ</dc:creator>
      <pubDate>Tue, 24 Jun 2025 11:40:03 +0000</pubDate>
      <link>https://dev.to/ozy31/aws-site-to-site-vpn-connection-and-distrubution-over-aws-transit-gateway-5465</link>
      <guid>https://dev.to/ozy31/aws-site-to-site-vpn-connection-and-distrubution-over-aws-transit-gateway-5465</guid>
      <description>&lt;h1&gt;
  
  
  AWS Site-to-Site VPN and Transit Gateway Cross-Account Routing
&lt;/h1&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;In enterprise cloud infrastructures, establishing secure and scalable network connections between different AWS accounts and VPCs (Virtual Private Cloud) is of critical importance. AWS provides powerful services such as &lt;strong&gt;Site-to-Site VPN&lt;/strong&gt; and &lt;strong&gt;Transit Gateway&lt;/strong&gt; to meet this need. In this article, we will explain step by step how to route network traffic between multiple AWS accounts using Site-to-Site VPN and Transit Gateway.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Basic Concepts
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Site-to-Site VPN
&lt;/h3&gt;

&lt;p&gt;AWS Site-to-Site VPN creates an encrypted tunnel between your on-premises data center and your AWS VPC. This enables secure data transmission between your on-premises network and cloud environment.&lt;/p&gt;

&lt;h3&gt;
  
  
  Transit Gateway
&lt;/h3&gt;

&lt;p&gt;AWS Transit Gateway allows you to connect multiple VPCs and on-premises networks through a central hub. This structure simplifies network management and increases scalability.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Scenario and Architecture Definition
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Account A:&lt;/strong&gt; Main AWS account connected to on-premises data center.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Account B and C:&lt;/strong&gt; VPCs located in different AWS accounts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Objective:&lt;/strong&gt; Provide secure and centralized access from on-premises data center to VPCs in Account B and C.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  3. Step-by-Step Setup
&lt;/h2&gt;

&lt;h3&gt;
  
  
  3.1. Creating Transit Gateway (Account A)
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Sign in to &lt;strong&gt;AWS Management Console&lt;/strong&gt; with Account A.&lt;/li&gt;
&lt;li&gt;Follow the steps: &lt;strong&gt;VPC Dashboard&lt;/strong&gt; &amp;gt; &lt;strong&gt;Transit Gateways&lt;/strong&gt; &amp;gt; &lt;strong&gt;Create Transit Gateway&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Give the Transit Gateway a meaningful name and configure necessary settings.&lt;/li&gt;
&lt;li&gt;Note the Transit Gateway ID.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  3.2. Creating Transit Gateway Attachments
&lt;/h3&gt;

&lt;h4&gt;
  
  
  a) Connecting Account A's VPC to Transit Gateway
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Create a new attachment from the &lt;strong&gt;Transit Gateway Attachments&lt;/strong&gt; section in VPC Dashboard.&lt;/li&gt;
&lt;li&gt;Select the relevant VPC in Account A and connect it to the Transit Gateway.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  b) Creating VPC Attachments in Other Accounts (B and C)
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;In Account B and C, enable Transit Gateway sharing through &lt;strong&gt;Resource Access Manager (RAM)&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Share the Transit Gateway from Account A.&lt;/li&gt;
&lt;li&gt;In Account B and C, connect their own VPCs to the Transit Gateway using the shared Transit Gateway.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3.3. Site-to-Site VPN Setup
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Create &lt;strong&gt;Customer Gateway&lt;/strong&gt; and &lt;strong&gt;Virtual Private Gateway&lt;/strong&gt; in Account A.&lt;/li&gt;
&lt;li&gt;Create &lt;strong&gt;Site-to-Site VPN Connection&lt;/strong&gt; and enter your on-premises network information.&lt;/li&gt;
&lt;li&gt;Connect the VPN connection to the Transit Gateway (create Attachment).&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  3.4. Route Table and Routing
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Add routes to each VPC's route table to route traffic to other VPCs and on-premises network through the Transit Gateway.&lt;/li&gt;
&lt;li&gt;Define rules in the Transit Gateway Route Table to route incoming traffic to the correct VPC or VPN.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3.5. Security Groups and NACLs
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Configure security groups and network access control lists (NACL) to allow relevant ports and protocols.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  4. Testing and Validation
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Test the connection by pinging or SSH connecting to resources (e.g., EC2) in Account B and C from the on-premises network.&lt;/li&gt;
&lt;li&gt;Monitor traffic using AWS VPC Flow Logs and CloudWatch.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  5. Best Practices
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Segment Transit Gateway Route Tables to ensure network isolation.&lt;/li&gt;
&lt;li&gt;Securely manage resource sharing with IAM and RAM.&lt;/li&gt;
&lt;li&gt;Use two tunnels for high availability in VPN connections.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;With AWS Site-to-Site VPN and Transit Gateway, you can establish a centralized, secure, and scalable network architecture between different AWS accounts and on-premises networks. This structure greatly simplifies network management in large-scale and multi-account architectures.&lt;/p&gt;




&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6w5p2caq7nzi84f7fmsy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6w5p2caq7nzi84f7fmsy.png" alt="Image description" width="800" height="527"&gt;&lt;/a&gt;&lt;/p&gt;






&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

</description>
    </item>
    <item>
      <title>MFA AWS VIA TERMINAL</title>
      <dc:creator>Ahmet Ozan GAZİ</dc:creator>
      <pubDate>Mon, 20 May 2024 10:48:12 +0000</pubDate>
      <link>https://dev.to/ozy31/mfa-aws-via-terminal-406h</link>
      <guid>https://dev.to/ozy31/mfa-aws-via-terminal-406h</guid>
      <description>&lt;p&gt;In today's digital landscape, securing access to cloud services is paramount, particularly as cyber threats become increasingly sophisticated. Amazon Web Services (AWS), a leading cloud service provider, offers robust security features to protect sensitive data and infrastructure. One essential security measure is Multi-Factor Authentication (MFA), which enhances the traditional username and password login process by requiring an additional verification step. This essay will explore the process of configuring and using MFA to access AWS from a local terminal, highlighting the steps involved, the benefits of this security approach, and practical tips for seamless integration. By implementing MFA, users can significantly bolster their security posture, ensuring that access to their AWS environments is both secure and efficient.&lt;br&gt;
 In this article I want draw your attention to some point which we can make it happen with ease. First of all we need to do some configuration like installing necessary packages such as &lt;code&gt;os, subprocess, pyotp, json, base64, binascli, configparser, dotenv&lt;/code&gt;. These packages are prerequisites for making the scripts work . &lt;br&gt;
As a next step we need to define the environment variables which we'll us it in our &lt;code&gt;bash&lt;/code&gt; scripts. To do seo we create a .env file to keep all of our environmental variables to call them in the cli commands.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fisjpvcuzixpikj3vh2th.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fisjpvcuzixpikj3vh2th.png" alt="Image description" width="800" height="170"&gt;&lt;/a&gt;&lt;br&gt;
When configuring Multi-Factor Authentication (MFA) to securely access Amazon Web Services (AWS) from a local terminal, one of the crucial steps involves creating a .env file. This file, short for "environment variables," is essential for securely storing and managing sensitive information, such as AWS access keys, secret keys, and MFA device serial numbers. By using a .env file, users can avoid hard-coding these credentials directly into their scripts or applications, which enhances security and simplifies configuration management. The .env file should be placed in the root directory of the project and structured in a way that each variable is declared on a new line.&lt;br&gt;
 When it comes to creating a script for all of these configuration, naturally we need to acquire a snappy script which includes as follows,&lt;br&gt;
1- Imports and Environment Setup:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Imports the necessary modules.&lt;/li&gt;
&lt;li&gt;Loads environment variables from the .env file.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;2- Configuration Reading:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Reads AWS credentials from the specified source profile in the AWS credentials file.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;3-Validation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Verifies that the TOTP secret is valid and base32 encoded.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;4- MFA Token Generation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Generates a current MFA token using the TOTP secret.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;5- Session Token Retrieval:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Uses AWS STS to obtain a session token with the MFA token.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;6- Credentials Update:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Updates the AWS credentials file with the temporary session credentials.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Additionally, ensure the ~/.aws/profile under the $HOME directory is correctly configured for your AWS CLI.&lt;/p&gt;

&lt;p&gt;Here is the one example for script that we may use;&lt;br&gt;
&lt;code&gt;aws-login.py&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import os
import subprocess
import pyotp
import json
import base64
import binascii
import configparser
from dotenv import load_dotenv

# Load environment variables from .env file
load_dotenv()

# Retrieve variables from the environment
mfa_serial_number = os.getenv('MFA_SERIAL_NUMBER')
totp_secret = os.getenv('TOTP_SECRET')
source_profile = os.getenv('SOURCE_PROFILE', 'dev-mfa')
aws_profile = os.getenv('AWS_PROFILE', 'default')

# Read credentials from the source profile
config = configparser.ConfigParser()
credentials_file_path = os.path.expanduser('~/.aws/credentials')
config.read(credentials_file_path)

if source_profile not in config:
    print(f"Profile '{source_profile}' not found in {credentials_file_path}.")
    exit(1)

aws_access_key_id = config[source_profile].get('aws_access_key_id')
aws_secret_access_key = config[source_profile].get('aws_secret_access_key')

if not aws_access_key_id or not aws_secret_access_key:
    print(f"Missing credentials in profile '{source_profile}'.")
    exit(1)

# Ensure TOTP secret is base32 encoded
try:
    base64.b32decode(totp_secret, casefold=True)
except binascii.Error:
    print("The provided TOTP secret is not valid base32 encoded. Please check the secret.")
    exit(1)

# Generate MFA token
totp = pyotp.TOTP(totp_secret)
mfa_token = totp.now()

# Get temporary session token using MFA
try:
    response = subprocess.check_output([
        'aws', 'sts', 'get-session-token',
        '--serial-number', mfa_serial_number,
        '--token-code', mfa_token,
        '--output', 'json',
        '--profile', source_profile
    ])
except subprocess.CalledProcessError as e:
    print(f"Error getting session token: {e}")
    exit(1)

# Parse the response
response_json = json.loads(response)
credentials = response_json['Credentials']

# Update the AWS credentials file with the temporary credentials
if aws_profile not in config.sections():
    config.add_section(aws_profile)

config[aws_profile]['aws_access_key_id'] = credentials['AccessKeyId']
config[aws_profile]['aws_secret_access_key'] = credentials['SecretAccessKey']
config[aws_profile]['aws_session_token'] = credentials['SessionToken']

with open(credentials_file_path, 'w') as configfile:
    config.write(configfile)

print(f"🙌🏻 Welcome to Starlet Technologies AWS Cloud Services. All logins and acitivities are monitoring and recording.")
print("⏰ Your AWS CLi session will expire at:", credentials['Expiration'])

## Verify the identity of the temporary credentials
#try:
#    identity = subprocess.check_output(['aws', 'sts', 'get-caller-identity', '--profile', aws_profile])
#    print("Current Identity:", identity.decode('utf-8'))
#except subprocess.CalledProcessError as e:
#    print(f"Error verifying identity: {e}")
#    exit(1)
#
## Attempt to list S3 buckets
#try:
#    s3_buckets = subprocess.check_output(['aws', 's3', 'ls', '--profile', aws_profile])
#    print("S3 Buckets:", s3_buckets.decode('utf-8'))
#except subprocess.CalledProcessError as e:
#    print(f"Error listing S3 buckets: {e}")
#    exit(1)

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In order to make above script run, we use&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;python aws-login.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As a expected output we can see ;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fho39y4r7hzbiuizhx1r4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fho39y4r7hzbiuizhx1r4.png" alt="Image description" width="800" height="62"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For the troubleshooting purposes we recommend you to check if you have registered profile under &lt;code&gt;~/.aws/credentials&lt;/code&gt; file.&lt;/p&gt;

</description>
    </item>
  </channel>
</rss>
