DEV Community

Cover image for Your Side Project Doesn't Need a Public IP. Here's the Proof.
Dhruv malaviya
Dhruv malaviya

Posted on

Your Side Project Doesn't Need a Public IP. Here's the Proof.

I used to think every server needed a public IP. It's just... how servers work, right?

Then I moved my side project , a small Node.js API with a Postgres database , to Krova Cloud. Cubes don't get public IPs by default. Within a day, I realized I'd been accepting a bunch of nonsense I didn't have to.

No more SSH brute-forces. No open ports accidentally exposed. No anxiety about whether my firewall rules were perfect.

Here's what happened.

The problem with public IPs
I ran my project on a cheap VPS for two years. It worked. But one bored afternoon I checked the auth logs:

text

Failed password for root from 185.x.x.x
Failed password for root from 103.x.x.x
Failed password for root from 45.x.x.x

Thousands of attempts per day. From everywhere.

I had key-based SSH. I had fail2ban. I had UFW. But the attempts never stopped because my server's address was public. That's the whole job of a public IP — be findable.

The thing is, my app didn't need to be findable. My domain was. My users hit the API through the domain. The server itself just needed to exist.

Krova Cloud: a server without a public address
Krova Cloud gives you Firecracker microVMs called Cubes. Each Cube has:

Its own kernel
No public IP
A private network
Managed ingress for web traffic
IP-allowlisted port mappings for admin access
Creating one looks like this:

Bash

`npm i -g @krovacloud/cli

krova cubes create fileapi \
--cpu 2 \
--ram 4 \
--disk 40 \
--image ubuntu-24.04

krova ssh fileapi
root@fileapi:~#`
That's the whole setup. No VPC. No security groups. No public IP.

If you prefer APIs:

Bash

curl -X POST https://krova.cloud/api/v1/spaces/$SPACE/cubes \
-H "X-API-KEY: $KROVA_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"name": "fileapi",
"image": "ubuntu-24.04",
"resources": { "vcpu": 2, "ramGb": 4, "diskGb": 40 },
"sshPublicKey": "ssh-ed25519 AAAA...",
"region": "eu-central"
}'

Migrating the app
My stack was simple:

Node.js API running on port 3000
Postgres on the same machine
Nginx as a reverse proxy with HTTPS
On Krova, I kept the same setup but pointed my domain at Krova's managed ingress instead of a public IP. HTTPS was handled automatically. The origin stayed hidden.

For SSH, I opened a port mapping locked to my home IP:

Bash

krova ports add fileapi \
--protocol tcp \
--port 22 \
--allowed-ips 203.0.113.10/32

Only I can reach SSH. Everyone else sees the domain and the API. The server itself is invisible.

The result
After 48 hours, I checked the auth logs.

Zero SSH attempts.

Not "reduced." Not "better." Zero. Because there's no address to attack.

My API response times were the same. My users didn't notice anything. My deploy process barely changed. But the background noise of the internet trying to break in just... stopped.

What about isolation?
I mentioned Cubes are Firecracker microVMs. That means each one boots its own kernel.

For a tiny side project, that's probably overkill. But honestly? I like overkill when it comes to isolation. I've read enough container escape write-ups to prefer a real VM boundary for anything I care about.

text

VPS: public IP + shared kernel with other tenants
Container: maybe public IP + definitely shared kernel
Krova Cube: no public IP + own kernel

For me, that's an easy trade.

Is this for every project?
No.

If you're building a complex multi-region microservices platform, use AWS or GCP. Krova is simpler, which means it does less.

But for side projects, small SaaS apps, CI runners, databases, or anything where "give me an isolated server" is enough, this is the cleanest setup I've found.

Try it yourself
If you're curious, signup doesn't need a card and the first $5 top-up doubles to $10:

Bash

npm i -g @krovacloud/cli
krova signup
krova cubes create test --cpu 1 --ram 2 --disk 20 --image ubuntu-24.04
krova ssh test

Once inside, run:

Bash

ss -tlnp
Nothing is listening publicly. That's the whole point.

The lesson
We accept public IPs as the default because they're familiar. But most apps don't need their server to be addressable from the entire internet. They need a domain, TLS, and a way for the owner to admin things.

Deleting the public IP didn't just reduce my attack surface. It removed the background anxiety that came with it.

Have you run anything without a public IP? Did it feel wrong at first, or immediately better? Drop a comment — I'd love to hear.

Top comments (2)

Collapse
 
techdocs profile image
TechDocs

This makes a lot of sense. If the app only needs to be reachable through the domain, exposing the whole server publicly just adds more risk. Keeping the setup simple and reducing the attack surface is a big win.

Collapse
 
dhruv_malaviya_cdcc71e595 profile image
Dhruv malaviya

Exactly