Skip to content

How Do You Secure Vibecoded Apps Before Deployment?

Problem

When I deployed my first vibecoded app, I thought everything was fine. The app worked, users could sign up, and the database was running. Then I checked my server logs a week later:

[ERROR] Connection refused: port 5432 exposed to public
[WARN] High CPU usage from unknown IP addresses
[ALERT] Database storage at 95% - bots created 50,000 fake accounts

I had no HTTPS, no monitoring, no bot protection, and no backups. The app that worked perfectly in development became a security nightmare in production.

What Happened?

I was vibecoding with AI tools, focused on shipping features fast. The AI helped me build a working prototype in days. But it didn’t remind me about security basics:

  • No HTTPS configured
  • No error tracking or monitoring
  • No rate limiting or bot protection
  • No automated backups
  • Debug endpoints still exposed
  • Unused ports left open

As someone on Reddit put it: “Shipping early is a feature, but shipping insecurely is a bug that eventually kills your startup.”

The Solution

I created a pre-deployment security checklist. Here’s what I learned.

Step 1: HTTPS Configuration

First, I checked if my app had HTTPS:

Terminal window
curl -I http://myapp.com
# HTTP/1.1 200 OK <-- No HTTPS!

This was wrong. Every production app needs encrypted connections. I set up Let’s Encrypt:

Setup Let's Encrypt SSL
# Install certbot
sudo apt install certbot python3-certbot-nginx
# Get free SSL certificate
sudo certbot --nginx -d myapp.com -d www.myapp.com
# Auto-renewal
sudo systemctl enable certbot.timer

Then I configured Nginx to force HTTPS:

/etc/nginx/sites-available/myapp
# Force HTTPS redirect
server {
listen 80;
server_name myapp.com www.myapp.com;
return 301 https://$server_name$request_uri;
}
# HTTPS server
server {
listen 443 ssl http2;
server_name myapp.com www.myapp.com;
ssl_certificate /etc/letsencrypt/live/myapp.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/myapp.com/privkey.pem;
# Security headers
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Frame-Options DENY always;
add_header X-Content-Type-Options nosniff always;
}

Now when I checked:

Terminal window
curl -I https://myapp.com
# HTTP/2 200
# strict-transport-security: max-age=31536000; includeSubDomains

Step 2: Real-Time Monitoring

Next, I added error tracking with Sentry:

app.py
import sentry_sdk
from flask import Flask
# Initialize Sentry before creating the app
sentry_sdk.init(
dsn="https://[email protected]/project-id",
traces_sample_rate=0.1, # 10% of transactions
profiles_sample_rate=0.1,
environment="production"
)
app = Flask(__name__)
@app.route("/")
def index():
try:
result = risky_operation()
return result
except Exception as e:
# This error is now tracked in Sentry
sentry_sdk.capture_exception(e)
return "An error occurred", 500

I also added structured logging:

logging_config.py
import logging
import json
from datetime import datetime
class JSONFormatter(logging.Formatter):
def format(self, record):
log_entry = {
"timestamp": datetime.utcnow().isoformat(),
"level": record.levelname,
"message": record.getMessage(),
"module": record.module,
"function": record.funcName,
}
if record.exc_info:
log_entry["exception"] = self.formatException(record.exc_info)
return json.dumps(log_entry)
# Configure logging
logging.basicConfig(
level=logging.INFO,
handlers=[logging.StreamHandler()]
)
logger = logging.getLogger()
logger.handlers[0].setFormatter(JSONFormatter())

Now I get alerts when things break:

[Sentry Alert] Error: ConnectionTimeout in app.py:42
[Log] {"level": "ERROR", "message": "Database connection failed", "count": 3}

Step 3: Bot Protection

I noticed my free tier limits were being exhausted. Bots were creating thousands of fake accounts. I added Cloudflare Turnstile:

templates/signup.html
<form action="/signup" method="POST">
<input type="email" name="email" required>
<input type="password" name="password" required>
<!-- Cloudflare Turnstile widget -->
<div class="cf-turnstile"
data-sitekey="your-site-key"
data-callback="onTurnstileSuccess">
</div>
<button type="submit">Sign Up</button>
</form>
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
app.py
import requests
from flask import request, jsonify
TURNSTILE_SECRET = os.environ.get("TURNSTILE_SECRET_KEY")
def verify_turnstile(token, ip):
"""Verify Cloudflare Turnstile token"""
response = requests.post(
"https://challenges.cloudflare.com/turnstile/v0/siteverify",
data={
"secret": TURNSTILE_SECRET,
"response": token,
"remoteip": ip
}
)
result = response.json()
return result.get("success", False)
@app.route("/signup", methods=["POST"])
def signup():
turnstile_token = request.form.get("cf-turnstile-response")
if not verify_turnstile(turnstile_token, request.remote_addr):
return "Bot verification failed", 403
# Continue with signup
email = request.form.get("email")
# ... create user account

I also added rate limiting:

