This guide covers setting up a headless Debian server VM, installing Docker, deploying a project via Git, and accessing it on the local network. This is the foundation of how real-world application servers are built and managed.
Your Machine (Browser)
│
▼
pfSense (192.168.1.1) ← routes traffic
│
▼
Dev Server VM (192.168.1.4) ← Debian, headless, SSH only
│
▼
Docker Container ← your project runs here
│
▼
Exposed Port (e.g. :3000) ← accessible on LAN
Name → dev-server
RAM → 2048 MB (2GB)
CPU → 2 cores
Disk → 20GB
Network → VMnet3 (LAN)
During installation the following options were selected:
Language → English
Location → India
Hostname → dev-server
Domain → home.lab
Root password → (strong password set)
User account → (personal username + password)
Disk → Guided, entire disk, single partition
At the software selection screen:
☐ Debian desktop environment ← unchecked (no GUI needed)
☐ GNOME ← unchecked
☑ SSH server ← checked (remote access)
☑ Standard system utilities ← checked
No GUI means:
- Lower RAM and CPU usage
- Faster boot
- More resources available for your application
- Same as how production servers are run in the real world
After installation, log in directly on the VM console and run:
# Switch to root
su -
# Update all packages
apt update && apt upgrade -y
# Install sudo
apt install sudo -y
# Give your user sudo privileges
usermod -aG sudo yourusername
# Exit root
exitVerify sudo works:
sudo apt updateServers must always have the same IP. Set it manually so it never changes after a reboot.
sudo nano /etc/network/interfacesFind the existing DHCP line and replace with:
iface ens33 inet static
address 192.168.1.4
netmask 255.255.255.0
gateway 192.168.1.1
dns-nameservers 192.168.1.2
Apply the change:
sudo systemctl restart networkingVerify:
ip addr show # confirms 192.168.1.4
ping 8.8.8.8 # confirms internet worksAdd DNS record in Pi-hole:
dev.home.lab → 192.168.1.4
From this point, never work inside the VM window directly. SSH in from your main machine — this is how real server management works.
From Windows (PowerShell or Terminal):
ssh yourusername@192.168.1.4From Linux/macOS:
ssh yourusername@192.168.1.4You are now managing the server remotely, exactly like a real sysadmin would.
Docker is installed from the official Docker repository, not from Debian's default packages (which can be outdated).
# Install required dependencies
sudo apt install ca-certificates curl gnupg -y
# Create directory for Docker's GPG key
sudo install -m 0755 -d /etc/apt/keyrings
# Add Docker's official GPG key
curl -fsSL https://download.docker.com/linux/debian/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg
# Add Docker's official repository
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
https://download.docker.com/linux/debian \
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
# Install Docker Engine and Compose
sudo apt update
sudo apt install docker-ce docker-ce-cli containerd.io docker-compose-plugin -y
# Add your user to docker group (avoids needing sudo for every docker command)
sudo usermod -aG docker $USER
# Log out and back in for group change to take effect
exitSSH back in, then verify:
docker --version
docker compose versionsudo apt install git -y# Clone into home directory
git clone https://github.com/yourusername/yourproject.git
# Move into project folder
cd yourproject# Build and start containers in detached mode (runs in background)
docker compose up -d
# Verify containers are running
docker psYou should see your container listed with status Up.
With the container running and a port exposed (e.g. 3000), the project is accessible from any machine on your network:
http://192.168.1.4:3000 ← by IP and port
http://dev.home.lab:3000 ← by local domain (if DNS record added)
Browser sends request to 192.168.1.4:3000
│
▼
Debian VM receives it on port 3000
│
▼
Docker forwards it to the container's internal port
│
▼
Your application handles the request
│
▼
Response travels back to browser
In your docker-compose.yml the ports section defines this mapping:
ports:
- "3000:3000" # host_port:container_portLeft side (3000) = port your VM listens on Right side (3000) = port your app listens on inside the container
They don't have to match:
ports:
- "8000:3000" # access on :8000, app runs on :3000 inside# See running containers
docker ps
# See all containers including stopped
docker ps -a
# View live logs
docker logs -f containername
# Stop the project
docker compose down
# Restart the project
docker compose restart
# Pull latest image and redeploy
docker compose pull && docker compose up -d
# Enter a running container (like SSH into it)
docker exec -it containername bash
# Check resource usage
docker statsWhen you push new code and want to redeploy:
# Pull latest code
git pull
# Rebuild and restart containers
docker compose down
docker compose up -d --build
# Verify new version is running
docker ps
docker logs containername| Machine | IP | Role |
|---|---|---|
| pfSense | 192.168.1.1 |
Router, Firewall, DHCP |
| Services VM | 192.168.1.2 |
Pi-hole DNS + Nginx Proxy |
| VPN VM | 192.168.1.3 |
WireGuard + OpenVPN |
| Dev Server | 192.168.1.4 |
Application server, Docker |
| Your Home Lab | Real World Equivalent |
|---|---|
| Debian VM | Cloud VM (AWS EC2, DigitalOcean Droplet) |
| VMware VMnet3 | VPC / Private Network |
| pfSense | Cloud Firewall / Security Groups |
| Docker on Debian | Docker on production server |
| Git clone | CI/CD pipeline pulls latest image |
| Local domain | Public domain with SSL |
| WireGuard VPN | Corporate VPN / Bastion host |
The only difference between your setup and production is:
- Scale (more servers, more containers)
- Automation (CI/CD deploys instead of manual git pull)
- Public SSL certificates instead of local ones
The architecture and principles are identical.