How One Security Setting Broke My Website During Testing


When I first started deploying my own applications on a VPS, I thought SSL certificates were the complicated part.

I was wrong.

One of the strangest issues I faced came from a security feature that was actually working exactly as designed.

The feature was called HSTS.

And it completely broke my testing workflow for a while.

Everything Was Working Fine

I had:

  • A VPS running Ubuntu

  • Node.js backend

  • PM2 managing processes

  • Nginx configured

  • My application running correctly

While testing, I was opening the application directly through the VPS IP address.

Something like:

http://123.123.123.123:7001

This was convenient because the domain was not fully configured yet.

I could quickly check whether the backend was running.

Everything looked normal.

Until suddenly the browser stopped opening the application.

Instead, I started getting:

ERR_SSL_PROTOCOL_ERROR

At first, I assumed:

  • Nginx was broken

  • PM2 crashed

  • SSL certificate failed

  • Firewall issue

  • Browser cache issue

I checked almost everything.

Nothing seemed wrong.

The Hidden Culprit: HSTS

After a lot of debugging with AI tools, I learned about a security header called:

Strict-Transport-Security

This is commonly called HSTS.

Its purpose is simple:

When a browser receives this header, it remembers:

"Always use HTTPS for this website in the future."

This is actually a very good security feature.

It protects users from:

  • accidentally using HTTP

  • downgrade attacks

  • insecure connections

For a real production website, this is recommended.

Why The Problem Happened

The issue was not HSTS itself.

The issue was when I enabled it.

At that time, I was still testing using the VPS IP address:

http://123.123.123.123:7001

The browser received the HSTS instruction and remembered:

"This address must always use HTTPS."

From that point forward, the browser automatically started trying:

https://123.123.123.123:7001

But there was a problem.

Port 7001 was just my Node.js application.

It was serving plain HTTP.

There was no SSL certificate attached to that port.

So the browser attempted HTTPS.

The server responded with plain HTTP.

And everything failed.

Result:

ERR_SSL_PROTOCOL_ERROR

The Confusing Part

The application itself was still running.

PM2 was healthy.

The backend process was healthy.

The port was open.

Nothing had actually crashed.

The browser was simply refusing to communicate the way I expected.

That made the issue extremely confusing to diagnose.

How We Fixed It

The solution turned out to be simple.

During testing, HSTS was disabled.

The application only sends HSTS headers when:

ENABLE_HSTS=true

During development and IP-based testing:

ENABLE_HSTS=false

This prevents browsers from permanently forcing HTTPS while the application is still being tested.

The Correct Production Flow

Once everything was ready, I followed the proper deployment sequence:

Step 1

Configure domain:

yourdomain.com

Step 2

Install SSL certificate.

Step 3

Configure Nginx reverse proxy.

Step 4

Verify:

https://yourdomain.com

works correctly.

Step 5

Enable:

ENABLE_HSTS=true

Now the browser is supposed to force HTTPS.

Because HTTPS actually exists.

Why HSTS Is Still A Good Thing

This experience taught me something important.

Many production issues happen because a security feature is working exactly as intended.

HSTS was not broken.

My timing was wrong.

I enabled a production security feature during an earlier testing phase.

Once the domain, SSL certificate, and reverse proxy were configured properly, HSTS became beneficial instead of problematic.

What I Learned

As a non-traditional developer using AI tools, I often assumed:

If a security setting sounds important, enable it immediately.

Now I think differently.

Many security features depend on other infrastructure already being in place.

The sequence matters.

For HSTS, the correct order is:

Domain
↓
SSL Certificate
↓
HTTPS Working
↓
Enable HSTS

Not the other way around.

Sometimes the hardest bugs are not caused by broken code.

They are caused by correctly working systems that were enabled at the wrong time.

Why I Finally Started Using GitHub for Deployment Instead of Local Files

 


After spending weeks pushing files manually to my VPS, I eventually realized something important:

The problem was not PowerShell.

The problem was my workflow.

As a non-coder learning through AI tools like Claude Code and Codex, my initial approach was simple:

  • Edit files locally
  • Upload them to VPS
  • Restart PM2
  • Refresh browser
  • Hope everything works

For small projects, this approach was surprisingly effective.

But as my applications became larger, with React frontends, Node.js backends, databases, and multiple services running together, I started facing problems that had nothing to do with coding.

