# AI Will Kill Your Project and You Won't Even Notice. Part One: Deploy

> Source: <https://dev.to/shpuli/ai-will-kill-your-project-and-you-wont-even-notice-part-one-deploy-4hcn>
> Published: 2026-07-28 08:45:07+00:00

I've got an idea for a series of posts about our vibe-coding problems. Today I'll talk about the lumps I took in the part that was hardest for me: deploy.

My favorite pet project was about astrology: you'd send your birth data to a Telegram bot, the service would calculate your natal chart, and an AI would answer questions about it. In six months the project didn't make a single cent, and one fine day, right in the middle of *eclipse season*, I shut it down. But along the way it walked me into a decent number of gotchas, and those I want to share.

So. You have a brilliant idea for a brilliant service. You talked it over with an AI, the AI produced code and promises of money raining down. Just one small thing left: that code currently lives — and probably works — only on your laptop. And it needs to be on the internet.

I see three options, and two of them rule themselves out.

**Locally, from your own computer.** Forward the ports, set up a domain, make sure the machine doesn't fall asleep, figure out security, never turn the laptop off. I can't do that. I'm just a cat.

**On a rented bare VPS.** They hand you a blank box — install the updates yourself, the firewall, the web server, the certificates, the monitoring, patch it all yourself until the end of time. I can't do that either. I've got tiny paws.

Which leaves **specialized hosting** — it takes some of this off your hands. Then the question becomes: which one.

I had three.

I had two microservices: one calculated natal charts, the other talked to the human via AI. You might tell me this is overkill, but I had a serious, important reason that I may write about in a separate post (spoiler: an AI can quietly drag in a dependency whose license forces you to open-source your closed code — and, well, oops).

Whatever the reasons, I needed to roll out two microservices, with one facing the internet and the other listening only to the first. Not every host makes that easy enough to set up.

By the way, an important note for your wallet: **every microservice is a separate bill.** Two microservices inside a single project of mine = I pay twice. Keep that in mind when an AI cheerfully suggests "let's split this out into its own service."

If you don't have a DB, skip this section. I did have one: I needed to store natal charts, plus subscriptions and payment dates (as if there were any). My database was hosted separately, and for free.

I'm going to tiptoe past that "for free" for now, because a free DB can become a problem, and that deserves its own post. For now remember one thing: when you're choosing a host, look separately at how it hosts databases. Many charge for it, and free tiers can have nasty surprises.

There are hosts that position themselves as "for complete beginners": every setting through a beautiful UI, just click the checkboxes. Sounds great, but I wouldn't. Here's why:

you're tied to that host forever — your configuration lives in their UI, not with you;

checkboxes have to be watched and ticked by hand, and you have two hands and a memory that leaks;

and most importantly — you can't ask an AI to check your checkboxes for you. Code, you can.

Which leads to an unexpected conclusion: **Infrastructure as Code is the ideal vibe-coder option.** Not an advanced topic for DevOps graybeards — it's a lifeline for beginners. Your infrastructure is described in a text file, the file sits next to your code, and you can hand it to an AI and ask, "take a look, did I screw anything up?" You can't check checkboxes that way — first you have to find them all in that beautiful, trendy UI.

