By Stack
Keeping .env off the public web on shared hosting
Make .env, .git, and vendor unreachable when you deploy Laravel-style apps on shared hosting. The real fix: app body outside the web root, expose only public/. Placement diagram, .htaccess first aid, the restructure, ITD's bootstrap-redirect trap, and self-check.
For: anyone running Laravel-style apps (where only public/ should be exposed) on shared hosting with Apache/.htaccess. Distilled from a real incident (→ .env exposed to the world) — for securing your own deployment.
ITD's view: this isn't 'one person's mistake'
Early shared-hosting culture put everything directly under public_html. Laravel (which expects only public/ exposed) flowed into that, and a dangerous layout became the de-facto standard — even vendors' own docs put projects inside public_html. So the fix is process, not vigilance: change your deployment default.
Get the placement right (diagram)
✗ Dangerous (body at the public root)
public_html/ ├─ .env ← readable! ├─ .git/ vendor/ └─ public/
✓ Safe (body outside, only public)
app-laravel/ ← outside web root ├─ .env .git/ ← unreachable └─ public/ → required from public_html
Step 1: First aid (fast, reversible)
Add a deny block at the top of the web root's .htaccess (a defensive config example). Normal pages are unaffected.
# === SECURITY BLOCK ===
RedirectMatch 404 (?i)/\.(env|git)(\..*)?(/|$)
<FilesMatch "(?i)\.(sql|sql\.gz|bak|old|swp|save|orig|tgz)$|^credentials\.json$|\.bk[._]">
Require all denied
</FilesMatch>
<FilesMatch "(?i)^(phpinfo|info)\.php$">
Require all denied
</FilesMatch>
# if Laravel internals are visible at /<app>/storage/... etc.
RedirectMatch 403 (?i)^/[^/]+/(storage|bootstrap/cache|config|database|resources|routes|tests|vendor)(/|$)
# === END ===First aid isn't the goal
A blacklist is whack-a-mole — you'll forget composer.lock, new filenames add new holes. Treat it as buying time until Step 2.
Step 2: Restructure (the permanent fix)
Move the app body outside the web root and leave a tiny entry point that just loads it.
# 1. Move the body outside the web root (same filesystem → mv is instant, atomic)
mv ~/example/public_html/app ~/example/app-laravel
# 2. Put only a minimal entry in the web root
mkdir -p ~/example/public_html/sub
cat > ~/example/public_html/sub/index.php <<'PHP'
<?php require __DIR__ . '/../../app-laravel/public/index.php';
PHP
# 3. Copy Laravel public/.htaccess, symlink static assets
# 4. Clear config cache
rm -f ~/example/app-laravel/bootstrap/cache/{config,services,packages,routes-v7}.phpITD's hard-won trap: bootstrap-redirect over symlink
Making public_html itself a symlink to public/ can return 500 on a registered subdomain's docroot in some environments (it doesn't follow). Keeping the docroot a real directory and require-ing by absolute path from a small index.php (the "bootstrap-redirect" shape) is more stable on scope-constrained shared hosting. Also, opcache can memorize a failed load and keep returning 500 — last resort is to change the entry filename (entry.php).
Step 3: Rotate keys (assume they were seen)
If there was any exposure window, treat the .env keys as already seen and replace them. Order: external API / OAuth secrets → encryption keys → mail → DB. See the .env glossary. Re-issuing an OAuth secret can invalidate refresh_tokens, so have a re-authorization flow ready first.
Step 4: Self-check (make it a habit)
Finally, confirm nothing is actually exposed on your own site (only against domains you own).
# A 200 with a body means it's exposed. 403/404 is okay for now.
curl -sI https://your-domain/.env | head -1
curl -sI https://your-domain/.git/config | head -1Checking this on every deploy catches placement mistakes early.
Read next
- Incident: The .env exposed to the whole world
- Glossary: What is .env
- Basics: What's dangerous about .env and API keys
FAQ
QWhat's the fastest thing I can do right now?
Add a deny block to your web root's .htaccess for .env, .git, SQL dumps, credentials.json, etc. It's reversible and doesn't affect normal pages — but it's first aid; the real fix is restructuring.
QWhy isn't .htaccess alone enough?
A blacklist is whack-a-mole — new filenames add new holes. Move the app body outside the web root and .env simply can't be reached, with no rules to maintain.
QWhy not just symlink public/ to expose it?
In some environments, re-pointing a registered subdomain's docroot to another tree via symlink doesn't follow and returns 500. In ITD's experience, keeping the docroot a real directory and `require`-ing by absolute path from a small index.php (the 'bootstrap-redirect' shape) is more stable.