The real challenge became managing files.

What Is a Deployment Pipeline?

When I first heard terms like CI/CD Pipeline or Deployment Pipeline, I thought they were concepts used only by large software companies.

In reality, a deployment pipeline is simply the path your code takes from development to production.

A very basic pipeline looks like this:

Developer writes code
↓
Code stored in GitHub
↓
Server pulls latest version
↓
Application builds
↓
Application restarts

That is it.

The goal is not complexity.

The goal is consistency.

The Problem With Local Files

When code exists only on your laptop, several problems appear quickly.

Imagine this situation:

  • You update three files.
  • Claude Code modifies two more.
  • Codex suggests another change.
  • You manually upload only some of them.
  • One file gets missed.

Now your VPS and local machine are no longer identical.

The server may be running code that does not exist on your laptop.

Or your laptop may contain code that never reached production.

At that point debugging becomes difficult.

You are no longer debugging code.

You are debugging file versions.

GitHub Became My Single Source of Truth

Once I started using GitHub properly, things became much simpler.

Instead of asking:

"Which version is on the server?"

I could simply check GitHub.

The repository became the official source of the project.

The workflow changed to:

Make changes
↓
Commit
↓
Push to GitHub
↓
Login to VPS
↓
git pull
↓
Build and restart

Now I always know exactly what version is running.

Why This Matters Even More With AI Coding Tools

One thing I discovered while using Claude Code and Codex is that both tools work much better when the project is stored in Git.

AI tools can:

  • Read project structure
  • Understand file history
  • Compare changes
  • Suggest commits
  • Generate pull requests
  • Review modifications

When files only exist locally, collaboration becomes harder.

When everything is stored in GitHub, both humans and AI tools can work from the same source.

Multiple Developers Can Work Simultaneously

Even though I mostly work alone, I finally understood why professional teams rely on Git.

Imagine:

Developer A works on authentication.

Developer B works on dashboard UI.

Developer C fixes backend APIs.

All of them can work simultaneously without overwriting each other's files.

GitHub keeps track of changes and combines them safely.

Without Git, this would quickly become chaotic.

The Biggest Benefit: Confidence

The biggest advantage was not speed.

The biggest advantage was confidence.

Earlier my deployment process looked like this:

Edit file
↓
Upload file
↓
Restart app
↓
Hope everything uploaded correctly

Now it looks like this:

Commit code
↓
Push to GitHub
↓
git pull on VPS
↓
Build
↓
Restart

The process feels predictable.

And predictable systems are easier to trust.

Do I Still Use Direct File Pushes?

Sometimes, yes.

For quick experiments or emergency fixes, directly pushing a single file can still be useful.

But for production applications, GitHub became my preferred workflow.

The larger the application becomes, the more valuable version control becomes.

What I Learned

At first, I thought GitHub was just a backup service.

Later I realized it is actually a communication system between:

  • Developers
  • AI coding tools
  • VPS servers
  • Production applications

Once I understood that, my deployments became simpler, more reliable, and much easier to manage.

And for someone learning software development through AI assistance rather than traditional programming, that was one of the most valuable lessons I learned.

The Second Thing Every Non-Coder Should Do After Buying a VPS: Enable the Firewall

When I first bought my own VPS, I was honestly excited just to see Ubuntu running.

I felt like:

“Now I own a real server.”

I installed the OS, connected through SSH, deployed my app using PM2, opened the domain… and the app worked.

At that moment, I thought:

“Done. Server setup complete.”

But later I realized something important:

Installing the OS is only the first step.

The second most important step is enabling a firewall properly.

And many beginners skip it completely.

What I Thought Initially

As a non-coder learning through AI tools like Claude Code and OpenAI Codex, I initially believed:

  • if SSH works
  • if PM2 is running
  • if my app opens in browser

…then the server is safe.

But that is not true.

By default, many VPS servers expose ports openly unless you explicitly restrict them.

That means:

  • unnecessary ports may remain open
  • services may become reachable publicly
  • random bots can scan your server
  • unwanted traffic can hit services directly

And beginners often do not even realize this is happening.

Why Firewall Matters So Much

Think of a firewall like the main security gate of your VPS.

Without firewall rules, your server may accept connections on ports you forgot about.