rate_limiter.py
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
limiter = Limiter(
app=app,
key_func=get_remote_address,
default_limits=["200 per day", "50 per hour"],
storage_uri="redis://localhost:6379"
)
@app.route("/api/login", methods=["POST"])
@limiter.limit("5 per minute") # Stricter for sensitive endpoints
def login():
# Login logic
pass
@app.route("/api/signup", methods=["POST"])
@limiter.limit("3 per hour") # Prevent mass account creation
def signup():
# Signup logic
pass

Step 4: Automated Backups

I set up automated database backups:

backup.sh
#!/bin/bash
# Database backup script
BACKUP_DIR="/backups/postgres"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
DB_NAME="myapp_db"
# Create backup
mkdir -p $BACKUP_DIR
pg_dump -U postgres $DB_NAME | gzip > "$BACKUP_DIR/${DB_NAME}_${TIMESTAMP}.sql.gz"
# Upload to S3 (optional)
aws s3 cp "$BACKUP_DIR/${DB_NAME}_${TIMESTAMP}.sql.gz" s3://my-backups/db/
# Keep only last 7 days
find $BACKUP_DIR -name "*.gz" -mtime +7 -delete
echo "Backup completed: ${DB_NAME}_${TIMESTAMP}.sql.gz"
crontab -e
# Daily backup at 2 AM
0 2 * * * /home/user/scripts/backup.sh >> /var/log/backup.log 2>&1

I also tested restore:

Terminal window
# Test restore (do this monthly!)
gunzip -c /backups/postgres/myapp_db_20260321_020000.sql.gz | psql -U postgres test_restore_db
# Verify restore
psql -U postgres test_restore_db -c "SELECT COUNT(*) FROM users;"

Step 5: Surface Area Reduction

Finally, I scanned for unnecessary exposure. I asked the AI that helped me build the app:

“Check my server for open ports and services I don’t need”

It ran:

Terminal window
# Check open ports
sudo ss -tlnp
# Output showed:
# LISTEN 0 128 *:22 *:* users:(("sshd"))
# LISTEN 0 128 *:80 *:* users:(("nginx"))
# LISTEN 0 128 *:443 *:* users:(("nginx"))
# LISTEN 0 128 *:5432 *:* users:(("postgres")) <-- PUBLIC!
# LISTEN 0 128 *:6379 *:* users:(("redis")) <-- PUBLIC!
# LISTEN 0 128 *:3000 *:* users:(("node")) <-- DEV SERVER!

I had three problems:

  1. PostgreSQL was exposed to public (port 5432)
  2. Redis was exposed to public (port 6379)
  3. A dev server was still running (port 3000)

I fixed this:

Terminal window
# Stop unnecessary services
sudo systemctl stop node-dev-app
sudo systemctl disable node-dev-app
# Bind PostgreSQL to localhost only
sudo vim /etc/postgresql/14/main/postgresql.conf
# listen_addresses = 'localhost'
# Bind Redis to localhost only
sudo vim /etc/redis/redis.conf
# bind 127.0.0.1
# Restart services
sudo systemctl restart postgresql
sudo systemctl restart redis

I also removed debug endpoints from my code:

app.py
# REMOVE THESE BEFORE DEPLOYMENT:
@app.route("/debug/users")
def debug_users(): # Never expose this!
return jsonify(User.query.all())
@app.route("/debug/env")
def debug_env(): # Never expose this!
return jsonify(dict(os.environ))
@app.route("/debug/reset-db")
def debug_reset(): # Never expose this!
db.drop_all()
db.create_all()
return "Reset complete"

Common Mistakes

Looking back, here are the mistakes I made:

MistakeWhy It’s BadFix
No HTTPSData transmitted in plain textUse Let’s Encrypt
No monitoringErrors go unnoticedAdd Sentry or Datadog
No bot protectionBots drain resourcesAdd Turnstile or reCAPTCHA
No backupsData loss is permanentSet up daily backups
Debug endpoints exposedSecurity vulnerabilitiesRemove or protect with auth
Unused ports openAttack surface expandedClose with firewall

The Checklist

Before every deployment, I now run through this checklist:

[ ] HTTPS configured with valid SSL certificate
[ ] Error tracking installed (Sentry, Datadog, etc.)
[ ] Activity logging enabled
[ ] Rate limiting on sensitive endpoints
[ ] Bot protection on forms
[ ] Automated backups configured
[ ] Backup restore tested
[ ] All debug endpoints removed or protected
[ ] Database not exposed to public
[ ] Redis not exposed to public
[ ] Unused ports closed
[ ] Unused services disabled

Summary

In this post, I explained how to secure vibecoded apps before deployment. The key points are:

  • HTTPS is non-negotiable - Let’s Encrypt is free and easy
  • Monitoring catches issues early - set up before you need it
  • Bot protection prevents resource drain - add before hitting production
  • Backups enable recovery - test restores before you need them
  • Reduce attack surface - close unused ports and services

Speed to market matters, but a single security incident can erase all momentum. Users will not forgive data breaches, and recovery costs dwarf the time saved by skipping security.

Final Words + More Resources

My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me

Here are also the most important links from this article along with some further resources that will help you in this scope:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments