Back to blog
DevOps·March 11, 2026·Sayan Roor

Complete Guide: Deploy a Web App with Docker, Nginx & SSL to a Production Server

Step-by-step guide to deploying a web application on a VPS: Docker Compose, Nginx reverse proxy, Let's Encrypt SSL certificates, PostgreSQL, Redis, MinIO, monitoring, backups, and CI/CD.

DockerNginxDevOpsDeploymentServer
Deploy a web app with Docker and Nginx to a server in 2026

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

  1. Server Requirements
  2. Project Architecture
  3. Server Preparation
  4. Installing Docker and Nginx
  5. Docker Compose: Service Configuration
  6. Nginx as a Reverse Proxy
  7. SSL Certificates with Let's Encrypt
  8. PostgreSQL, Redis, and MinIO in Docker
  9. Environment Variables and Secrets
  10. Firewall Configuration
  11. Monitoring (Prometheus + Grafana)
  12. Automated Backups
  13. CI/CD with GitHub Actions
  14. Troubleshooting
  15. Production Checklist

1. Server Requirements

Before you begin, make sure you have a VPS with sufficient resources.

ParameterMinimumRecommended
CPU2 vCPU4 vCPU
RAM4 GB8 GB
Disk40 GB SSD80+ GB SSD
OSUbuntu 22.04 LTSUbuntu 24.04 LTS
Network100 Mbps1 Gbps

Where to rent a server:

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 files
  • git — version control
  • htop — system monitoring
  • ufw — firewall
  • fail2ban — brute-force protection

3.4. Create a Deploy User

Running as root is 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:

TypeNameValueTTL
A@YOUR_SERVER_IP3600
AwwwYOUR_SERVER_IP3600
AapiYOUR_SERVER_IP3600

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 .env files 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

MetricDescriptionAlert Threshold
Server CPUProcessor usage> 80%
RAMMemory usage> 85%
DiskDisk utilization> 90%
PostgreSQLActive connections> 80
RedisMemory usage> 200 MB
APIResponse 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 NameValue
DEPLOY_HOSTServer IP address
DEPLOY_USERdeploy
DEPLOY_SSH_KEYPrivate key

13.3. GitHub Actions Workflow

Create .github/workflows/deploy.yml:

Now, every push to main will 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
  • .env files 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:

  1. Server — Secured with a dedicated deploy user
  2. Docker Compose — All services isolated and reproducible
  3. Nginx — Reverse proxy with static file caching
  4. SSL — Free Let's Encrypt certificates with auto-renewal
  5. Firewall — Only necessary ports open
  6. Backups — Automated, daily, with rotation
  7. CI/CD — Automatic deployment on push to main
  8. Monitoring — Metrics and alerts via Grafana

This architecture can handle up to 10,000 RPS with proper caching and horizontal container scaling. Happy deploying!

Sayan Roor

Full‑stack developer. I build Next.js & TypeScript web apps with focus on performance and conversion.