For example:

  • development ports
  • test APIs
  • databases
  • admin dashboards
  • temporary Node.js ports

Sometimes while testing apps, we expose ports just to “quickly check something.”

Then we forget to close them.

That becomes dangerous later.

The Mistake Many Beginners Make

The common beginner workflow looks like this:

Buy VPS
↓
Install Ubuntu
↓
Install Node.js
↓
Run PM2
↓
App works
↓
Done

But firewall configuration is missing there.

A safer flow is:

Buy VPS
↓
Install OS
↓
Enable firewall
↓
Allow only required ports
↓
Deploy app
↓
Test app
↓
Open additional ports only if needed

My Simple Rule Now

Now I follow a simple approach:

Open only the ports actually required.

Usually:

  • Port 22 → SSH
  • Port 80 → HTTP
  • Port 443 → HTTPS

Everything else stays blocked unless truly needed.

PM2 and App Testing Changed My Thinking

Initially, when testing PM2 apps, I used to directly expose app ports publicly.

For example:

  • React frontend
  • Node backend
  • test APIs
  • temporary admin panels

Because I just wanted to quickly test whether the app was working.

But later I understood:

just because PM2 runs on a port does not mean that port should remain publicly accessible forever.

Now my flow is more careful.

My Current Beginner-Friendly Workflow

Now I usually do this:

Step 1

Deploy app with PM2.

Step 2

Temporarily allow the app port in firewall.

Step 3

Test whether the app works correctly.

Step 4

Move traffic properly through Nginx reverse proxy.

Step 5

Close unnecessary direct public ports later if not needed.

This reduced confusion and made my setup cleaner.

The Good Thing About VPS Panels Today

The good part is that many VPS providers now offer simple firewall UI panels.

You no longer need to memorize complex Linux firewall commands initially.

You can simply:

  • create ACCEPT rules
  • choose TCP
  • enter port numbers
  • block remaining traffic

That makes learning much easier for non-coders.

One Important Realization

Earlier, I used to think:

“Firewall is only for advanced DevOps engineers.”

But honestly, firewall setup is one of the most practical beginner security steps.

Because when you start hosting:

  • APIs
  • AI tools
  • dashboards
  • databases
  • admin panels

…your VPS slowly becomes a real internet-facing machine.

And internet bots continuously scan public IPs automatically.

I Am Still Learning Too

Even now, I am not a cybersecurity expert or DevOps engineer.

I still learn many things through:

  • AI tools
  • trial and error
  • production mistakes
  • debugging sessions

But enabling firewall early is one habit I am very glad I learned before my projects became larger.

Because small security habits become important very quickly once multiple apps start running on one VPS.

Final Thought

For non-coders entering the VPS world through AI-assisted development:

Buying the server is exciting.

Deploying the first app feels magical.

But learning basic firewall discipline early can save you from many future mistakes.

Not because your VPS will instantly get hacked otherwise.

But because learning:

  • which ports are open
  • which services are public
  • how traffic reaches your app

…is part of truly understanding how your production system works.

Why my web app started showing ???? symbols on pushing file through Powershell

When I first started building apps using AI tools like Claude Code and OpenAI Codex, I honestly did not know much about deployments, Linux servers, SSH, encoding, or production workflows.

Most of my learning was happening while building real apps.

Sometimes Claude Code suggested commands. Sometimes Codex explained errors. Many times, I was just carefully copying commands into PowerShell and hoping I would not break my VPS.

And surprisingly… many things actually worked.

Until one strange issue started appearing everywhere.

???

At first, I thought:

  • my VPS was corrupted
  • React build was failing
  • SSH transfer was broken
  • or maybe AI generated wrong commands

But later I discovered something unexpected.

The problem was encoding.

My Early Deployment Workflow

At that stage, I was not using proper Git-based deployment yet.

For quick frontend fixes, I was directly pushing files from my Windows laptop to the VPS using PowerShell.

Something like this: 

Get-Content "D:\projects\myapp\frontend\src\App.js" -Raw -Encoding UTF8 | ssh root@123.123.123.123 "cat > /opt/myapp/frontend/src/App.js"

For a beginner like me, this honestly felt magical.

I could:

  • edit a file locally
  • push it directly to VPS
  • refresh browser
  • instantly see changes live

No Git pull.
No CI/CD setup.
No rebuilding entire projects.