I tried several hosts. I won't describe the ones that didn't work out — in case of lawyers. But I liked [Fly.io](http://Fly.io), and here's why:

that same Infrastructure as Code, out of the box;

deploy with a single command straight from the folder — `fly deploy`

, no git required, no pushes, just from the directory with your code;

controls for who talks to whom over the network — which service accepts requests from the internet, and which one only answers another service.

This is where their ad would go, if anyone had paid me. Nobody did, so let me tell you about the rough edges too. At first it seemed to me that Fly was magic: it takes my code and figures out how to run it. It didn't invent a thing. It dutifully executed four files that I had written myself beforehand (kidding, of course — "myself," the AI wrote them for me):

** Dockerfile** (two of them, one per service) — I had these from the start, because I never managed to run the astrology calculation library without one. But in general, I sincerely and wholeheartedly recommend doing all local development in containers — it will save your sanity. But alas, you're finally going to have to learn Docker in 2026.

** fly.toml** (also two) — a description of how the service lives on the platform: name, region, machine size, which ports are exposed, health checks. I'll write about these in detail below.

The `fly deploy`

command reads `fly.toml`

from the folder, builds an image from the `Dockerfile`

, uploads it and rolls out a machine with the described parameters. No magic whatsoever. :(

A reminder of the setup: I have two apps. `dearstarsbot`

— the chatterbox, it faces outward. `sweservice`

— the natal chart calculator, it should listen only to the bot and to nobody else. The chatterbox goes to the calculator for the math. That's the whole topology.

**The chatterbox**

```
app = 'dearstarsbot'
primary_region = 'fra'

[build]
  build-target = "bot"

[env]
  METRICS_PORT = "8081"
  CHART_API_URL = "http://sweservice.flycast/chart"
  CITY = "London"
  OPENAI_MODEL = "gpt-4o"
  SUPPORT_CHAT_ID = "12345666"

[http_service]
  internal_port = 8081
  force_https = true
  auto_stop_machines = 'off'
  auto_start_machines = true
  min_machines_running = 1
  max_machines_running = 1
  processes = ['app']

[[http_service.checks]]
  interval = "10s"
  timeout = "2s"
  grace_period = "5s"
  method = "GET"
  path = "/health"

[[vm]]
  memory = '1gb'
  cpu_kind = 'shared'
  cpus = 1
  memory_mb = 1024
```

Going line by line through what isn't obvious:

** build-target = "bot"** — needed if your Dockerfile has several stages (

`FROM ... AS bot`

); then Fly builds not the whole Dockerfile but the specific stage named `bot`

. If there are no stages, you don't need the line.** [env]** — this is the non-secret config: the metrics port, the address of the second service, the default city, the OpenAI model, and my personal chat id so I get notifications. Note: no keys here.

** CHART_API_URL = "http://sweservice.flycast/chart"** — the first wire between the services. This is the address the chatterbox goes to for a chart calculation. Let's break the address down:

`sweservice`

is the name of the second app, `.flycast`

is its private address inside my network on Fly, and `http://`

(not `https`

!) — because flycast only ever speaks http. Remember that `http`

, it'll come up again.** internal_port = 8081** — the port the chatterbox listens on inside the container. Functionally the chatterbox only has outgoing traffic (requests to the Telegram API), but I also had metrics, and this is the port they came out of.

** force_https = true** — public service, everyone gets pushed to https. Makes sense.

** auto_stop_machines = 'off' + min = 1 + max = 1** — the chatterbox lives in exactly one instance and never falls asleep. Why — I'll explain in the gotchas section.

** [[http_service.checks]]** — every 10 seconds Fly pokes

`/health`

and decides whether the service is alive. This will also come up in the gotchas.** [[vm]]** — the parameters of the machine Fly will run the app on.

`cpu_kind = 'shared'`

— a shared processor; I'll talk about it a bit further down, but not in the gotchas.

**The calculator:**

```
app = 'sweservice'
primary_region = 'fra'

[build]
  build-target = "final"

[env]
  METRICS_PORT = "8080"
  CGO_ENABLED = 1
  CITY = "London"

[http_service]
  internal_port = 8080
  force_https = false
  auto_stop_machines = 'stop'
  auto_start_machines = true
  min_machines_running = 1
  max_machines_running = 1
  processes = ['app']

[[vm]]
  memory = '1gb'
  cpu_kind = 'shared'
  cpus = 1
```

Here we mostly look at the differences:

** CGO_ENABLED = 1** — a fairly exotic line, needed only if, like me, you have Go on top of C: inside this service lives a C library for the calculations. If you don't have Go on top of C, forget you ever saw this line.

** internal_port = 8080** — the second wire between the services. Remember

`CHART_API_URL = ".../chart"`

on the chatterbox? Well, `/chart`

lives on exactly this port. The port the calculator listens on is exactly the port the chatterbox knocks on.** force_https = false** — and this isn't an oversight, it's a requirement. flycast only works over http, and if you put

`true`

here, the private connection between the services simply breaks. That same `http://`

from the chatterbox's address, just seen from the other side.** auto_stop_machines = 'stop'** — supposedly "let it sleep when nobody's asking." But right next to it sits

`min_machines_running = 1`

, and that 1 won't let the machine fall asleep. Which means `'stop'`

here quietly does nothing. Oops.And one more oops: look at both files again and find the line that makes `sweservice`

private.

It isn't there.

Both services declare `[http_service]`

. Both, if you go by `fly.toml`

alone, are public. Privacy isn't described in the config at all.

It comes from somewhere else: `sweservice`

simply has no public IP address — only a private flycast one. Unfortunately, there's no way to do this through `fly.toml`

, only through the beautiful UI or the terminal:

```
fly ips allocate-v6 --private   # assign a private flycast address
fly ips list                    # check that there are no public addresses
fly ips release <address>       # remove the public one, if Fly already slapped one on
```

The sneaky part is that on a service's first deploy, Fly slaps a public IP on it automatically. So you're insecure by default — privacy is opt-in, and opting in means going and pulling the address off yourself. Forget, and your internal calculator is hanging out on the open internet.

So why can't you manage this from the one place where it would actually make sense — the config file? Great question. Pretty much everyone asks the Fly team that one. It's in the backlog somewhere, I'm sure.

An interesting machine setting from my `fly.toml`

(or from the UI); let me explain it.

Fly doesn't give you a whole CPU core, it slices it up among a bunch of other people's machines: you're renting not a core, but slices of its time. How many slices do you get? On average — about 6% of one core: as if your processor worked for 3–4 seconds out of every minute. Sounds pathetic, but there's a trick: while you idle, you bank burst credit, and when you need it, you grab a whole core at 100% for several minutes. So shared isn't "always slow," it's "fast in short bursts, slow if you hammer the CPU nonstop."

Who this doesn't work for. If I'd spun up some kind of Counter-Strike server on this machine — it loads the CPU continuously, every tick — the burst credit would have burned out in minutes, and after that the game would lag at those very 6%. Anything that uses the processor evenly and without pauses (a game server, video transcoding, mining) isn't going to survive on shared.

For my workloads, though, it's perfect. Calculating a natal chart is about three seconds of CPU, but the operation is one-off: the user calculates once, the result goes into the database, and from then on all questions go to the database, not to the calculator. Even with millions of users (ah, if only) I'm not worried — I'm not going to get a million *new* ones per hour, and the old charts are already calculated. The chatterbox barely touches the processor either: the longest thing there is waiting for the AI's answer, and I do that waiting asynchronously, with the CPU free the whole time. Both of my workloads are a burst once in a blue moon, not a constant load. The ideal client for shared.

Something else you might think about while ticking this checkbox: I've got real people's data — won't it leak to my neighbor on the core? It won't. You're sharing processor time, not memory: every machine is an isolated VM, and someone else's code doesn't get into your RAM or your database.

Anyway, for me this was a really good way to save money — and I took it.

**Gotcha 1: Keys, or the most expensive self-own.** Deploy isn't only "where do I put the code." It's also "how do I put the keys so they don't leak." I had three: the key to the AI provider, the Telegram bot token, the database credentials.

Where do you put them? Not in the code and not in `fly.toml`

. A vibe-coding classic: the AI hardcoded a key, you committed it to a public repository, and a couple of days later your balance is zero or negative, because somebody found your key and ran their own workloads on it.

Someone might tell me: but you can make the repository private! And I'll answer: iCloud is private too, and look how many times it's been breached. Keys should be encrypted, not just sitting in a private place.

For this, Fly has a ~~beautiful UI~~ terminal command, `fly secrets set`

```
fly secrets set OPENAI_API_KEY=sk-... BOT_TOKEN=...
```

The keys get passed into the machine as environment variables at startup and don't end up in the built image.

You do this once and forget it, but Fly remembers — in encrypted form. You can look at which keys are already set, but not at the values themselves: `fly secrets list`

will show you only their hashes.

```
NAME             DIGEST            DATE
OPENAI_API_KEY   b9e37b7b239ee4…   2 days ago
BOT_TOKEN        cdbe3268a82bfe…   2 days ago
```

**Gotcha 2: But there was a health check right there!** And Fly did do something with it, just not what I thought. Remember the checks section in my `fly.toml`

, the one that knocks on /health every 10 seconds? That's Fly checking: "you alive in there?" The app answers "yep" — so it's alive, all good.

I thought that if it's "not yep," Fly would restart it. But no. It just stops sending traffic to it. There is a restart option, but for that the app has to fall over completely — like, actually exit, with some scary exit code. If it didn't fall over, well, fine then.

So there I am drinking tea while somebody breaks my chatterbox, and in such a clever way that it hangs but doesn't crash. /health says "not yep," Fly silently pulls the traffic. And I, you know, have polling — there's no incoming traffic there anyway, which does nothing for me. What helped was a friend who wanted to ask the bot whether he should change jobs. The bot didn't answer. The friend messaged me.

Okay, so the gotcha got me. What did I do about it? First, I started calling panic() on everything I could — better that the service honestly falls over and comes back by restart policy than that it hangs silently. Second, a backup plan for the unforeseen: once a day I'd message the bot myself and see whether it answered. Mom says I'm a real engineer.

**Gotcha 3: Keep two machines per service.** The host keeps pushing you to run two copies of every service — for reliability. In theory, this would have saved me from gotcha 2: the hung app would go on hanging, its copy would go on answering, and so on until the end of time. But that's in theory, and for most services, not for me. My chatterbox, which polls the Telegram API, has to exist in exactly one instance, otherwise the two copies start conflicting and throwing errors. And now, as I write this, I'm thinking the problem could have been worked around if I'd used a webhook instead of polling — but that's me thinking now, and back then I wasn't thinking that.

And by the way, I'd also have had to pay twice as much for two machines.

**Gotcha 4: Saving money and scale-to-zero.** Hosts offer a cheap mode: the service falls asleep when idle and wakes up when a request arrives. Cheaper — yes, because these days billing is most often "by usage." But the first request after the sleep waits several seconds for the wake-up. I didn't even try this: it was immediately obvious to me that for a chatbot, a several-second pause = a lost human. The calculator would have been a great candidate for that kind of savings, but back then I still had money and the hope of making more, so I went straight for the slightly more expensive always-on tier.

About **$13 a month** — for two always-on machines of my size (a gig of memory, shared CPU). The shortest section.

AI is good. It'll write the code and sprinkle em dashes through your article. It'll make the choice for you too: shared or not, one machine or two — except it'll probably pick whatever the host recommends by default, or whatever counts as normal for everybody, without stopping to think about whether it suits *you*. So I've collected the places where, at the deploy stage, it's worth stopping to think: each one of them hits either the project or the wallet, and sometimes both at once.

Anyway: use your head, and may AI help us all.

*Argue in the comments. Tell me where I made a good call and where I made a so-so one. Stories of your own vibe-deploying are also welcome.*
