Production Reference
Deploying a Laravel App to a Live VPS
A complete, beginner-friendly, end-to-end reference for taking a Laravel application from a GoDaddy domain and a bare Ubuntu VPS to a fully secured, optimized, production-ready website — covering DNS, Apache and Nginx, PHP, MySQL, Git, SSL, database administration, backups, and ongoing maintenance.
The Stack You're Building
1. Domain Purchase & DNS Configuration
DNS translates yourdomain.com into an IP address, since computers route traffic by IP, not name. When someone visits your domain, their device asks DNS "what IP is this?", gets your VPS's IP back, and connects directly to it on port 80/443.
Step 1 — Get your VPS's public IP
Found in your Hostinger hPanel → VPS → Overview.
Step 2 — Set A records in GoDaddy
GoDaddy → My Domains → DNS Management. Remove any default parking/forwarding records first, then add:
| Type | Name | Value | TTL |
|---|---|---|---|
| A | @ | your VPS IP | 600s |
| A | www | your VPS IP | 600s |
⚠ Common conflict
GoDaddy often ships a default CNAME record for www. You cannot have both a CNAME and A record on the same name — delete/edit the existing CNAME instead of adding a duplicate.
⚠ Domain forwarding overrides DNS
If your domain still shows a GoDaddy landing page after correct A records, check Domain Settings → Forwarding and disable/delete any forwarding rule — it intercepts requests independently of your DNS records.
Step 3 — Verify propagation
DNS changes take 10 minutes–48 hours to propagate.
dig yourdomain.com +short
curl -I http://your.vps.ip
Or check dnschecker.org.
2. Initial Ubuntu VPS Setup
Connect via SSH
ssh root@your_vps_ip
First connection shows a host-authenticity warning — type yes, this is normal and only appears once.
Update the system
apt update && apt upgrade -y
Create a non-root user
Never operate daily as root — one mistake or exploited bug can compromise the whole system.
adduser deployer
usermod -aG sudo deployer
Note: adduser will prompt you to set a password interactively (nothing displays as you type — that's normal) and ask for Full Name/Room Number/Phone fields. These are legacy cosmetic fields with zero functional effect — press Enter to skip all of them.
Log in as your new user going forward
ssh deployer@your_vps_ip
The login banner mentioning "ESM Apps" or "System restart required" is standard Ubuntu messaging — not an error. A reboot (sudo reboot) can be done anytime, not urgently.
3. Server Security Essentials
Configure the firewall (UFW)
⚠ Always allow SSH before enabling UFW
Skipping this order can permanently lock you out of the server.
sudo ufw allow OpenSSH
sudo ufw allow 'Apache Full' # or 80,443/tcp for Nginx
sudo ufw enable # type 'y' to confirm
sudo ufw status
Expected output includes OpenSSH ALLOW Anywhere and your web server rule as ALLOW.
Harden SSH — disable root login
Only do this after confirming your deployer + sudo login works.
sudo nano /etc/ssh/sshd_config
# find the line: PermitRootLogin yes
# change to: PermitRootLogin no
# Save: Ctrl+O, Enter, Ctrl+X
sudo sshd -t # validates config, no output = OK
sudo systemctl restart ssh
Test in a new terminal (keep the current one open): ssh root@vps_ip should now be refused, while ssh deployer@vps_ip should still work.
Also check Hostinger's own network firewall
Separate from UFW — in hPanel → your VPS → Firewall, confirm ports 80 and 443 are allowed inbound. A running Apache/Nginx service can still be unreachable externally if this panel-level firewall blocks it.
4. Web Server Option A — Apache (Full Setup)
Step 1 — Install Apache
sudo apt update
sudo apt install apache2 -y
sudo systemctl enable apache2
sudo systemctl start apache2
sudo systemctl status apache2 # confirm "active (running)"
Step 2 — Verify it's reachable before adding Laravel/DNS complexity
curl -I http://your_vps_ip
# from a browser, http://your_vps_ip should show the Apache2 Ubuntu Default Page
Step 3 — Enable required modules
sudo a2enmod rewrite headers ssl
sudo systemctl restart apache2
rewrite— required for Laravel's clean URLs (removingindex.php)headers— needed for security headersssl— needed for HTTPS later
Step 4 — Choose PHP handler: mod_php vs PHP-FPM
Apache can run PHP two ways. Pick one:
| Method | Notes |
|---|---|
| libapache2-mod-php8.3 | Simplest — PHP runs embedded inside Apache's own processes. Fine for small/medium sites. |
| php8.3-fpm + mod_proxy_fcgi | PHP runs as a separate FPM pool; better performance/isolation under load. |
If using PHP-FPM with Apache instead of mod_php:
sudo apt install php8.3-fpm libapache2-mod-fcgid -y
sudo a2enmod proxy_fcgi setenvif
sudo a2enconf php8.3-fpm
sudo systemctl restart apache2
sudo systemctl enable php8.3-fpm --now
Step 5 — Create the VirtualHost (see Part 12 for full file)
Full VirtualHost content, enabling, and the exact AllowOverride requirement are covered in Part 12 — do that step next, then return here to finish enabling the site below.
sudo a2ensite yourdomain.com.conf
sudo a2dissite 000-default.conf
sudo apache2ctl configtest
sudo systemctl reload apache2
Useful Apache management commands
sudo systemctl start apache2
sudo systemctl stop apache2
sudo systemctl restart apache2 # full restart, drops connections briefly
sudo systemctl reload apache2 # applies config changes, no downtime
sudo apache2ctl configtest # always run before reload/restart
apache2 -v # check installed version
sudo a2ensite .conf # enable a site
sudo a2dissite .conf # disable a site
sudo a2enmod # enable a module
sudo a2dismod # disable a module
Log locations
sudo tail -f /var/log/apache2/error.log
sudo tail -f /var/log/apache2/access.log
sudo tail -f /var/log/apache2/yourdomain_error.log # per-vhost, if configured
⚠ The #1 cause of "routes 404 but homepage works"
Your VirtualHost's <Directory> block must have AllowOverride All and must exactly match your real DocumentRoot path (typos like a missing letter break this silently, with no error shown). See Part 12.
4b. Web Server Option B — Nginx (Full Setup)
Nginx doesn't execute PHP itself — it needs PHP-FPM (a separate process) to hand off .php requests to over a socket or TCP port. Choose either Apache or Nginx, not both, unless you know why you'd want that.
Step 1 — Install Nginx
sudo apt update
sudo apt install nginx -y
sudo systemctl enable nginx
sudo systemctl start nginx
sudo systemctl status nginx # confirm "active (running)"
curl -I http://your_vps_ip
# browser: http://your_vps_ip should show "Welcome to nginx!"
Step 2 — Install PHP-FPM
Nginx requires PHP-FPM specifically (not mod_php, which is an Apache-only module). Install the same extensions as Part 5, plus the FPM package:
sudo apt install php8.3-fpm php8.3-mysql php8.3-xml php8.3-mbstring \
php8.3-curl php8.3-zip php8.3-bcmath php8.3-gd php8.3-intl php8.3-cli -y
sudo systemctl enable php8.3-fpm --now
sudo systemctl status php8.3-fpm # confirm "active (running)"
Confirm the socket file exists (referenced in the server block below):
ls /run/php/php8.3-fpm.sock
Step 3 — Remove/keep the default site
sudo rm /etc/nginx/sites-enabled/default # optional, avoids confusion with your vhost
Step 4 — Create the server block for Laravel
sudo nano /etc/nginx/sites-available/yourdomain.com
server {
listen 80;
server_name yourdomain.com www.yourdomain.com;
root /var/www/yourdomain.com/public;
add_header X-Frame-Options "SAMEORIGIN";
add_header X-Content-Type-Options "nosniff";
index index.php;
charset utf-8;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location = /favicon.ico { access_log off; log_not_found off; }
location = /robots.txt { access_log off; log_not_found off; }
error_page 404 /index.php;
location ~ \.php$ {
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;
}
location ~ /\.(?!well-known).* {
deny all;
}
client_max_body_size 20M; # raise if you handle large file uploads
}
Step 5 — Enable the site & test config
sudo ln -s /etc/nginx/sites-available/yourdomain.com /etc/nginx/sites-enabled/
sudo nginx -t # test config syntax — must show "syntax is ok"
sudo systemctl reload nginx
Step 6 — File permissions (same as Apache, different group nuance)
sudo chown -R deployer:www-data /var/www/yourdomain.com
sudo chmod -R ug+rwx storage bootstrap/cache
Nginx itself typically runs as www-data, but PHP-FPM's pool user matters more here — check /etc/php/8.3/fpm/pool.d/www.conf for the user/group directives (default is also www-data).
Useful Nginx & PHP-FPM commands
sudo systemctl start nginx
sudo systemctl stop nginx
sudo systemctl restart nginx
sudo systemctl reload nginx # no-downtime config reload
sudo nginx -t # test config before every reload
nginx -v # check version
sudo systemctl restart php8.3-fpm # restart FPM after php.ini changes
sudo systemctl status php8.3-fpm
Log locations
sudo tail -f /var/log/nginx/error.log
sudo tail -f /var/log/nginx/access.log
sudo tail -f /var/log/php8.3-fpm.log
Key difference from Apache: Nginx has no .htaccess equivalent — all rewrite logic lives directly in the server block's try_files directive shown above, which already handles Laravel's clean URLs correctly out of the box. Any change to the server block requires nginx -t then reload to take effect — unlike Apache's .htaccess, which applies live without a reload.
⚠ 502 Bad Gateway
Almost always means PHP-FPM isn't running, or the socket path in fastcgi_pass doesn't match the actual socket file. Check sudo systemctl status php8.3-fpm and ls /run/php/ to confirm the exact socket filename.
5. PHP Installation & Laravel Extensions
Check your Laravel version's PHP requirement first (e.g. Laravel 11/12 needs PHP 8.2+).
sudo apt install software-properties-common -y
sudo add-apt-repository ppa:ondrej/php -y
sudo apt update
sudo apt install php8.3 libapache2-mod-php8.3 \
php8.3-mysql php8.3-xml php8.3-mbstring php8.3-curl \
php8.3-zip php8.3-bcmath php8.3-gd php8.3-intl php8.3-cli \
php8.3-fpm -y # php8.3-fpm only needed for Nginx setups
| Extension | Why it's needed |
|---|---|
| mysql | Database driver |
| xml / mbstring | Core Laravel dependencies |
| curl | Outgoing HTTP requests |
| zip | Composer package extraction |
| bcmath | Precise math (billing, etc.) |
| gd | Image processing |
| intl | Localization |
php -v
6. MySQL Installation & Configuration
sudo apt install mysql-server -y
sudo mysql_secure_installation
Walking through the secure-installation wizard
- Setup VALIDATE PASSWORD component? → Press
Y, then choose1(MEDIUM) for a good balance of strength vs. convenience. - Set root password → choose a strong password and save it somewhere safe (this is MySQL's own root, separate from Ubuntu's).
- Remove anonymous users? →
Y - Disallow root login remotely? →
Y - Remove test database? →
Y - Reload privilege tables now? →
Y
Changing the password policy later: you don't need to rerun the wizard — from inside mysql, run SHOW VARIABLES LIKE 'validate_password%'; to check current settings, or SET GLOBAL validate_password.policy = LOW; to temporarily relax it (resets on reboot unless made permanent in the config file).
7. Database Creation & Dedicated User
Never let Laravel use the MySQL root user — a scoped user limits damage if the app is ever compromised.
sudo mysql -u root -p
CREATE DATABASE laravel_app;
CREATE USER 'laravel_user'@'localhost' IDENTIFIED BY 'Strong_Pass_2026';
GRANT ALL PRIVILEGES ON laravel_app.* TO 'laravel_user'@'localhost';
FLUSH PRIVILEGES;
EXIT;
⚠ Avoid these characters in MySQL passwords
Never use ;, \, quotes, or backticks — ; terminates the SQL statement early, causing confusing syntax errors, and \ is an escape character. Stick to letters, numbers, and safe symbols like ! _ - @.
⚠ localhost vs 127.0.0.1
MySQL treats these as different hosts for authentication (localhost = Unix socket, 127.0.0.1 = TCP). If your .env's DB_HOST doesn't match the host the user was granted on, you'll get "Access denied" even with the correct password. Safest fix — grant the user on all three:
CREATE USER 'laravel_user'@'127.0.0.1' IDENTIFIED BY 'Strong_Pass_2026';
GRANT ALL PRIVILEGES ON laravel_app.* TO 'laravel_user'@'127.0.0.1';
CREATE USER 'laravel_user'@'%' IDENTIFIED BY 'Strong_Pass_2026';
GRANT ALL PRIVILEGES ON laravel_app.* TO 'laravel_user'@'%';
FLUSH PRIVILEGES;
Verify a user's grants anytime
SELECT User, Host FROM mysql.user WHERE User='laravel_user';
8. Git & Composer Installation
Git
sudo apt install git -y
git --version
Composer
php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
sudo php composer-setup.php --install-dir=/usr/local/bin --filename=composer
php -r "unlink('composer-setup.php');"
composer -V
"Directory not writable" error? Run the install command with sudo as shown above, since /usr/local/bin is root-owned.
9. Deploying the Laravel Project
Create the project directory
sudo mkdir -p /var/www/yourdomain.com
sudo chown -R deployer:deployer /var/www/yourdomain.com
cd /var/www/yourdomain.com
Clone from GitHub (recommended over manual upload)
git clone https://github.com/yourusername/your-repo.git .
Using Git from the start makes future updates (Part 18) a simple git pull instead of re-uploading files manually.
Install PHP dependencies
composer install --optimize-autoloader --no-dev
--no-dev skips testing/dev-only tools, keeping production leaner and more secure.
⚠ composer.lock / composer.json version mismatch
If you see "package X is in the lock file as vY but constraint requires vZ" — sync them with:
composer update package/name --with-all-dependencies
Commit the updated composer.lock afterward so this doesn't recur on the next deploy.
Install Node.js & build frontend assets (Vite)
Modern Laravel apps compile CSS/JS via Vite. Skipping this causes a Vite manifest not found 500 error.
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt install -y nodejs
npm install
npm run build
ls public/build/manifest.json # confirm it now exists
10. Environment Configuration (.env)
cp .env.example .env
nano .env
APP_ENV=production
APP_DEBUG=false
APP_URL=https://yourdomain.com
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=laravel_app
DB_USERNAME=laravel_user
DB_PASSWORD="Strong_Pass_2026"
⚠ APP_DEBUG=false is critical
With debug mode on, any error page leaks full stack traces including DB credentials and file paths to any visitor. Always false in production.
⚠ Quote passwords with special characters
Characters like # start a comment in .env — anything after it on that line is silently dropped. Wrap values in double quotes to be safe, or avoid special characters in the password entirely.
Generate the application key
php artisan key:generate
Used to encrypt sessions/cookies — without it, Laravel throws a "no application encryption key" error.
11. File & Directory Permissions
Apache/Nginx runs as user www-data. Laravel needs write access to storage/ and bootstrap/cache/ for logs, sessions, and compiled views.
sudo chown -R deployer:www-data /var/www/yourdomain.com
sudo find /var/www/yourdomain.com -type f -exec chmod 644 {} \;
sudo find /var/www/yourdomain.com -type d -exec chmod 755 {} \;
sudo chmod -R ug+rwx storage bootstrap/cache
644/755 is the safe baseline; storage/cache need write access as the exception, not the rule.
12. Virtual Host Configuration
Laravel's entry point is public/index.php — the web server's document root must point to the public/ folder, never the project root.
Apache VirtualHost
sudo nano /etc/apache2/sites-available/yourdomain.com.conf
<VirtualHost *:80>
ServerName yourdomain.com
ServerAlias www.yourdomain.com
DocumentRoot /var/www/yourdomain.com/public
<Directory /var/www/yourdomain.com/public>
AllowOverride All
Require all granted
</Directory>
ErrorLog ${APACHE_LOG_DIR}/yourdomain_error.log
CustomLog ${APACHE_LOG_DIR}/yourdomain_access.log combined
</VirtualHost>
sudo a2ensite yourdomain.com.conf
sudo a2dissite 000-default.conf
sudo apache2ctl configtest
sudo systemctl reload apache2
⚠ Real bug encountered: a one-letter typo
A <Directory> path reading .../dorbellindia.co/public (missing the final "m") caused AllowOverride All to silently never apply — the path simply didn't match the real DocumentRoot. Symptom: homepage loads fine, but every other route returns Apache's raw "Not Found" (not Laravel's 404). Always copy-paste the exact DocumentRoot path into the Directory block rather than retyping it.
Nginx equivalent
See Part 4b — the root directive plays the same role as DocumentRoot, and clean-URL routing is handled by try_files rather than .htaccess.
13. Migrations, Storage Link & Seeding
php artisan migrate --force
php artisan storage:link
php artisan db:seed --force # optional, only if you have seeders
--force is required because Artisan blocks destructive DB commands in production unless explicitly confirmed. storage:link creates the symlink from public/storage to storage/app/public, needed for publicly accessible uploaded files.
⚠ "Access denied for user" during migrate
This is almost always one of: (1) wrong password in .env, (2) a stale cached config still holding old DB values (fix: php artisan config:clear and delete bootstrap/cache/config.php if present), or (3) a localhost vs 127.0.0.1 host mismatch (see Part 7). Test the exact credentials manually first: mysql -u user -p -h 127.0.0.1 dbname — if that works but Artisan still fails, it's a caching issue, not credentials.
14. SSL via Let's Encrypt
Apache
sudo apt install certbot python3-certbot-apache -y
sudo certbot --apache -d yourdomain.com -d www.yourdomain.com
Nginx
sudo apt install certbot python3-certbot-nginx -y
sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com
Choose "redirect" when prompted, so HTTP automatically forwards to HTTPS. Auto-renewal is configured automatically — verify with:
sudo certbot renew --dry-run
15. Production Optimization
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan event:cache
⚠ config:cache freezes your .env values
Once cached, editing .env has no effect until you re-run php artisan config:cache (or at least config:clear). This is the single most common "why didn't my change take effect" bug in Laravel production deployments.
Queue workers (Supervisor)
sudo apt install supervisor -y
sudo nano /etc/supervisor/conf.d/laravel-worker.conf
[program:laravel-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/yourdomain.com/artisan queue:work --sleep=3 --tries=3
autostart=true
autorestart=true
user=deployer
numprocs=1
redirect_stderr=true
stdout_logfile=/var/www/yourdomain.com/storage/logs/worker.log
sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl start laravel-worker:*
Scheduler (cron)
crontab -e -u deployer
* * * * * cd /var/www/yourdomain.com && php artisan schedule:run >> /dev/null 2>&1
16. Verification Checklist
17. Backup & Restore Procedures
Database backup
mysqldump -u laravel_user -p laravel_app > backup_$(date +%F).sql
Database restore
mysql -u laravel_user -p laravel_app < backup_2026-07-29.sql
Project files backup
tar -czvf laravel_backup_$(date +%F).tar.gz /var/www/yourdomain.com
Automate daily backups via cron
0 2 * * * mysqldump -u laravel_user -pYourPassword laravel_app > /home/deployer/backups/db_$(date +\%F).sql
Important: push backups off-server too (S3, Google Drive, etc.). A backup stored only on the same VPS offers no protection if that VPS itself fails or is compromised.
18. Safe Redeployment Workflow (Future Updates)
On your local machine
git add .
git commit -m "describe your change"
git push origin main
On the VPS server
cd /var/www/yourdomain.com
php artisan down # maintenance mode
git pull origin main
composer install --optimize-autoloader --no-dev
npm install && npm run build # if frontend assets changed
php artisan migrate --force
php artisan config:clear
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan queue:restart # workers must reload new code
php artisan up
⚠ Don't forget queue:restart
Queue workers cache old code in memory — new deployments silently keep running stale logic until workers are restarted.
Sanity check after deploy: curl -I https://yourdomain.com should return 200 OK.
19. Logs & Monitoring
| What | Where |
|---|---|
| Laravel app errors | storage/logs/laravel.log |
| Apache errors | /var/log/apache2/yourdomain_error.log |
| Apache access | /var/log/apache2/yourdomain_access.log |
| Nginx errors | /var/log/nginx/error.log |
| MySQL errors | /var/log/mysql/error.log |
| System-wide | journalctl -xe |
Get the actual error (not just the stack trace tail):
grep "production.ERROR" storage/logs/laravel.log | tail -n 1
tail -f storage/logs/laravel.log # live tail while debugging
Terminal tip: avoid pasting large multi-line log output back into your live terminal prompt — bash may try to interpret parts of it as commands, causing confusing syntax errors. Copy log output to a text editor or just read it in place with tail/less.
20. Database Administration: phpMyAdmin & Adminer
Option A — phpMyAdmin
sudo apt install phpmyadmin -y # select apache2 when prompted
Reachable at https://yourdomain.com/phpmyadmin if auto-linked, otherwise:
sudo ln -s /usr/share/phpmyadmin /var/www/yourdomain.com/public/phpmyadmin
Option B — Adminer (lighter, recommended)
Step 1 — Create a dedicated directory (outside Laravel's public/)
sudo mkdir -p /var/www/adminer
cd /var/www/adminer
Keeping it outside Laravel's public/ means Laravel's own .htaccess rewrite rules never interfere with it, and it can be locked down independently of your app.
Step 2 — Download Adminer (single-file install)
sudo wget "https://www.adminer.org/latest.php" -O index.php
That's the entire installation — Adminer ships as one PHP file with no dependencies or build step.
Step 3 — Set correct ownership & permissions
sudo chown -R www-data:www-data /var/www/adminer
sudo chmod -R 750 /var/www/adminer
Step 4a — Apache: config with a non-obvious path + Basic Auth
sudo nano /etc/apache2/sites-available/adminer.conf
Alias /db-manage-x7k9 /var/www/adminer
<Directory /var/www/adminer>
Options -Indexes
AllowOverride None
# Optional: restrict to a static home/office IP (skip if your IP is dynamic)
# Require ip 103.45.212.87
AuthType Basic
AuthName "Restricted Database Access"
AuthUserFile /etc/apache2/.adminer_htpasswd
Require valid-user
</Directory>
sudo apt install apache2-utils -y
sudo htpasswd -c /etc/apache2/.adminer_htpasswd youradminusername
sudo a2ensite adminer.conf
sudo apache2ctl configtest
sudo systemctl reload apache2
Step 4b — Nginx equivalent
sudo apt install apache2-utils -y # htpasswd tool works fine even on Nginx servers
sudo htpasswd -c /etc/nginx/.adminer_htpasswd youradminusername
sudo nano /etc/nginx/sites-available/adminer
server {
listen 80;
server_name yourdomain.com;
location /db-manage-x7k9 {
alias /var/www/adminer;
index index.php;
auth_basic "Restricted Database Access";
auth_basic_user_file /etc/nginx/.adminer_htpasswd;
# Optional static-IP restriction:
# allow 103.45.212.87;
# deny all;
location ~ \.php$ {
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
fastcgi_param SCRIPT_FILENAME $request_filename;
include fastcgi_params;
}
}
}
sudo ln -s /etc/nginx/sites-available/adminer /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
Step 5 — Finding your IP for the optional restriction
From your own computer (not the VPS), visit:
https://whatismyip.com
https://ifconfig.me
⚠ Dynamic IPs
Most home/mobile connections (especially 4G/5G hotspots) get a new IP periodically. If you lock Adminer to today's IP and it's dynamic, you risk locking yourself out within days. If unsure, skip the IP restriction and rely on Basic Auth + a strong Adminer password instead — enable the site only when actively using it.
Step 6 — Force HTTPS for the Adminer path (after SSL is set up, Part 14)
# Apache — add above your main VirtualHost's :80 block
<VirtualHost *:80>
ServerName yourdomain.com
Redirect permanent /db-manage-x7k9 https://yourdomain.com/db-manage-x7k9
</VirtualHost>
Certbot (Part 14) will usually offer to redirect all HTTP → HTTPS automatically for the whole domain, which covers this too.
Step 7 — Logging in
Visit https://yourdomain.com/db-manage-x7k9. Basic Auth prompt appears first (browser popup), then Adminer's own login form:
| System | MySQL |
| Server | localhost (match your MySQL user's granted host — see Part 7) |
| Username / Password | your scoped DB user (never MySQL root) — same values as .env |
| Database | optional — leave blank to browse all DBs the user can access |
⚠ "Access denied" on Adminer login
Double-check the exact username — it's easy to accidentally type a variant (e.g. adding a suffix) that was never actually created in MySQL. Confirm with: SELECT User, Host FROM mysql.user WHERE User LIKE 'your_user%'; and use the exact match shown.
Step 8 — Using Adminer day-to-day
- Browse/edit data — click a table name, use the "Select" and "Edit" tabs
- Export a database — select DB → "Export" tab → choose format → Export
- Import a database — select DB → "Import" tab → choose file → Execute
- Run raw SQL — "SQL command" tab, useful for quick fixes or one-off queries
Step 9 — Clean white theme (optional)
cd /var/www/adminer
sudo wget "https://raw.githubusercontent.com/vrana/adminer/master/designs/pepa-linha/adminer.css" -O adminer.css
sudo chown www-data:www-data adminer.css
sudo chmod 640 adminer.css
Adminer auto-detects and applies adminer.css in the same folder for both the login screen and the app UI — no config changes needed. Hard refresh (Ctrl+Shift+R) to see it. Swap pepa-linha for nicu in the URL to try an alternative clean theme.
Step 10 — Updating Adminer later
cd /var/www/adminer
sudo wget "https://www.adminer.org/latest.php" -O index.php
sudo chown www-data:www-data index.php
Security best practices for any web DB tool
- Never expose it without at least one access layer (Basic Auth, and IP restriction if you have a static IP)
- Always use HTTPS — never send DB passwords over plain HTTP
- Use a random, non-guessable path instead of the obvious
/admineror/phpmyadmin - Log in only with a scoped app-level user, never MySQL root
- Rotate the Basic Auth password periodically and don't reuse it elsewhere
- Disable the site when not actively using it:
# Apache sudo a2dissite adminer.conf && sudo systemctl reload apache2 # Nginx sudo rm /etc/nginx/sites-enabled/adminer && sudo systemctl reload nginx - Most secure option: skip exposing any DB UI publicly — connect via an SSH tunnel from a desktop client (TablePlus, DBeaver) instead, keeping zero public-facing DB admin surface
21. Troubleshooting Common Errors
GoDaddy parking page still shows after correct DNS
Check Domain Settings → Forwarding in GoDaddy and remove any forwarding rule — it overrides DNS at the HTTP level.
ERR_CONNECTION_TIMED_OUT on your domain/IP
Web server not installed/running (sudo systemctl status apache2), or blocked by UFW / Hostinger's panel-level firewall.
SSH "Permission denied, please try again"
Wrong password (nothing displays while typing — that's normal), or too many failed attempts triggering a temporary IP block. Reset the password via hPanel if unsure.
"Access denied for user" on migrate/cache commands
Check password match, run php artisan config:clear, verify localhost vs 127.0.0.1 grants match .env's DB_HOST.
500 error: "Vite manifest not found"
Frontend assets were never built. Run npm install && npm run build.
Homepage works, every other route is a raw Apache "Not Found"
mod_rewrite not enabled, or AllowOverride All missing/mismatched in the VirtualHost's <Directory> block (double-check for typos in the path).
MySQL "syntax error" right after CREATE USER
Missing semicolon between statements, or a password containing ;/\ that MySQL misinterprets as a new command.
22. Production Best Practices Summary
- Never point DocumentRoot to the project root — always
public/, or your.envbecomes publicly downloadable. - Always set
APP_DEBUG=falsein production. - Use a dedicated, scoped MySQL user per application — never root.
- Keep MySQL bound to localhost; never expose port 3306 externally.
- Restart queue workers after every deploy that changes job/handler code.
- Keep the server patched:
sudo apt update && sudo apt upgrade -yregularly. - Consider
fail2banto auto-block brute-force SSH/login attempts. - Automate off-server backups (S3/Drive) — a same-server-only backup isn't real protection.
- Disable any exposed DB admin tool when not actively in use.
- Use Git for all deployments — it makes rollbacks and updates safe and repeatable.