Step‑by‑Step Guide to Configure Nginx as a Secure Reverse Proxy for Dockerized Apps on a Linux VPS
Step‑by‑Step Guide to Configure Nginx as a Secure Reverse Proxy for Dockerized Apps on a Linux VPS
Prerequisites: A fresh Linux VPS (Ubuntu 22.04 LTS recommended), root or sudo access, a domain name pointing to the server’s public IP, basic familiarity with SSH and Docker commands.
1. Provision the VPS and Update the System
Start by logging into your server via SSH. Run a full package update to ensure you’re working with the latest security patches.
ssh root@your.vps.ip
# Update package lists and upgrade existing packages
apt update && apt upgrade -y
# Install essential utilities
apt install -y curl wget gnupg2 ca-certificates lsb-release apt-transport-https
If you need a reliable environment for this tutorial, you can rely on DevNix Cloud VPS to provide a clean Ubuntu installation with predictable performance.
2. Install Docker Engine and Docker Compose
Docker will host the application containers, while Docker Compose simplifies multi‑container orchestration.
# Add Docker’s official GPG key
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | apt-key add -
# Set up the stable repository
add-apt-repository \
"deb [arch=$(dpkg --print-architecture)] https://download.docker.com/linux/ubuntu \
$(lsb_release -cs) stable"
# Install Docker Engine
apt update
apt install -y docker-ce docker-ce-cli containerd.io
# Verify Docker installation
docker run --rm hello-world
# Install Docker Compose (v2 plugin)
apt install -y docker-compose-plugin
# Verify Docker Compose
docker compose version
3. Create a Sample Dockerized Application
For demonstration, we’ll use a simple Node.js “Hello World” app. Create a project directory and a Dockerfile.
mkdir -p /opt/webapp
cd /opt/webapp
cat > app.js <<'EOF'
const http = require('http');
const port = 3000;
http.createServer((req, res) => {
res.writeHead(200, {'Content-Type': 'text/plain'});
res.end('Hello from Docker!\n');
}).listen(port);
EOF
cat > package.json <<'EOF'
{
"name": "hello-docker",
"version": "1.0.0",
"main": "app.js",
"scripts": {
"start": "node app.js"
},
"dependencies": {
"express": "^4.18.2"
}
}
EOF
cat > Dockerfile <<'EOF'
FROM node:20-alpine
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 3000
CMD ["npm", "start"]
EOF
cat > docker-compose.yml <<'EOF'
version: "3.9"
services:
web:
build: .
restart: unless-stopped
ports:
- "3000:3000"
EOF
Build and launch the container:
docker compose up -d --build
# Verify it’s running
docker ps
4. Install Nginx and Harden Its Configuration
Nginx will accept traffic on ports 80/443, forward HTTP requests to the Docker container, and terminate TLS.
apt install -y nginx
# Remove the default site to avoid conflicts
rm /etc/nginx/sites-enabled/default
Create a new server block for your domain:
cat > /etc/nginx/sites-available/example.com <<'EOF'
server {
listen 80;
server_name example.com www.example.com;
# Redirect all HTTP to HTTPS
return 301 https://$host$request_uri;
}
EOF
ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/
nginx -t && systemctl reload nginx
5. Obtain Free TLS Certificates with Let’s Encrypt
We’ll use Certbot’s Nginx plugin, which automatically updates the Nginx configuration.
apt install -y certbot python3-certbot-nginx
# Request a certificate (replace with your domain)
certbot --nginx -d example.com -d www.example.com --agree-tos --redirect --no-eff-email -m admin@example.com
Certbot creates a second server block that listens on port 443 and adds the SSL directives. Verify the configuration:
nginx -t && systemctl reload nginx
6. Wire Nginx to the Docker Container
Modify the SSL server block to proxy traffic to the internal Docker service. Open the file generated by Certbot (usually /etc/nginx/sites-available/example.com) and add a location block.
server {
listen 443 ssl http2;
server_name example.com www.example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
# Proxy all requests to the Docker container
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
Test the final configuration and reload Nginx:
nginx -t && systemctl reload nginx
7. Automate Certificate Renewal
Let’s Encrypt certificates are valid for 90 days. Certbot installs a systemd timer that runs twice daily. Verify it’s active:
systemctl list-timers | grep certbot
Optionally, perform a dry‑run to confirm renewal works:
certbot renew --dry-run
8. Verify End‑to‑End Functionality
Open a browser and navigate to https://example.com. You should see “Hello from Docker!”. Use curl to confirm the SSL handshake:
curl -I https://example.com
# Expected HTTP/2 200 OK response
If you encounter “502 Bad Gateway”, check that the Docker container is listening on the expected port and that Nginx’s proxy_pass address matches 127.0.0.1:3000.
9. Optional: Enable HTTP/2 and Security Headers
Adding a few headers improves security and performance.
add_header X-Content-Type-Options nosniff;
add_header X-Frame-Options SAMEORIGIN;
add_header X-XSS-Protection "1; mode=block";
add_header Referrer-Policy "no-referrer-when-downgrade";
Place these directives inside the SSL server block, reload Nginx, and re‑test.
Conclusion
By following this guide you have transformed a plain Docker container into a production‑ready service behind a hardened Nginx reverse proxy with automatic TLS termination. The setup scales easily: add more services to docker-compose.yml, expose them on distinct internal ports, and extend Nginx with additional location blocks. Regularly monitor Docker health and keep your VPS patched to maintain a secure, high‑availability environment.