Complete Guide: Deploy a Web App with Docker, Nginx & SSL to a Production Server
A step-by-step guide to deploying your application on a VPS. Each step includes the command, explanation, and expected result. Works for Next.js, NestJS, and any other stack.
Table of Contents
- Server Requirements
- Project Architecture
- Server Preparation
- Installing Docker and Nginx
- Docker Compose: Service Configuration
- Nginx as a Reverse Proxy
- SSL Certificates with Let's Encrypt
- PostgreSQL, Redis, and MinIO in Docker
- Environment Variables and Secrets
- Firewall Configuration
- Monitoring (Prometheus + Grafana)
- Automated Backups
- CI/CD with GitHub Actions
- Troubleshooting
- Production Checklist
1. Server Requirements
Before you begin, make sure you have a VPS with sufficient resources.
| Parameter | Minimum | Recommended |
|---|---|---|
| CPU | 2 vCPU | 4 vCPU |
| RAM | 4 GB | 8 GB |
| Disk | 40 GB SSD | 80+ GB SSD |
| OS | Ubuntu 22.04 LTS | Ubuntu 24.04 LTS |
| Network | 100 Mbps | 1 Gbps |
Where to rent a server:
- Hetzner — Europe, from $4/mo
- DigitalOcean — from $6/mo
- Linode/Akamai — from $5/mo
- AWS Lightsail — from $5/mo
You will also need:
- A domain name (e.g.,
your-domain.com) with DNS access - An SSH client (built into macOS/Linux; on Windows use PuTTY or Windows Terminal)
2. Project Architecture
Before starting the deployment, it is important to understand how the components interact:
User (Browser)
|
v
+-------------------+
| Nginx (:80/443) | <-- Accepts all requests, routes them
+----+----------+---+
| |
v v
+----------+ +----------+
| Web | | API |
| (Next.js)| | (NestJS) | <-- Apps in Docker containers
| :3000 | | :3001 |
+----------+ +--+-+-+---+
| | |
+---------+ | +--------+
v v v
+----------+ +----------+ +---------+
|PostgreSQL| | Redis | | MinIO |
| (DB) | | (cache) | | (files) |
| :5432 | | :6379 | | :9000 |
+----------+ +----------+ +---------+
Each component's role:
- Nginx — Receives internet requests, terminates SSL, proxies to containers
- Web — Frontend (SSR/SSG), what the user sees
- API — Server-side logic (auth, data, file uploads)
- PostgreSQL — Primary database
- Redis — Caching and queues
- MinIO — S3-compatible file storage
3. Server Preparation
3.1. Connect to the Server
3.2. Update the System
Downloads and installs all security updates. Takes 1-3 minutes.
3.3. Install Essential Utilities
What we installed:
curl,wget— downloading filesgit— version controlhtop— system monitoringufw— firewallfail2ban— brute-force protection
3.4. Create a Deploy User
Running as
rootis dangerous — a single mistake can break the entire server. Create a separate user.
3.5. Set Up SSH Key for the New User
3.6. Harden SSH
Open the SSH configuration:
Change the following lines:
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
MaxAuthTries 3
Restart SSH:
Important: Before closing your current session, open a new terminal and verify you can connect:
ssh deploy@YOUR_SERVER_IP
3.7. Switch to the Deploy User
From this point on, run all commands as deploy:
4. Installing Docker and Nginx
4.1. Docker
Verify:
Expected output:
Docker version 27.x.x, build ...
Docker Compose version v2.x.x
4.2. Nginx
Verify:
You should see Active: active (running).
4.3. Certbot (for SSL)
5. Docker Compose: Service Configuration
Create a docker-compose.prod.yml file in your project root. This is the main file describing all services.
Note: All infrastructure ports are bound to
127.0.0.1— they are not accessible from the internet directly, only through Nginx.
6. Nginx as a Reverse Proxy
Nginx accepts requests from the internet and routes them to Docker containers. Without Nginx, the site won't be accessible via your domain.
6.1. Main Site Configuration
Important: Create configs with HTTP only (port 80). Certbot will add SSL blocks automatically.
6.2. API Configuration
6.3. Activate the Configs
6.4. Test and Reload
Expected output:
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
7. SSL Certificates with Let's Encrypt
SSL encrypts the connection between the user and the server. Without it, browsers display an "Insecure Site" warning. Let's Encrypt provides free certificates.
7.1. Configure DNS
Before obtaining certificates, ensure your DNS records are set up:
| Type | Name | Value | TTL |
|---|---|---|---|
| A | @ | YOUR_SERVER_IP | 3600 |
| A | www | YOUR_SERVER_IP | 3600 |
| A | api | YOUR_SERVER_IP | 3600 |
Verify:
7.2. Obtain Certificates
Certbot will ask for your email and offer to redirect HTTP to HTTPS — choose "Redirect".
7.3. Verify Auto-Renewal
Expected output: Congratulations, all simulated renewals succeeded
Certificates automatically renew every 60 days. No additional action is needed.
8. PostgreSQL, Redis, and MinIO in Docker
8.1. Start Infrastructure Services
Start the base services your application depends on:
Wait 15 seconds for database initialization:
8.2. Verify
All 3 services should be Up (healthy).
8.3. Set Up MinIO
Install MinIO Client:
Connect and create a bucket:
8.4. Build and Start the Application
8.5. Initialize the Database
Expected output: Your database is now in sync with your Prisma schema.
9. Environment Variables and Secrets
9.1. Generate Secrets
Every password and secret must be unique. Never use passwords from examples!
9.2. The .env File
Create a .env file alongside docker-compose.prod.yml:
Security: Never commit
.envfiles to Git. Add them to.gitignore.
10. Firewall Configuration
The firewall blocks all ports except those needed. Without it, anyone can connect to your database directly!
Verify:
Expected output:
Status: active
To Action From
-- ------ ----
22/tcp ALLOW IN Anywhere
80/tcp ALLOW IN Anywhere
443/tcp ALLOW IN Anywhere
Ports 3000, 3001, 5432, 6379, 9000 are NOT exposed — they are only accessible through Nginx.
Configure fail2ban
After 3 failed login attempts, the IP address is blocked for 1 hour.
11. Monitoring (Prometheus + Grafana)
Monitoring is optional but highly recommended for production environments.
11.1. Add Monitoring Services to Docker Compose
Add these services to your docker-compose.prod.yml:
11.2. Start Monitoring
11.3. What to Monitor
| Metric | Description | Alert Threshold |
|---|---|---|
| Server CPU | Processor usage | > 80% |
| RAM | Memory usage | > 85% |
| Disk | Disk utilization | > 90% |
| PostgreSQL | Active connections | > 80 |
| Redis | Memory usage | > 200 MB |
| API | Response time, 5xx errors | > 2s / > 1% |
12. Automated Backups
Rule: If you haven't tested a restore, your backup doesn't exist.
12.1. Backup Script
12.2. Schedule with Cron
Add this line:
0 3 * * * /home/deploy/backup.sh >> /home/deploy/backups/backup.log 2>&1
The backup will run every day at 3:00 AM.
12.3. Restore from Backup
13. CI/CD with GitHub Actions
13.1. Prepare an SSH Key
On the server:
Copy the private key.
13.2. GitHub Secrets
Go to: Settings > Secrets and variables > Actions > New repository secret
| Secret Name | Value |
|---|---|
DEPLOY_HOST | Server IP address |
DEPLOY_USER | deploy |
DEPLOY_SSH_KEY | Private key |
13.3. GitHub Actions Workflow
Create .github/workflows/deploy.yml:
Now, every push to
mainwill automatically deploy the application.
14. Troubleshooting
502 Bad Gateway
Nginx is running but the application is down.
Connection Refused
Nginx is not running.
Out of Memory During Build
Docker builds require ~2 GB RAM. Add swap:
Container Keeps Restarting
Common causes:
- Incorrect variables in
.env - Database unreachable
- Port already in use
Running Out of Disk Space
Useful Diagnostic Commands
15. Production Checklist
Security
- SSH: root login disabled, key-only auth
- UFW: only ports 22, 80, 443 open
- fail2ban configured and running
- SSL certificates installed and auto-renewing
- JWT secrets are unique
- Database password is generated
- MinIO password is generated
-
.envfiles not in Git
Infrastructure
- PostgreSQL running (healthcheck green)
- Redis running (healthcheck green)
- MinIO running, bucket created
- Nginx configured as reverse proxy
- Infrastructure ports bound to 127.0.0.1
Application
- Site loads over HTTPS
- API responds to health endpoint
- Authentication works
- File uploads work
- CORS configured for the correct domain
Maintenance
- Cron backup configured (daily)
- Test restore completed
- CI/CD workflow functional
- Monitoring set up (optional)
- Alerts on critical metrics
Conclusion
You have deployed a full production environment:
- Server — Secured with a dedicated deploy user
- Docker Compose — All services isolated and reproducible
- Nginx — Reverse proxy with static file caching
- SSL — Free Let's Encrypt certificates with auto-renewal
- Firewall — Only necessary ports open
- Backups — Automated, daily, with rotation
- CI/CD — Automatic deployment on push to main
- Monitoring — Metrics and alerts via Grafana
This architecture can handle up to 10,000 RPS with proper caching and horizontal container scaling. Happy deploying!
