Deploying Next.js and Laravel on one DigitalOcean droplet
A straightforward production layout for a Next.js front end with a Laravel API behind Nginx, plus the verification steps that catch a bad deploy before users do.
Plenty of products run happily on a single well-configured server for a long time. This guide describes a simple layout for a Next.js front end and a Laravel API on one DigitalOcean droplet, and, more importantly, how to confirm each deploy actually worked.
The layout
Internet
│
Nginx (TLS, ports 80 and 443)
├── example.com → Next.js (Node process on 127.0.0.1:3000)
└── api.example.com → Laravel (PHP-FPM socket)
│
MySQL (127.0.0.1 only)
Only Nginx listens on a public port. The Node process and the database are bound to localhost, so they cannot be reached from outside even if a firewall rule is wrong.
Server basics
- Create the droplet and add your SSH key; disable password logins.
- Create a non-root user for deployments and run the apps as that user.
- Enable a firewall that only allows SSH, HTTP and HTTPS.
- Install Nginx, PHP-FPM, MySQL, Node.js and Composer.
- Point both DNS names at the droplet and issue certificates with Let's Encrypt (
certbot --nginx).
Nginx for the API
server {
server_name api.example.com;
root /var/www/api/current/public;
index index.php;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
}
location ~ /\.(?!well-known).* { deny all; }
}
The last block matters: it stops anyone fetching .env or .git if the document root is ever misconfigured.
Nginx for the Next.js app
server {
server_name example.com;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
Run the app with a process manager (systemd or PM2) so it restarts after a crash or a reboot, and never run next dev in production.
A deployment that can be rolled back
Deploy each release into its own timestamped directory and switch a current symlink once everything is ready. Rolling back is then one symlink change.
/var/www/api/
releases/20261028-1530/
releases/20261027-0910/
shared/.env, shared/storage/
current -> releases/20261028-1530
For the Laravel release, the sequence is: install dependencies, link the shared .env and storage, run migrations, warm the caches, switch the symlink, reload PHP-FPM and restart queue workers.
composer install --no-dev --optimize-autoloader
php artisan migrate --force
php artisan config:cache && php artisan route:cache && php artisan view:cache
ln -sfn "$RELEASE" /var/www/api/current
sudo systemctl reload php8.3-fpm
php artisan queue:restart
Run migrate with care. Prefer additive migrations (new columns and tables) so the old release keeps working against the new schema if you need to roll back.
For the Next.js release: install dependencies, run next build with the production environment variables, then restart the Node service.
Secrets and configuration
Keep .env files outside the release directory, owned by the deploy user and unreadable by others. Production values should never be committed. After changing .env, run php artisan config:cache again; a cached config ignores later edits to the file, which is a classic cause of "it works locally".
Verify every deploy
A deploy is not finished when the commands exit. Confirm it:
- Health endpoint. Expose a cheap
/api/healththat checks the database connection and returns 200. Hit it right after deploy. - Smoke test the money path. Load the home page, sign in, and run one critical flow, such as placing a test order.
- Read the logs. Watch the Nginx error log, PHP-FPM log and
storage/logsfor a minute after the switch. - Check the queue. Make sure workers restarted and jobs are being processed.
- Look at the version. Show the release id or commit hash somewhere (this site's footer does) so you can tell which build is live.
If any check fails, switch the symlink back and restart. Investigate afterwards, not during an outage.
What to add as you grow
A single droplet is a fine place to start, but plan the next steps: automated backups of the database, uptime monitoring with alerts, a CI pipeline that runs tests and builds before anything reaches the server, and eventually a separate database host. None of those change the layout above, which is why it is a good one to begin with.