From choosing the right internet connection to configuring the firewall, running a Docker host with a reverse proxy, and hardening every layer — here's the complete stack for hosting your own production websites from your own premises.
For self-hosting you need two things: a static public IP (so DNS can point at you) and unblocked ports 80 and 443 (most home broadband blocks them). Here's how the common options compare in India.
| Connection type | Speed (down/up) | Static IP | 80/443 open | SLA | Monthly cost (INR) | Best for |
|---|---|---|---|---|---|---|
| Home Broadband (retail) | 100-500 / shared | ✕ Not usually | ✕ Often blocked | None | ₹700-1,500 | Not viable for hosting |
| Home Broadband + Static IP add-on | 100-1000 / shared | ✓ (add-on ~₹500) | ✓ Check ISP | None | ₹1,200-2,500 | Dev, low-traffic sites |
| Business Broadband (Jio, Airtel Biz) | 200-1000 / symmetric or 1:4 | ✓ Included | ✓ Open | 99% | ₹2,500-6,000 | SMB production |
| Fibre Leased Line 1:1 | 10-1000 / symmetric | ✓ Multiple /29 | ✓ Open | 99.5-99.9% | ₹6,000-25,000 | Business critical |
| MPLS / dedicated ILL | 10-10,000 / symmetric | ✓ /29 or /28 | ✓ Open | 99.95% + finite | ₹25,000-2,00,000+ | Enterprise / multi-site |
The modem your ISP installs also acts as a router with its own NAT. If you leave it that way you get double NAT — pfSense can't see the public IP, can't issue Let's Encrypt certs, and port forwards get painful. Solution: put the modem in bridge mode so it passes the public session to pfSense.
Login to the ISP modem (usually 192.168.1.1 or 192.168.0.1). Admin password is on the sticker or provided at install. Write down the PPPoE username + password — you'll type it into pfSense.
Look for Operation Mode, WAN Setup or Connection Type. Change from Router mode / NAT → Bridge mode (sometimes called "transparent bridging" or "passthrough").
You don't want devices connecting around pfSense. Turn off both 2.4 GHz and 5 GHz radios. Disable DHCP while you're there.
After saving, the modem reboots. Your LAN loses internet until pfSense is configured in Step 3.
Bridge. Some firmwares hide the option — call support and ask them to enable "bridge mode" on their side.Plug the ISP modem's output into pfSense's WAN port. Then configure pfSense to authenticate to the ISP.
For most Indian fibre: PPPoE. For business broadband with static IP: Static IPv4. For ACT/managed-switch setups: DHCP.
Username + Password from the ISP. Leave Service name blank unless the ISP specified one.
At the bottom of the same page, tick Block private networks and Block bogon networks. Standard production hardening.
Go to Status › Interfaces. The WAN should show a public IP. Run curl ifconfig.me from a LAN client — it should return the same IP.
203.0.113.22): IPv4 Configuration Type = Static IPv4, IPv4 Address = your IP / subnet mask (usually /30 or /29), Upstream Gateway = the ISP's gateway IP. Add A record for your domain(s) → 203.0.113.22.
Because we'll run HAProxy on pfSense (Step 5), we forward 80 and 443 directly to the firewall's WAN IP — not to an internal server. HAProxy then handles the Host-header routing to the right backend.
Since HAProxy binds on pfSense directly, there's no separate backend host. Just the WAN firewall rule above.
Install two packages: haproxy-devel (modern HAProxy build) and acme (Let's Encrypt client). HAProxy handles HTTP(S) reverse-proxying with SNI-based host routing; ACME auto-issues + renews your certificates.
Install haproxy-devel and acme. Reboot not required.
Tick Enable HAProxy, set Max SSL Diffie-Hellman to 2048, save.
Add a new certificate for *.yourdomain.com using DNS-01 validation (your domain's API — Cloudflare, GoDaddy, Route53 all supported). With a wildcard you cover all subdomains with one cert.
Each backend points to your Docker host on a different container port. Example below.
Listen on WAN address :443 with SSL offloading using the wildcard cert. Under Access Control Lists add one rule per site matching the Host header. Under Actions route each matched ACL to its backend.
Separate frontend on WAN :80 with a single action: Redirect scheme to https code 301. So any plain-HTTP visitor jumps to HTTPS.
Click Apply. Hit https://site1.yourdomain.com — browser should show green lock + your site's content from the Docker host.
Name site1_backend Server list Name docker-host Address 192.168.10.20 Port 8001 # container's published port SSL (unchecked) # LAN traffic stays plain HTTP Verify SSL cert (unchecked) Check method HTTP HTTP check URI / HTTP check host site1.yourdomain.com
# Access Control Lists — Host header match ACL 1 Name: host_site1 Expression: Host matches: site1.yourdomain.com ACL 2 Name: host_site2 Expression: Host matches: site2.yourdomain.com ACL 3 Name: host_site3 Expression: Host matches: site3.yourdomain.com # Actions — route matched ACL to backend Action 1 Use backend: site1_backend Condition ACL: host_site1 Action 2 Use backend: site2_backend Condition ACL: host_site2 Action 3 Use backend: site3_backend Condition ACL: host_site3 # Default backend (for unmatched hosts) — a "not found" page Default backend default_404
Inside the LAN, a small Ubuntu server runs N containers — each serving one website on its own internal port. You can manage the containers by hand with Docker Compose, or install a free hosting manager that gives you a Heroku-style web UI over the same Docker engine. The host doesn't need a public IP; pfSense's HAProxy fronts everything.
Any recent PC / mini-PC / NUC with 16 GB RAM + 512 GB SSD is plenty for 20+ static or WordPress sites. Pick a box with at least one 1 GbE NIC.
Give it a static DHCP lease from pfSense (192.168.10.20). SSH in and run:
# System updates + base tools sudo apt update && sudo apt upgrade -y sudo apt install -y ca-certificates curl gnupg unattended-upgrades # Docker engine + Compose (official convenience script) curl -fsSL https://get.docker.com | sudo sh sudo usermod -aG docker $USER sudo systemctl enable --now docker # Sanity check docker --version docker compose version
Raw Docker Compose works, but becomes tedious once you cross 3–4 sites. A hosting manager wraps the same Docker engine with a web UI — click-to-deploy, Let's Encrypt automation, one-click restarts, and log viewers. All four below are fully free and self-hosted.
The modern Heroku / Netlify replacement. Dashboard deploys Git repos, Docker images, databases, and static sites. Built-in Traefik, automatic SSL, team invitations.
Long-running, well-documented. Web UI + CLI. Built on Docker Swarm, so scaling to multiple nodes is straightforward.
The original mini-Heroku. 100% CLI-driven, git-push-to-deploy. Perfect if you like terminals and want zero extra services running.
Not a deployer per se — it's a Docker management GUI. If you want to keep writing docker-compose.yml by hand but need a pane-of-glass to start/stop/log containers, this is it.
services: site1: image: nginx:1.27-alpine container_name: site1 restart: unless-stopped ports: - "8001:80" # HAProxy backend target volumes: - "./site1/html:/usr/share/nginx/html:ro" read_only: true tmpfs: - /var/cache/nginx - /var/run - /tmp security_opt: [no-new-privileges:true] cap_drop: [ALL] cap_add: [CHOWN, SETGID, SETUID, NET_BIND_SERVICE] logging: { driver: json-file, options: { max-size: 10m, max-file: "3" } } site2: image: wordpress:php8.2-apache container_name: site2 restart: unless-stopped ports: ["8002:80"] environment: WORDPRESS_DB_HOST: site2_db WORDPRESS_DB_USER: wp WORDPRESS_DB_PASSWORD: "strong-pw" WORDPRESS_DB_NAME: wp volumes: ["./site2/wp-content:/var/www/html/wp-content"] depends_on: [site2_db] site2_db: image: mariadb:11 container_name: site2_db restart: unless-stopped environment: MARIADB_DATABASE: wp MARIADB_USER: wp MARIADB_PASSWORD: "strong-pw" MARIADB_ROOT_PASSWORD: "strong-root-pw" volumes: ["./site2/db:/var/lib/mysql"] site3: image: node:22-alpine container_name: site3 restart: unless-stopped ports: ["8003:3000"] working_dir: /app volumes: ["./site3:/app"] command: ["node", "server.js"]
cd /opt/sites && docker compose up -d — Docker pulls images, starts containers. Each site is now reachable from pfSense at 192.168.10.20:800X. Hit those ports from HAProxy backends (Step 5).
At your domain registrar (or Cloudflare if you use their DNS), create A records for each site pointing to your public IP from Step 3.
# At your DNS host (GoDaddy / Cloudflare / registrar): Type Name Value TTL A site1.yourdomain 203.0.113.22 Auto A site2.yourdomain 203.0.113.22 Auto A site3.yourdomain 203.0.113.22 Auto A @ 203.0.113.22 Auto # bare root domain A www 203.0.113.22 Auto # www alias
You're now publicly reachable on port 443. That's a target. Three layers of defence:
Follow the Day-1 hardening playbook — change admin password, HTTPS-only WebGUI on custom port, restrict admin to management VLAN, 2FA, Suricata IDS on WAN, AutoConfigBackup.
# SSH: key-only, custom port, disable root login sudo sed -i 's/^#\?Port .*/Port 52022/' /etc/ssh/sshd_config sudo sed -i 's/^#\?PermitRootLogin .*/PermitRootLogin no/' /etc/ssh/sshd_config sudo sed -i 's/^#\?PasswordAuthentication .*/PasswordAuthentication no/' /etc/ssh/sshd_config sudo systemctl restart sshd # Host firewall — allow only 22 and container ports from LAN sudo apt install -y ufw sudo ufw default deny incoming sudo ufw default allow outgoing sudo ufw allow from 192.168.10.0/24 to any port 52022 proto tcp # SSH from LAN only sudo ufw allow from 192.168.10.1 to any port 8001:8099 proto tcp # pfSense -> containers sudo ufw enable # Fail2ban — block SSH brute forcers sudo apt install -y fail2ban sudo systemctl enable --now fail2ban # Automatic security updates sudo dpkg-reconfigure --priority=low unattended-upgrades # Disable services you don't need sudo systemctl disable --now snapd cups bluetooth avahi-daemon 2>/dev/null || true
/tmp, /var/cache/nginx) as in-memory tmpfs instead of writable disk layer.nginx:1.27-alpine not nginx:latest. Then run docker compose pull weekly for controlled updates.docker compose down + rsync or restic to Backblaze B2 / S3. Test a restore every month.ISP planning + modem reconfiguration + pfSense install + HAProxy + Let's Encrypt + Docker host + first 3 sites migrated + hardening + documentation. Typical turnaround: 3 working days.