Automated provisioning script for secure remote development, pentesting, and AI workstations on Ubuntu 24.04 VPS instances.
This script transforms a fresh Ubuntu 24.04 VPS into a fully configured development environment with:
- Zero-trust network access via Tailscale
- Internal service routing via Traefik
- Local AI inference with Ollama and Open WebUI
- Browser-based IDE with code-server
- Pentest toolkit with Exegol
- Claude Code CLI integration
The entire stack runs behind Tailscale with no public ports exposed (except SSH), making it suitable for sensitive development and security research.
[Internet]
|
[Firewall]
Port 5522 only
|
+------------------------------------+------------------------------------+
| VPS |
| |
| [Tailscale] <---> [Your Devices] |
| | |
| v |
| [Traefik :80] ----+---- [code-server] code.internal |
| (internal) | |
| +---- [Open WebUI] ai.internal |
| | |
| +---- [Ollama API] ollama.internal |
| | |
| +---- [Traefik Dashboard] traefik.internal |
| |
| [Exegol Container] <---> [HTB/THM VPN] |
| (on-demand, host network) |
| |
+---------------------------------------------------------------------------+
- Ubuntu 24.04 LTS (tested on Hostinger KVM)
- Docker and Docker Compose pre-installed
- Minimum 4GB RAM (8GB+ recommended for Ollama)
- Root access for initial setup
- SSH public key for authentication
# Clone the repository
git clone https://github.com/gl0bal01/devbox.git
cd devbox
# Edit configuration (lines 20-30)
nano setup.sh
# Run as root
chmod +x setup.sh
./setup.shEdit these variables in setup.sh before running:
NEW_USER="dev" # Username to create
USER_EMAIL="admin@example.com" # Email for Let's Encrypt (future use)
SSH_PORT="5522" # SSH port (default: 5522)
SSH_PUBLIC_KEY="ssh-ed25519 AAAA..." # Your public key
DOMAIN="example.com" # Your domain (optional)| Component | Purpose |
|---|---|
| User with sudo | Non-root user with passwordless sudo |
| SSH hardening | Non-standard port, key-only auth, root disabled |
| UFW firewall | Default deny, SSH only |
| Tailscale | Zero-trust mesh VPN |
| Docker | Container runtime (verified, not installed) |
| Traefik v3.6 | Internal reverse proxy with label-based routing |
| Ollama | Local LLM inference server |
| Open WebUI | Chat interface for Ollama |
| code-server | VS Code in browser |
| mise | Polyglot version manager (node, python, go, etc.) |
| lazygit | Terminal UI for git |
| lazydocker | Terminal UI for Docker |
| lazyvim | Neovim distribution with IDE features |
| Oh-My-Zsh | Shell configuration with aliases |
| Exegol | Pentest container (pulled on first use) |
From a new terminal on your local machine:
ssh -p 5522 dev@YOUR_SERVER_IPsudo tailscale up --accept-routesOpen the provided URL in your browser to authenticate.
cd ~/docker
./start-all.shAdd to /etc/hosts on your local machine:
TAILSCALE_IP code.internal ai.internal traefik.internal ollama.internal
Replace TAILSCALE_IP with your server's Tailscale IP (tailscale ip -4).
./install-claude-code.sh
claude logindocker exec -it ollama ollama pull llama3.2
docker exec -it ollama ollama pull codellamastart-all # Start all services
stop-all # Stop all services
status # Show service status and Tailscale info| Service | URL |
|---|---|
| VS Code | http://code.internal |
| Open WebUI | http://ai.internal |
| Traefik Dashboard | http://traefik.internal |
| Ollama API | http://ollama.internal or localhost:11434 |
# Connect to HTB VPN
htb-vpn ~/htb/lab.ovpn
# Check VPN status
htb-vpn status
# Launch Exegol with host network (inherits VPN)
exegol
# Disconnect VPN
htb-vpn stopInside Exegol, all tools (nmap, metasploit, gobuster, etc.) have direct access to the HTB network.
dps # docker ps (formatted)
dpsa # docker ps -a (formatted)
dlog NAME # docker logs -f NAME
dex NAME bash # docker exec -it NAME bash
dc up -d # docker compose up -d
dc down # docker compose down
dprune # docker system prune -af
lzd # lazydocker TUIlg # lazygit TUI
gs # git status
gp # git pull
gP # git pushvim # neovim (lazyvim)
vi # neovim (lazyvim)tsip # Show Tailscale IP
tsstatus # Show Tailscale status
tsup # Connect Tailscale
tsdown # Disconnect Tailscale~/
├── docker/
│ ├── traefik/
│ │ ├── docker-compose.yml
│ │ ├── traefik.yml
│ │ └── dynamic/
│ ├── ollama-openwebui/
│ │ └── docker-compose.yml
│ ├── code-server/
│ │ └── docker-compose.yml
│ ├── exegol-workspace/
│ ├── start-all.sh
│ ├── stop-all.sh
│ ├── status.sh
│ ├── exegol-htb.sh
│ └── htb-vpn.sh
├── projects/
├── htb/
└── install-claude-code.sh
- Only SSH (port 5522) is exposed to the public internet
- All other services are accessible only via Tailscale
- UFW firewall configured with default deny incoming
- SSH: Key-based authentication only, password auth disabled
- Root login: Disabled
- code-server: Password protected (generated during setup)
- Open WebUI: Application-level auth
- Rotate the generated passwords stored in the setup output
- Enable MagicDNS in Tailscale admin console
- Configure Tailscale ACLs for multi-device access control
- Regularly update containers:
docker compose pull && docker compose up -d - Monitor logs:
docker logs -f traefik
The script is designed to be run multiple times safely:
- Existing users are preserved
- Existing Docker networks are reused
- SSH configuration changes prompt for confirmation
- Docker stack overwrites require explicit confirmation
- SSH keys are not duplicated
# Verify SSH is running
sudo systemctl status ssh
# Check listening port
sudo ss -tlnp | grep ssh
# Verify firewall
sudo ufw status# Check containers are running
docker ps
# Check Traefik logs
docker logs traefik
# Verify Tailscale connection
tailscale status
# Test internal routing
curl -H "Host: ai.internal" http://localhost# Re-authenticate
sudo tailscale logout
sudo tailscale up --accept-routes# Verify HTB VPN is connected on host
ip addr show tun0
# Check VPN logs
tail -f /tmp/htb-vpn.logCreate a new docker-compose.yml with Traefik labels:
services:
myservice:
image: myimage:latest
labels:
- "traefik.enable=true"
- "traefik.http.routers.myservice.rule=Host(`myservice.internal`)"
- "traefik.http.services.myservice.loadbalancer.server.port=8080"
networks:
- proxy-net
networks:
proxy-net:
external: trueFor temporary public access, add Cloudflare Tunnel:
services:
cloudflared:
image: cloudflare/cloudflared:latest
command: tunnel --no-autoupdate run --token YOUR_TUNNEL_TOKEN
restart: unless-stoppedConfigure public hostnames in Cloudflare Zero Trust dashboard.
| Provider | Instance Type | Status |
|---|---|---|
| Hostinger | KVM 8 (32GB RAM, 8 vCPU) | Verified |
| Hetzner | CX31 | Compatible |
| DigitalOcean | Droplet | Compatible |
| AWS | EC2 t3.medium+ | Compatible |
MIT License. See LICENSE for details.