Production-hardened provisioning for secure remote development, penetration testing, and AI workstations on Ubuntu 24.04 VPS instances.
v3.0 highlights:
- Bootstrap-only
setup.sh— tracked compose files, helper scripts, Traefik config, and middleware live underservices/andscripts/in the repo.setup.shrsyncs them into~/docker/. No more heredoc sprawl. - SHA-pinned upstream images via
docker compose config --lock-image-digests+ committeddocker-compose.lock.yml. Weekly CI refresh + cosign keyless signing + SBOM + SLSA provenance. See ADR-0003. - Verified downloads — every
curl | shin the bootstrap is routed throughfetch_and_verifywith pinned SHA256s inscripts/lib/download-manifest.sh. Emergency override:DEVBOX_ALLOW_UNVERIFIED=1. See ADR-0005. - Traefik security middleware — HSTS/frame-deny/CSP/rate-limit/IPAllowList (Tailscale IPv4 CGNAT + IPv6 ULA + loopback) wired at the entryPoint level, not globally. See ADR-0006.
- Honest privilege model —
devis in thedockergroup (root-equivalent). Sudoers whitelists onlyufw/tailscale/openvpn/specificsystemctl. The security boundary is Tailscale ACL + SSH key auth + UFW default-deny. See docs/security.md and ADR-0011. - Operator-friendly ops —
backup.shwith gpg/age encryption-at-rest,diagnose.shbundle for incident reports, pre-rsync snapshots in~/.local/share/devbox/backups/, flock guard against concurrent installs. See docs/ops.md. - 55/55 bats unit tests passing. Full verification matrix gated by
scripts/ci/+.github/workflows/.
- Overview
- Features
- Requirements
- Quick Start
- Configuration
- Post-Installation
- Reproducibility & Verification
- Usage
- Security
- Troubleshooting
- Documentation
- Directory Structure
- License
DevBox transforms a fresh Ubuntu 24.04 VPS into a fully configured development environment. The entire stack runs behind Tailscale with no public ports exposed except SSH.
Two access paths are supported:
Path A — Tailscale-only (default): All services on port 80, bound to the Tailscale IP. No domain or certificates needed. HTTP over WireGuard is already encrypted.
Path B — Public HTTPS (opt-in): Adds port 443, Let's Encrypt wildcard cert via OVH DNS-01. Requires ENABLE_HTTPS=true, a domain, and ~/.config/devbox/ovh.env with OVH API credentials.
Path A (default) Path B (opt-in)
──────────────── ───────────────
[Your Devices] ──Tailscale──> [Traefik :80] [Internet] ──> [Traefik :443]
| |
(Tailscale IP) | (0.0.0.0, TLS) |
v v
+---------------------------[VPS]-------------------------------------------+
| |
| [Tailscale] <──> [Your Devices] [Internet (optional, HTTPS only)] |
| | |
| v |
| [Traefik] ──────+──── [Open WebUI] ai.internal / ai.DOMAIN |
| | | |
| | +──── [Ollama API] ollama.internal (basicAuth) |
| | | |
| | +──── [Traefik Dashboard] traefik.internal (basicAuth) |
| | |
| +──> [Docker Socket Proxy] ──> /var/run/docker.sock |
| (internal network, read-only API access) |
| |
| [Exegol Container] <──> [HTB/THM VPN] |
| (noVNC bound via UFW to tailscale0 only — see docs/exegol.md) |
| |
| [AI Dev Stack] — Claude Code, OpenCode, Goose, LLM, Fabric |
| (native CLI tools for AI-assisted development) |
| |
+---------------------------------------------------------------------------+
| Component | Description |
|---|---|
| Tailscale | Zero-trust mesh VPN for secure access |
| Traefik | Internal reverse proxy with label-based routing + security middleware |
| Ollama | Local LLM inference server (with basicAuth on the external route) |
| Open WebUI | Chat interface for Ollama models |
| Exegol | Penetration testing container with full toolkit (noVNC scoped to Tailscale) |
| Tool | Provider | Purpose |
|---|---|---|
| Claude Code | Anthropic | AI-assisted coding CLI |
| OpenCode | Open-source | Multi-provider AI coding |
| Goose | Block | AI coding agent |
| LLM | Datasette | CLI for language models |
| Fabric | danielmiessler | AI prompts framework |
- mise: Polyglot version manager (Node, Python, Go)
- lazygit / lazydocker / lazyvim: Terminal UIs
- Rust + cargo-binstall + zellij: Modern terminal/dev stack
- Bun: Fast JavaScript runtime
- Oh-My-Zsh: Shell configuration with DevBox aliases
- Secrets stored in
.envfiles with 600 permissions (rendered at install time via whitelistedenvsubst, never committed) - Docker socket proxy prevents container escape
- All containers run with
no-new-privileges,cap_drop: ALL, explicitcap_add - Traefik dashboard protected with basic authentication
- Ollama external route protected with separate basicAuth (internal openwebui→ollama bypasses auth)
- Security headers middleware: HSTS (HTTPS mode), X-Frame-Options, nosniff, referrer-policy
- Rate limiting: 100 req/min average, burst 50
- IPAllowList: Tailscale CGNAT IPv4 + IPv6 ULA + loopback
- Per-container resource limits and healthchecks
stop_grace_periodtuned per service (60s for Ollama to drain in-flight inference)- Log rotation via
json-filedriver (max 10MB × 3 files) — drift-checked by CI umask 077+flockguard + cleanup trap insetup.sh(partial-install prevention)- Pre-rsync snapshot of every overwritten file to
~/.local/share/devbox/backups/<ts>/ - SSH key-only auth, custom port, UFW default-deny,
fail2ban
- Ubuntu 24.04 LTS
- Docker and Docker Compose plugin (v2.22+) pre-installed
- Minimum 4GB RAM (24GB+ recommended for Ollama with larger models)
- Root access for initial setup
- SSH public key for authentication
# Clone the repository
git clone https://github.com/gl0bal01/devbox.git
cd devbox
# (Optional) Override defaults via env vars
export DEVBOX_USER=dev
export DEVBOX_SSH_PORT=5522
export SSH_PUBLIC_KEY="$(cat ~/.ssh/id_ed25519.pub)"
# (Optional) For HTTPS mode, create the OVH credentials file FIRST:
install -d -m 0700 ~/.config/devbox
cat > ~/.config/devbox/ovh.env <<EOF
OVH_ENDPOINT=ovh-eu
OVH_APPLICATION_KEY=your-key
OVH_APPLICATION_SECRET=your-secret
OVH_CONSUMER_KEY=your-consumer-key
EOF
chmod 600 ~/.config/devbox/ovh.env
export ENABLE_HTTPS=true
export DEVBOX_DOMAIN=example.com
# Run as root
chmod +x setup.sh
sudo ./setup.shAfter setup.sh completes, follow the Post-Installation steps.
Configuration via environment variables (overrides the defaults in setup.sh):
| Variable | Default | Purpose |
|---|---|---|
DEVBOX_USER |
dev |
Username to create |
DEVBOX_EMAIL |
admin@example.com |
Email for Let's Encrypt |
DEVBOX_SSH_PORT |
5522 |
SSH port |
DEVBOX_DOMAIN |
example.com |
Domain (HTTPS mode only) |
ENABLE_HTTPS |
false |
Opt-in to public HTTPS |
SSH_PUBLIC_KEY |
(unset) | Your SSH pubkey string |
OPENWEBUI_SECRET |
(auto-generated) | Session secret for Open WebUI |
For HTTPS mode, create ~/.config/devbox/ovh.env before running setup.sh. The script refuses ENABLE_HTTPS=true if the file is missing. OVH credentials NEVER live in tracked source per ADR-0005.
Default setup (Tailscale-only): Leave
ENABLE_HTTPS=false. Services are accessible at.internalhostnames over Tailscale.Public HTTPS: Set
ENABLE_HTTPS=true,DEVBOX_DOMAIN, and create~/.config/devbox/ovh.env. See docs/https-setup.md.
The script reads your SSH public key from these sources (in order):
- Environment variable:
SSH_PUBLIC_KEY="ssh-ed25519 AAAA..." - File:
~/.ssh/devbox_authorized_keyor/root/.ssh/devbox_authorized_key - Manual: Add key to
~/.ssh/authorized_keysafter setup
From a new terminal on your local machine (do NOT close the setup terminal until this works):
ssh -p 5522 dev@YOUR_SERVER_IPsudo tailscale up --accept-routes --advertise-tags=tag:devboxPer ADR-0003, lockfiles are generated by running docker compose config --lock-image-digests on the target host. Run it twice to handle the first-generation tag-strip drift (see Critic Issue #2 in the plan):
cd ~/docker/../devbox # or wherever the repo lives
./scripts/update-images.sh --apply
./scripts/update-images.sh --apply # second pass stabilizes
git add services/*/docker-compose.lock.yml
git commit -m "lock: initial image digests"cd ~/docker
./start-all.shAdd to /etc/hosts on your local machine (replace with your Tailscale IP):
100.X.Y.Z ai.internal traefik.internal ollama.internal exegol.internal
Get your Tailscale IP with tailscale ip -4.
~/docker/install-ai-dev-stack.sh # Interactive menu
~/docker/install-ai-dev-stack.sh --all # Install all tools
~/docker/install-ai-dev-stack.sh --status # Check installation status
~/docker/install-ai-dev-stack.sh --update # Update installed toolsAfter installation:
claude login # Authenticate Claude Code
goose configure # Configure Goose
llm keys set openai # Set OpenAI key for LLM
fabric --setup # Configure Fabric
export ANTHROPIC_API_KEY=... # Set key for OpenCodedocker exec -it ollama ollama pull llama3.2
docker exec -it ollama ollama pull codellama~/docker/security-check.shDevBox ships its release artifact as a cosign-keyless-signed, SBOM-attested, SLSA-provenance-attested tarball via the .github/workflows/weekly-rebuild.yml CI pipeline. Three install modes are documented in docs/updating.md:
| Mode | How | Signed? | Smoke-tested? | Recommended for |
|---|---|---|---|---|
latest |
git pull origin main |
No | No | Development only |
weekly-YYYYMMDD |
git checkout weekly-20260420 or download release tarball |
Yes (cosign keyless) | Yes | Production |
@sha256:<digest> |
Pin release tarball by SHA256 | Yes | Yes | Maximum paranoia |
latest is NOT recommended for any environment where compromise has consequences. It tracks main HEAD with no signature, no smoke test, no provenance. Use weekly-YYYYMMDD for any persistent install.
# Download the release tarball + signature + certificate
gh release download weekly-20260420 \
-p devbox.tar -p devbox.tar.sha256 \
-p devbox.tar.sig -p devbox.tar.pem
# Verify SHA256 first
sha256sum -c devbox.tar.sha256
# Verify cosign keyless signature
cosign verify-blob devbox.tar \
--signature devbox.tar.sig \
--certificate devbox.tar.pem \
--certificate-identity-regexp '^https://github.com/gl0bal01/devbox/.*' \
--certificate-oidc-issuer 'https://token.actions.githubusercontent.com'Verify both --certificate-identity and --certificate-oidc-issuer — without them, any cosign keyless signature from any GitHub Actions workflow would be accepted.
Committed docker-compose.lock.yml files target x86_64 (the CI runner architecture). Operators on arm64 (e.g. Apple Silicon Linux VMs) must regenerate lockfiles locally with ./scripts/update-images.sh --apply on their own host before start-all.sh. Documented in ADR-0003.
If Sigstore (Rekor/Fulcio) is down, CI fails closed and no weekly-* tag is cut. Emergency path: git checkout main && sudo ./setup.sh — treat as untrusted and re-verify by running weekly-rebuild manually as soon as Sigstore recovers. See docs/updating.md.
start-all # Start all services (uses --wait --wait-timeout 120)
stop-all # Stop all services (reverse order)
status # Show service status + Tailscale info + COMPOSE_FILE in use
security-check # Verify security hardening (10 checks)
devbox-backup # Encrypted backup of volumes + configs (gpg/age)
devbox-diagnose # Bundle diagnostic info for incident reportsThree opt-in operator scripts under scripts/host/ add per-target
hardening on top of the baseline. Each is dry-run by default;
--apply is required to mutate. See docs/harden-modules.md
for full caveats.
# Scope Docker DNAT rules to a CIDR (default Tailscale CGNAT 100.64.0.0/10)
sudo scripts/host/harden-dnat-scope.sh # dry-run
sudo scripts/host/harden-dnat-scope.sh --apply
# fail2ban traefik-auth + recidive jails (only installs when Traefik is publicly bound)
sudo scripts/host/harden-fail2ban.sh # dry-run
sudo scripts/host/harden-fail2ban.sh --apply
# Generic age-encrypted, systemd-timed backup pipeline (per service tag)
sudo scripts/host/harden-backup-skeleton.sh \
--tag myapp --path /home/myapp --path /var/lib/myapp # dry-run
sudo scripts/host/harden-backup-skeleton.sh \
--tag myapp --path /home/myapp --path /var/lib/myapp --applyThese modules support the mixed Tailscale + public topology — they
act per target, not globally. A host can run some services bound to the
Tailscale IP and others bound to 0.0.0.0; harden-fail2ban.sh strict-
detects each Traefik container and refuses to install jails on the
Tailscale-only shape (use Tailscale ACLs instead). harden-dnat-scope.sh
accepts --port to scope its action to one published port.
| Service | URL (default) | URL (ENABLE_HTTPS=true) |
|---|---|---|
| Open WebUI | http://ai.internal | https://ai.DOMAIN |
| Traefik Dashboard | http://traefik.internal | http://traefik.internal |
| Ollama API | http://ollama.internal | http://ollama.internal |
All .internal URLs require Tailscale. Add to /etc/hosts on your laptop:
100.X.Y.Z ai.internal traefik.internal ollama.internal exegol.internal
# Set your server's Tailscale IP
export OLLAMA_SERVER_IP="100.x.x.x"
# Configure shell functions for quick AI queries (jq-safe, idempotent)
bash scripts/laptop/ollama-setup.sh
# Configure Zed editor (jq deep-merge — preserves existing settings)
bash scripts/laptop/zed-setup.shSee docs/remote-ide-setup.md for manual SSH tunnel setup.
# Connect to HTB VPN (PID-file based, no global pkill)
htb-vpn ~/htb/lab.ovpn
# Check status
htb-vpn status
# Start Exegol with desktop (random per-container password)
exegol # Default: exegol-htb on port 45377
exegol osint-box --port 45378 # Multiple containers on different ports
# Rotate VNC password for a running container
exegol-reset ~/docker/exegol-reset-vnc.sh exegol-htb
# List containers
exegol-list
# Access at: http://exegol.internal:45377/vnc.html
# Username: root, password: printed once at container start
# UFW restricts the port to the tailscale0 interface only — not publicly reachableSee docs/exegol.md for the full workflow.
Defined in scripts/install/dev-zshrc and installed to ~/.zshrc. See that file for the full list — highlights:
- Docker:
dps,dpsa,dlog NAME,dex NAME bash,dc up -d,dprune - Git:
lg(lazygit),gs,gp,gP - Tailscale:
tsip,tsstatus,tsup,tsdown - DevBox:
start-all,stop-all,status,security-check,exegol,exegol-reset,htb-vpn,devbox-backup,devbox-diagnose
- Only SSH (port 5522) is exposed to the public internet
- All other services are accessible only via Tailscale (or optional HTTPS via configured domain)
- UFW firewall configured with default deny incoming
- Traefik ports 80/443 bound to Tailscale IP — inaccessible from LAN by design
- Exegol noVNC port: container-internal
0.0.0.0, host-side UFW rule scoped totailscale0interface
Per docs/security.md and ADR-0011:
devis in thedockergroup. This is equivalent to root for anyone who compromises thedevaccount (docker socket → bind-mount/). The security boundary is the Tailscale ACL + SSH key auth + UFW default-deny, not local privilege separation.
Sudoers whitelist (only commands NOT handled by the docker boundary):
dev ALL=(root) NOPASSWD: /usr/sbin/ufw, /usr/bin/tailscale, /usr/sbin/openvpn
dev ALL=(root) NOPASSWD: /bin/systemctl restart docker, /bin/systemctl reload ufw
dev ALL=(root) PASSWD: ALL
| Measure | Implementation |
|---|---|
| Secrets Management | .env files with 600 perms, rendered at install time from .env.template |
| Docker Socket Protection | Traefik uses docker-socket-proxy (read-only API subset) |
| Privilege Escalation Prevention | All containers: no-new-privileges: true |
| Capability Dropping | All containers: cap_drop: ALL + explicit cap_add whitelist |
| Resource Limits | Memory, CPU, and PID limits on every container |
| Log Rotation | json-file driver, 10MB × 3 files, drift-checked by CI |
| Grace Period | stop_grace_period tuned per service (60s for Ollama) |
| Service | Method |
|---|---|
| SSH | Key-based only (password disabled, root disabled) |
| Open WebUI | Application-level (disable signup after admin creation) |
| Traefik Dashboard | Basic Auth (dashboard-auth middleware) |
| Ollama external route | Basic Auth (ollama-auth middleware) |
~/.devbox-credentialsis written viaumask 077+install -m 0600atomic permission drop- If
gpg --list-keysreturns a key at install time, the credentials file is GPG-encrypted and the plaintext isshred-ed trapregistered toshredthe plaintext on SIGINT (Scenario E mitigation)
- Save credentials from
~/.devbox-credentials(or.devbox-credentials.gpg) to a password manager, thenshred -uthe file - Disable Open WebUI signup after creating admin account
- Run
security-checkperiodically - Enable MagicDNS in Tailscale admin console
- Configure Tailscale ACLs for multi-device access control
- Use
weekly-YYYYMMDDinstalls in production; verify the cosign signature before promotion
sudo systemctl status ssh # Verify SSH is running
sudo ss -tlnp | grep ssh # Check listening port
sudo ufw status # Verify firewalldocker ps # Check containers are running
docker logs traefik # Check Traefik logs
tailscale status # Verify Tailscale connection
status # DevBox status dashboard
devbox-diagnose # Bundle full diagnostic tarballsudo tailscale logout
sudo tailscale up --accept-routesIf you see "Another devbox setup.sh is already running" — check for stale lock:
sudo ls -la /var/lock/devbox-setup.lock
sudo rm /var/lock/devbox-setup.lock # only if no real setup is runningFor more scenarios, see docs/troubleshooting.md and docs/ops.md.
| Document | Description |
|---|---|
| docs/security.md | Trust model, privilege boundaries, honest disclosure |
| docs/harden-modules.md | Opt-in harden-*.sh modules: DNAT scoping, fail2ban, encrypted backups |
| docs/updating.md | Digest refresh, cosign verify, install modes |
| docs/ops.md | Backup/restore, DR runbook, 6 pre-mortem scenarios |
| docs/quick-reference.md | Command cheat sheet |
| docs/https-setup.md | Enable public HTTPS with OVH DNS-01 |
| docs/exegol.md | Multi-container pentest workflows |
| docs/ollama-optimization.md | Performance tuning for Ollama |
| docs/remote-ide-setup.md | Configure local IDE with remote Ollama |
| docs/troubleshooting.md | Common issues and solutions |
| services/README.md | services/ tree conventions + rendering table |
| scripts/laptop/README.md | Configure laptop for remote Ollama |
| CONTRIBUTING.md | How to contribute |
All 13 ADRs are in docs/adr/:
| ADR | Title |
|---|---|
| 0001 | Extract heredoc-generated configs into a tracked services/ tree |
| 0002 | HTTPS via Compose override file |
| 0003 | SHA pinning via docker compose config --lock-image-digests |
| 0004 | setup.sh becomes a host-bootstrap-only installer |
| 0005 | Replace curl|sh with fetch_and_verify + pinned SHA256s |
| 0006 | Traefik security middleware wired at entryPoint level |
| 0007 | Per-file duplicated x-logging anchor with CI drift check |
| 0008 | Minor-pinned tag strategy with weekly refresh |
| 0009 | Template rendering with whitelisted envsubst |
| 0010 | TLS at registry + cosign keyless for release artifact |
| 0011 | Accept docker-group = root-equivalent |
| 0012 | Exegol image left unpinned (operator-controlled risk) |
| 0013 | No rollback machinery; snapshot-based recovery |
Repository structure (v3.0):
devbox/
├── setup.sh # Host bootstrap (963 lines, 4 heredocs)
├── services/ # Tracked compose + Traefik configs
│ ├── README.md # services/ conventions + rendering table
│ ├── traefik/
│ │ ├── docker-compose.yml # HTTP base with x-logging anchor
│ │ ├── docker-compose.https.yml # HTTPS override (port 443, OVH env)
│ │ ├── docker-compose.lock.yml # Generated by update-images.sh, committed
│ │ ├── traefik.yml # HTTP-only static config
│ │ ├── traefik.https.yml.template # HTTPS variant (envsubst $USER_EMAIL)
│ │ ├── .env.template # OVH credentials template
│ │ └── dynamic/
│ │ ├── dashboard-auth.yml.template
│ │ ├── ollama-auth.yml.template
│ │ ├── middlewares-base.yml # frame-deny, nosniff, referrer
│ │ ├── middlewares-https.yml # HSTS (HTTPS mode only)
│ │ ├── middlewares-rate.yml # rate limit
│ │ └── middlewares-allowlist.yml # IPAllowList
│ └── ollama-openwebui/
│ ├── docker-compose.yml
│ ├── docker-compose.https.yml
│ ├── docker-compose.lock.yml
│ └── .env.template
├── scripts/
│ ├── host/ # Runtime helpers (rsynced to ~/docker/)
│ │ ├── start-all.sh # Uses --wait --wait-timeout 120
│ │ ├── stop-all.sh
│ │ ├── status.sh
│ │ ├── security-check.sh # 10 checks
│ │ ├── exegol-start.sh # Random password + UFW tailscale0 rule
│ │ ├── exegol-reset-vnc.sh # Password rotation helper
│ │ ├── htb-vpn.sh # PID-file based, no global pkill
│ │ ├── backup.sh # gpg/age encryption-at-rest
│ │ ├── diagnose.sh # Incident-report bundle
│ │ ├── install-ai-dev-stack.sh # User-invoked AI tools installer
│ │ ├── harden-dnat-scope.sh # Scope Docker DNAT to a CIDR (opt-in)
│ │ ├── harden-fail2ban.sh # traefik-auth + recidive jails (opt-in)
│ │ └── harden-backup-skeleton.sh # Generic age-encrypted backup (opt-in)
│ ├── install/ # Install-time tracked files
│ │ ├── dev-zshrc # The canonical .zshrc
│ │ └── mise-profile.sh # /etc/profile.d/mise.sh
│ ├── laptop/ # Run on your laptop
│ │ ├── ollama-setup.sh # jq-safe JSON, idempotent
│ │ ├── zed-setup.sh # jq deep-merge
│ │ ├── shell-config.txt
│ │ └── README.md
│ ├── lib/ # Verified download infrastructure
│ │ ├── fetch-verify.sh # fetch_and_verify function
│ │ ├── download-manifest.sh # Pinned URL + SHA256 manifest
│ │ └── sources/ # Per-upstream version refresh handlers
│ │ ├── bun.sh
│ │ ├── claude.sh
│ │ ├── fabric.sh
│ │ ├── goose.sh
│ │ ├── lazydocker.sh
│ │ ├── lazygit.sh
│ │ ├── neovim.sh
│ │ ├── opencode.sh
│ │ ├── rustup.sh
│ │ └── tailscale.sh
│ ├── ci/ # CI helper scripts
│ │ ├── lint.sh # shellcheck + bash -n + bats
│ │ ├── check-compose-config.sh # HTTP + HTTPS compose validation
│ │ ├── check-anchor-consistency.sh # x-logging drift check
│ │ ├── smoke-test.sh # docker compose up + healthcheck
│ │ ├── sbom-targets.sh # Enumerate locked image digests
│ │ └── release-notes.sh # Weekly release note generator
│ ├── update-images.sh # Wraps compose --lock-image-digests
│ └── update-manifest.sh # Refreshes download-manifest.sh
├── tests/
│ ├── README.md
│ ├── lib/test-helpers.bash
│ └── unit/ # 55/55 bats tests passing
│ ├── anchor-consistency.bats # 8 tests
│ ├── fetch-verify.bats # 10 tests
│ ├── htb-vpn-pidfile.bats # 16 tests
│ ├── parse-image-line.bats # 12 tests
│ ├── render-env.bats # 9 tests
│ └── fixtures/bin/ # Mock openvpn, pkill, ip, sudo
├── docs/
│ ├── adr/ # 13 ADRs
│ ├── security.md # Honest privilege model
│ ├── updating.md # Digest refresh + cosign verify
│ ├── ops.md # DR runbook, 6 scenarios
│ ├── exegol.md
│ ├── https-setup.md
│ ├── ollama-optimization.md
│ ├── quick-reference.md
│ ├── remote-ide-setup.md
│ └── troubleshooting.md
├── .github/
│ └── workflows/
│ ├── weekly-rebuild.yml # cosign keyless + SBOM + SLSA provenance
│ └── pr-validate.yml # lint + compose-config + bats
├── CONTRIBUTING.md
├── LICENSE
└── README.md
Server directory structure (after running setup.sh):
~/
├── .devbox-credentials[.gpg] # Generated credentials (DELETE after saving)
├── .config/devbox/
│ ├── config.env # Install-level config (ENABLE_HTTPS, TAILSCALE_IP, COMPOSE_FILE)
│ └── ovh.env # OVH credentials (HTTPS mode; 600)
├── .local/share/devbox/backups/ # Pre-rsync snapshots (Scenario B)
├── docker/ # rsync'd from services/ + scripts/host/
│ ├── traefik/ # HTTP + HTTPS compose + dynamic middleware
│ ├── ollama-openwebui/
│ ├── exegol-workspace/
│ ├── start-all.sh, stop-all.sh, status.sh, security-check.sh
│ ├── exegol-start.sh, exegol-reset-vnc.sh, htb-vpn.sh
│ ├── backup.sh, diagnose.sh, install-ai-dev-stack.sh
│ └── (per-service .env files, rendered from .env.template at install time)
├── projects/ # Your code projects
└── htb/ # HTB .ovpn files
| 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.
- Tailscale Documentation
- Traefik Documentation
- Ollama Documentation
- Claude Code Documentation
- Exegol Documentation
- LazyVim Documentation
- Sigstore / cosign
- SLSA Framework
Last updated: 2026-04-13 (v3.0 — production-readiness refactor: SHA pinning, cosign keyless, honest privilege model, 55/55 tests, 13 ADRs)