Self-Host Next.js with Docker and Nginx
Vercel’s great for getting a Next.js app online fast, but if you’re looking to cut costs or take full control of your setup, self-hosting is the way to go. In this guide, I’ll walk you through spinning up a production-ready Next.js app on a VPS using Docker, and Nginx. In addition, I'll show you how to configure and automate Cloudflare SSL, and configure VPS Firewall settings.
Table of Contents
- Dockerize Next.js app locally
- Ubuntu Server Configuration
- Deploy Next.js app to VPS
- Configure and Automate HTTPS + SSL
- Configure VPS Firewall
Overview
Here’s the stack we’ll be working with:
- Docker – Keeps the app environment consistent across local and production.
- Nginx – Acts as a reverse proxy and handles SSL termination.
- Cloud provider of your choice – I’m using Linode, but you can roll with whatever VPS you prefer.
- Cloudflare – Speeds things up globally by caching static assets and protecting your origin.
- PuTTY and WinSCP – Tools for connecting to your server over SSH and transferring files from Windows.
1. Dockerize your Next.js app locally
First up, let’s dockerize the Next.js app. This gives us a clean, consistent environment we can run anywhere, local, VPS, whatever. Install Docker desktop, if you don't already have it, Docker Desktop for Windows.
Start by tweaking your next.config.mjs to output a standalone build. That keeps the production image lightweight:
Next, drop a Dockerfile in the root of your project and add this:
Create a .dockerignore file to keep unnecessary files out of the Docker build:
At this point, you should be able to build and run the app with Docker:
Try it out locally:
Head over to http://localhost:3000, you should see your Next.js root page.
Now let’s get it live so the rest of the world can see it too.
2. Ubuntu Server Configuration
This guide assumes you already have an Ubuntu server with your cloud provider of choice.
SSH into the server
Update your package list:
Then install Docker:
Docker should be good to go now. Run docker --version to double-check it’s installed.
Enable the firewall:
Make sure your cloud provider’s firewall is set to allow incoming traffic on ports 22, 80, 443, and 3000.
Let's not forget to update the server as well:
3. Deploy Next.js app to VPS
Install WinSCP if you don't already have it, this will help visualize files, directories, and deploy to our server.
In WinSCP log into your server. Create /website/ directory under /. Then drag and drop Next.js project files:
(Disclaimer) screenshot for demonstration, please don't copy unnecessary files over
Now test building your Next.js docker container on your VPS from the /website/ directory.
You should now be able to see your Next.js site at http://your-ip-addrress:3000.
Nginx Configuration and Testing
Nginx sits in front of your app, handling traffic on ports 80 and 443, and passing it to port 3000 where your site actually lives.
Begin with installing and running Nginx.
Nginx could honestly be its own course, but let’s keep it simple. Drop the config below into /etc/nginx/sites-available/default and carry on like a true script kiddie.
Check it and reload the config.
You should be able to access http://your-ip-address now without specifying the 3000 port.
if not, try symlinking /etc/nginx/sites-available/default & /etc/nginx/sites-enabled/default
Verify your Next.js site is live at http://your-ip-address, before proceeding to the next step.
4. Configure and Automate HTTPS + SSL
This guide assumes you're using Cloudflare.
Configure DNS Records
Ensure each domain has these two A records, specifying www & your-domain.com, with your origin server IP address. Ensure proxy status is enabled.
(Informational) Cloudflare Dashboard > DNS > Records
Enable SSL/TLS
In Cloudflare, head to the SSL/TLS > Overview settings and set your domain to Automatic or Flexible.
Hit https://your-domain.com to confirm HTTPS is live.
You could stop here—Cloudflare handles HTTPS for users, even if traffic to your origin stays on port 80.
But come on, grabbing a Let's Encrypt cert with autorenewal isn’t that deep. Let’s lock it down end-to-end.
(Optional) Obtain SSL certificates from Let's Encrypt
To obtain Let's Encrypt certs, we'll be using Certbot. Before we begin, test if snap is installed.
Ubuntu usually comes with snap base installed, but not always. Run the following if in need.
Remove Certbot packages from other package managers to ensure certbot runs from snap. Given this tutorial, you're likely using apt.
Install Certbot.
Prepare (symlink) Certbot.
Use Certbot to receive a Let's Encrypt certificate, edit our nginx configuration, and handle auto renewal for your domain(s).
Test automatic renewal.
The command to renew certbot is installed in one of the following locations:
- /etc/crontab/
- /etc/cron.*/*
- systemctl list-timers
Nice! You’ve got your Let’s Encrypt cert, and it’s all set to auto-renew. Now let’s make sure Nginx is locked in and ready to handle HTTPS traffic from Cloudflare on port 443.
(Optional) HTTPS Nginx Configuration
Edit /etc/nginx/sites-available/default.
- Add 2 server blocks per domain. One for listening on 80, one for 443.
- Add your-domain1.com next to server_name including www.
- Add the Let's Encrypt certificates you generated previously.
- ssl_certificate /etc/letsencrypt/live/your-domain1.com/fullchain.pem;
- ssl_certificate_key /etc/letsencrypt/live/your-domain1.com/privkey.pem;
- Add protocols and ciphers.
- ssl_protocols TLSv1.2 TLSv1.3;
- ssl_ciphers HIGH:!aNULL:!MD5;
TLDR Considerations:
- If you’re running multiple domains, give each one its own server block, sharing can get messy fast.
- As for HTTP on port 80? I don’t bother. Any stray requests get hit with a clean 301 redirect to HTTPS, just in case something weird slips through.
Save file, check, and reload Nginx.
At last, we’re ready for Cloudflare Full (Strict) SSL/TLS. Cue the drum roll... Time to lock it down end-to-end like a boss.
(Optional) Enable Cloudflare Full (strict) SSL/TLS
Head over to the Cloudflare Dashboard, jump into SSL/TLS > Overview, and flip your domain to Full (Strict). Boom, you’ve now got end-to-end encryption. Hit https://your-domain.com to see it in action.
🎉 Congrats, you’re officially legit.
Want to peek at your Let’s Encrypt cert? Temporarily disable the orange cloud (Proxy status) under DNS > Records so you’re hitting your origin directly. Then visit your site, click the padlock in the browser, and you’ll see your cert details.
Just don’t forget to turn the proxy back on when you’re done — Cloudflare’s got your back.
5. Configure VPS Firewall
We’ve locked this site down with HTTPS, might as well show the firewall some love too. Every cloud provider’s a little different, so your mileage may vary, but the core idea stays the same.
First up, both the Default Inbound and Default Outbound policies are set to drop. That means nothing gets in or out unless we explicitly say so. Every protocol, port, source, and destination has to earn its keep.
Inbound Rules
- Allow SSH (TCP 22)
- Required for remote access via PuTTY or WinSCP.
- For tighter security, consider restricting access to a known static IP range.
- Always use SSH keys — password authentication is for amateurs.
- Allow HTTPS (TCP 443)
- This rule is scoped to only accept inbound traffic from Cloudflare’s IP ranges.
- This ensures your origin server only responds to trusted proxy requests.
- You can find the current list of Cloudflare IPs here.
Outbound Rules
- Allow HTTP (TCP 80)
- Required for fetching packages and updates via apt.
- Allow HTTPS (TCP 443)
- Enables secure communication when pulling certs, packages, or external resources.
- Allow DNS (TCP/UDP 53)
- Needed for domain resolution when installing or updating packages with apt.
- No DNS, no downloads — keep it open.
Conclusion
You’ve just stood up a secure, production-ready Next.js app with Docker, Nginx, and Let’s Encrypt, all self-hosted and protected by Cloudflare. Whether you're scaling a project or sharpening your devops chops, this setup gives you full control, speed, and flexibility.
References Used:
Self-Host Next.js with Kamal and GitHub Actions
Configuring HTTPS servers
Installing snap on Ubuntu | Snapcraft documentation
Publishing an ASP.NET Core website to a cheap Linux VM host - Scott Hanselman's Blog
Ubuntu | Docker Docs
Windows | Docker Docs
How To Install Nginx on Ubuntu 20.04 | DigitalOcean
Let's Encrypt
Certbot
IP Ranges | Cloudflare