Setting Up a VPS Server with Docker, Nginx Proxy Manager, and Portainer
Hosting apps on a VPS gets much easier when infrastructure is containerized from day one. With Docker for runtime, Nginx Proxy Manager for reverse proxy + SSL, and Portainer for container management, you get a clean and scalable baseline.
This guide uses a Debian-based server and walks through the full setup step by step.
Why It Matters
- Keeps deployment predictable with containers.
- Makes domain routing and SSL simpler with Nginx Proxy Manager.
- Gives operational visibility through Portainer UI.
- Builds a reusable foundation for multiple apps on one VPS.
Core Concepts
1. Install Docker on Debian VPS
Update packages and install Docker using official repository.
sudo apt update && sudo apt upgrade -y
for pkg in docker.io docker-doc docker-compose podman-docker containerd runc; do
sudo apt-get remove -y $pkg
done
sudo apt-get update
sudo apt-get install -y ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian $(. /etc/os-release && echo \"$VERSION_CODENAME\") stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
Configure Docker for non-root usage:
sudo groupadd docker || true
sudo usermod -aG docker $USER
docker --version
2. Create Shared Docker Network
Create one external network for reverse-proxied services.
docker network create nginx_proxy
3. Deploy Nginx Proxy Manager
Create NPM stack and run it with Docker Compose.
mkdir -p ~/managers/nginx
cd ~/managers/nginx
services:
nginx-proxy-manager:
container_name: nginx
image: jc21/nginx-proxy-manager:latest
restart: unless-stopped
ports:
- "80:80"
- "443:443"
- "81:81"
volumes:
- ./data:/data
- ./letsencrypt:/etc/letsencrypt
networks:
default:
name: nginx_proxy
external: true
docker compose up -d
Default login URL:
http://<your-vps-ip>:81
Default credentials:
- Email:
admin@example.com - Password:
changeme
Change these immediately after first login.
4. Deploy Portainer
Create Portainer stack:
mkdir -p ~/managers/portainer
cd ~/managers/portainer
services:
portainer:
container_name: portainer
image: portainer/portainer-ce:latest
restart: unless-stopped
ports:
- "9000:9000"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ./portainer_data:/data
networks:
default:
name: nginx_proxy
external: true
docker compose up -d
Portainer initial URL:
http://<your-vps-ip>:9000
5. Expose NPM via Domain and SSL
In NPM UI, create proxy host for NPM dashboard itself:
- Domain:
npm.example.com - Forward Hostname/IP:
nginx - Forward Port:
81 - Enable SSL + Force SSL + HTTP/2
- Request Let's Encrypt certificate
6. Expose Portainer via Domain and SSL
Create second proxy host for Portainer:
- Domain:
portainer.example.com - Forward Hostname/IP:
portainer - Forward Port:
9000 - Enable SSL + Force SSL + HTTP/2
After validation, you can close direct admin ports and keep only 443 exposed publicly.
Practical Example
After both proxy hosts are configured:
-
https://npm.example.com-> Nginx Proxy Manager UI -
https://portainer.example.com-> Portainer UI
Now new services can be added behind NPM in minutes, not in three-terminal-debug mode.
Common Mistakes
- Using Docker from distro packages instead of official repo.
- Keeping default NPM admin credentials.
- Exposing management ports publicly forever.
- Not using a shared Docker network for proxy routing.
- Skipping persistent volume mounts for NPM/Portainer data.
Quick Recap
- Install Docker from official source.
- Create shared
nginx_proxynetwork. - Deploy NPM and Portainer as separate stacks.
- Configure domain-based SSL reverse proxy entries.
- Restrict insecure management access after setup.
Next Steps
- Add UFW hardening rules and fail2ban.
- Add automated backup for NPM and Portainer volumes.
- Add monitoring stack for server and container health.
- Add CI/CD deployment pipeline for app containers.
Top comments (2)
Thanks for this great post, it really helps!
A few comments/questions though :
1. Disabling ports 80 and 81
How do you disable those ports?
2. Enabling SSL
You could be more accurate regarding this point: one needs to go to the SSL tab, select "Force SSL" and "HTTP/2 support".
3. Let's Encrypt certificate
In the version of NPM I use, I'm not asked for my email adress.
Is there a way to automatically renew the certificates generated this way?
Thanks for feedback!
1: Best way to do that is blocking them using VPS service provider's network interface or policy for inbound rules. Other than that, edit docker-compose.yml, comment out lines in port section for 80 and 81. That should stop services from listening to that port. Restart with docker compose down/up -d. For Let's Encrypt, use DNS challenge if closing 80.
2: Yes, will update that part, thanks.
3: Email asked when making new certs. NPM checks for auto-renew every 12 hours, renews 30 days early, automatically. Check logs if fails: docker logs nginx.