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 accountsI 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:
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:
# Install certbotsudo apt install certbot python3-certbot-nginx
# Get free SSL certificatesudo certbot --nginx -d myapp.com -d www.myapp.com
# Auto-renewalsudo systemctl enable certbot.timerThen I configured Nginx to force HTTPS:
# Force HTTPS redirectserver { listen 80; server_name myapp.com www.myapp.com; return 301 https://$server_name$request_uri;}
# HTTPS serverserver { 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:
curl -I https://myapp.com# HTTP/2 200# strict-transport-security: max-age=31536000; includeSubDomainsStep 2: Real-Time Monitoring
Next, I added error tracking with Sentry:
import sentry_sdkfrom flask import Flask
# Initialize Sentry before creating the appsentry_sdk.init( 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", 500I also added structured logging:
import loggingimport jsonfrom 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 logginglogging.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:
<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>import requestsfrom 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 accountI also added rate limiting:
from flask_limiter import Limiterfrom 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 endpointsdef login(): # Login logic pass
@app.route("/api/signup", methods=["POST"])@limiter.limit("3 per hour") # Prevent mass account creationdef signup(): # Signup logic passStep 4: Automated Backups
I set up automated database backups:
#!/bin/bash
# Database backup scriptBACKUP_DIR="/backups/postgres"TIMESTAMP=$(date +%Y%m%d_%H%M%S)DB_NAME="myapp_db"
# Create backupmkdir -p $BACKUP_DIRpg_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 daysfind $BACKUP_DIR -name "*.gz" -mtime +7 -delete
echo "Backup completed: ${DB_NAME}_${TIMESTAMP}.sql.gz"# Daily backup at 2 AM0 2 * * * /home/user/scripts/backup.sh >> /var/log/backup.log 2>&1I also tested restore:
# Test restore (do this monthly!)gunzip -c /backups/postgres/myapp_db_20260321_020000.sql.gz | psql -U postgres test_restore_db
# Verify restorepsql -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:
# Check open portssudo 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:
- PostgreSQL was exposed to public (port 5432)
- Redis was exposed to public (port 6379)
- A dev server was still running (port 3000)
I fixed this:
# Stop unnecessary servicessudo systemctl stop node-dev-appsudo systemctl disable node-dev-app
# Bind PostgreSQL to localhost onlysudo vim /etc/postgresql/14/main/postgresql.conf# listen_addresses = 'localhost'
# Bind Redis to localhost onlysudo vim /etc/redis/redis.conf# bind 127.0.0.1
# Restart servicessudo systemctl restart postgresqlsudo systemctl restart redisI also removed debug endpoints from my code:
# 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:
| Mistake | Why It’s Bad | Fix |
|---|---|---|
| No HTTPS | Data transmitted in plain text | Use Let’s Encrypt |
| No monitoring | Errors go unnoticed | Add Sentry or Datadog |
| No bot protection | Bots drain resources | Add Turnstile or reCAPTCHA |
| No backups | Data loss is permanent | Set up daily backups |
| Debug endpoints exposed | Security vulnerabilities | Remove or protect with auth |
| Unused ports open | Attack surface expanded | Close 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 disabledSummary
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:
- 👨💻 Let's Encrypt - Free SSL Certificates
- 👨💻 Sentry - Error Tracking
- 👨💻 Cloudflare Turnstile - Bot Protection
- 👨💻 Datadog - Monitoring Platform
Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!
Comments