And for small apps, this method actually worked surprisingly well.

I even wrote a separate article explaining this quick workflow:

How I Push Single Files to My VPS Using PowerShell

Where The Real Problem Started

The strange issue started when I began working on larger React/Vite projects with separate frontend and backend setups.

Initially, my small apps worked fine with this PowerShell pushing method.

But once projects became larger, something unusual started happening:

  • special symbols broke
  • some text became corrupted
  • frontend behaved strangely
  • formatting disappeared
  • random ???? symbols appeared

Sometimes the app still built successfully.

Which made debugging even more confusing.

At first, I wasted time checking:

  • React code
  • PM2
  • Nginx
  • frontend build
  • SSH connection
  • VPS storage

But the actual problem was much simpler.

PowerShell encoding.

The Weird ???? Problem

This is what I started seeing in some files:

???

At first, it looked harmless.

But later I realized:

  • some characters were not transferring correctly
  • UTF-8 text was breaking
  • certain frontend files became partially corrupted

And because React/Vite projects depend heavily on many connected frontend files, even small corruption created weird production behavior.

For a tiny project, this was manageable.

For a bigger frontend/backend app, it started becoming risky.

The Small Fix That Helped

After debugging with AI tools and reading different suggestions online, I started running this before pushing files:

$OutputEncoding = [System.Text.UTF8Encoding]::new($false)
[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false)

This forces PowerShell to properly use UTF-8 encoding while sending content through SSH.

And surprisingly, the strange ???? issue mostly stopped.

Why I Still Liked This Method Initially

Even after these issues, I still liked this workflow during my early learning phase because:

  • it was fast
  • easy to understand
  • beginner-friendly
  • no complex deployment setup
  • useful for quick frontend edits

As someone learning through experimentation and AI guidance, it helped me move quickly without overcomplicating things.

And honestly, for small apps, it worked.

But Later I Learned The Bigger Problem

The encoding issue was only the beginning.

Later, when my apps became larger, I started facing:

  • incomplete uploads
  • partially transferred files
  • broken package.json
  • corrupted frontend bundles
  • blank white production screens

That was the moment I finally understood:

why developers use Git-based deployments instead of manually pushing files through SSH.

 And honestly… that lesson was much more painful.

→ Next Article:

“How a 0.69 KB File Broke My Production App”

How I Push Single Files to My VPS Using PowerShell (Without Git Pull Every Time)


 I am not a traditional coder. Most of my development journey is happening through AI tools like Anthropic Claude Code and OpenAI Codex.

Many times, I just explain what I want, and AI guides me with commands, fixes, and debugging steps.

But when you are learning like this, small issues can waste hours.

One issue I faced was very strange.

Whenever I pushed some files from my Windows PC to my VPS, symbols and text sometimes became broken. Instead of proper characters, I started seeing:

???

Especially in special symbols, formatted text, or sometimes Hindi content.

At first, I thought maybe my VPS was broken, or Claude Code generated something wrong. But after debugging with Claude Code only, I learned it was actually an encoding issue in PowerShell that happen when we push local files to VPS.

So now, before pushing files through SSH, I first run this code in Windows Powershell or Command Prompt:

$OutputEncoding = [System.Text.UTF8Encoding]::new($false)
[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false)

This forces PowerShell to use UTF-8 properly and avoids corrupted characters while sending content.

After that, I push a single file directly from my local system to the VPS using this command in Windows Command Prompt or Powershell:

Get-Content "D:\projects\myapp\frontend\src\App.js" -Raw -Encoding UTF8 | ssh root@123.123.123.123 "cat > /opt/myapp/frontend/src/App.js"

What this command basically does:

  • Reads the local file from your PC

  • Sends its content through SSH

  • Replaces the file directly on the VPS

For beginners or vibe coders, this feels much easier than rebuilding or uploading the whole project every time.

Why I personally like this method:

  • Very fast for small fixes

  • No need for full deployment

  • Useful when only one file changed

  • Easy to understand compared to complex DevOps pipelines

  • Works nicely when AI tools are helping you debug live issues

One important thing:

I have hidden my real project folders, server IP, and actual app names in the example above for security reasons. Never expose your real VPS IP or sensitive folder structure publicly in blogs or screenshots.

Honestly, many things in my journey are still trial and error. Sometimes Claude Code suggests one thing, Codex suggests another, and I test both until something works. But small tricks like this save a lot of time when you are building apps without deep coding knowledge.

Why I Bought My Own VPS as a Non-Coder (Even When Vercel and Railway Were Easier)

When I started building apps using AI tools like Claude Code and OpenAI Codex, I honestly did not know much about servers, Docker, deployments, or Linux commands.

In fact, platforms like Vercel and Railway felt magical to me.

You connect GitHub, click deploy, and your app goes live.

For many people, that is already enough.

So naturally, people may ask:

“If these platforms were already simple, why did you buy your own VPS?”

Honestly, even I asked myself the same question before spending money.

The VPS I Bought

I finally purchased a VPS plan costing around $87/year.

Configuration was something like:

  • 4 GB RAM
  • 4 vCPU
  • around 75 GB NVMe or 150 GB SSD storage

At that time, it felt like a huge decision for me because I am not a professional DevOps engineer or backend developer.

I was just someone trying to build real apps using AI assistance.

One Big Reason: Monthly Costs Started Scaring Me

In the beginning, Vercel and Railway were amazing.

They helped me:

  • deploy quickly
  • avoid Linux setup
  • avoid server management
  • focus on building features

And honestly, I still think they are excellent platforms for beginners.

But slowly my projects started becoming larger.

I started testing:

  • AI apps
  • React dashboards
  • Python APIs
  • PostgreSQL databases
  • image uploads
  • OCR tools
  • multiple frontend/backend apps together

And then I started noticing something:

Even without revenue, I was already paying for:

  • OpenAI APIs
  • Claude subscriptions
  • domains
  • databases
  • deployment platforms

Sometimes around $20/month or more was already going into hosting and related services.

Then another fear started:

“What happens if bandwidth or usage suddenly increases?”

Because on managed platforms:

  • bandwidth usage
  • serverless executions
  • database usage
  • storage
  • build minutes

…can slowly increase costs.

And when your apps are not earning yet, every recurring monthly expense starts feeling heavy.

That became one of the biggest reasons I started looking at a personal VPS.

The Feeling of “This Server Is Mine”

With the VPS, I liked the simplicity of:

“I already paid for the machine. Now I can experiment freely.”

That mental peace mattered to me.

I could:

  • host multiple apps
  • run frontend + backend together
  • test APIs
  • store files
  • use PM2
  • run cron jobs
  • experiment without checking usage dashboards every few hours

Not because VPS was cheaper in every situation.

But because the pricing felt predictable for my learning phase.

Another Reason Was Freedom

On platforms like Vercel or Railway, many things are simplified — which is actually great.

But sometimes I wanted to:

  • restart services manually
  • access logs deeply
  • control deployments
  • test Docker
  • learn Linux commands
  • understand how apps run in production

A VPS gave me that freedom.

Not because managed platforms are bad.

But because my curiosity started increasing.

But VPS Was Definitely NOT Easier

This is important.

Buying a VPS did NOT suddenly make me a developer.

Actually, it created many new problems:

  • Linux confusion
  • broken deployments
  • PM2 issues
  • Docker mistakes
  • Nginx errors
  • permission problems
  • restart failures
  • white screen production bugs

Sometimes I spent hours debugging problems that Vercel would have handled automatically.

So I do not think VPS is “better” for everyone.

For many developers, managed platforms are probably the smarter choice.

Then Why Do I Still Think Buying It Was Worth It?

Because it forced me to learn.

Not in a classroom way.

But through real production mistakes.

I slowly started understanding:

  • how apps actually run
  • how frontend and backend connect
  • what happens during deployment
  • why Git matters
  • why logs matter
  • how servers behave under memory pressure

Even today, I still depend heavily on AI tools for guidance.

Many commands are still suggested by Claude Code or Codex.

But now when something breaks, I panic less because I understand the flow slightly better than before.

My Honest Opinion So Far

For simple projects:

  • Vercel
  • Railway
  • Firebase

…are honestly amazing.

But if you are like me:

  • experimenting with multiple apps
  • testing AI workflows
  • learning deployment
  • hosting several side projects together
  • trying to reduce monthly recurring costs during the no-revenue phase

…then owning a VPS can become a valuable learning experience.

Not because it is perfect.

But because it teaches you what is happening behind the scenes.