DEV Community

ZèD
ZèD

Posted on • Edited on • Originally published at imzihad21.github.io

Setting Up a VPS Server with Docker, Nginx Proxy Manager, and Portainer

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
Enter fullscreen mode Exit fullscreen mode

Configure Docker for non-root usage:

sudo groupadd docker || true
sudo usermod -aG docker $USER
docker --version
Enter fullscreen mode Exit fullscreen mode

2. Create Shared Docker Network

Create one external network for reverse-proxied services.

docker network create nginx_proxy
Enter fullscreen mode Exit fullscreen mode

3. Deploy Nginx Proxy Manager

Create NPM stack and run it with Docker Compose.

mkdir -p ~/managers/nginx
cd ~/managers/nginx
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode
docker compose up -d
Enter fullscreen mode Exit fullscreen mode

Default login URL:

http://<your-vps-ip>:81
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode
docker compose up -d
Enter fullscreen mode Exit fullscreen mode

Portainer initial URL:

http://<your-vps-ip>:9000
Enter fullscreen mode Exit fullscreen mode

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_proxy network.
  • Deploy NPM and Portainer as separate stacks.
  • Configure domain-based SSL reverse proxy entries.
  • Restrict insecure management access after setup.

Next Steps

  1. Add UFW hardening rules and fail2ban.
  2. Add automated backup for NPM and Portainer volumes.
  3. Add monitoring stack for server and container health.
  4. Add CI/CD deployment pipeline for app containers.

Top comments (2)

Collapse
 
neopium profile image
Ben

Thanks for this great post, it really helps!
A few comments/questions though :

1. Disabling ports 80 and 81

Optional: After this, you can disable port 80 and 81 which uses http so that only https request using 443 ports can pass through securely. This is mainly for restricting unsecured ports.

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?

Collapse
 
imzihad21 profile image
ZèD

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.