create resources, deploy code in prod and dev. supports hetzner and multipass
Project description
vpseasy
Install
pip install vpseasy
pub_keys = load_pub_keys()
print(f'{len(pub_keys)} key(s) found')
if pub_keys: print(pub_keys[0][:3] + '...')
4 key(s) found
ssh...
Cloud-init
multi_init()
— local Multipass VMs (no UFW).
vps_init()
— production (UFW, fail2ban, Docker).
_vm = 'testvm'
mi = multi_init(_vm, docker=False) # docker=False: skip install+reboot, much faster for local testing
print(mi.yaml)
ci = vps_init('demo-prod', pub_keys)
print(ci.yaml)
#cloud-config
hostname: demo-prod
preserve_hostname: false
packages:
- curl
- fail2ban
- unattended-upgrades
package_update: true
package_upgrade: true
disable_root: true
ssh_pwauth: false
users:
- name: deploy
groups:
- sudo
shell: /bin/bash
sudo:
- ALL=(ALL) NOPASSWD:ALL
ssh_authorized_keys:
- ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAcfGCEzt9TJzVOBmlzU4N8LvLKQxUQQ/mIikwArFO1K karthikrajgopal.laxminaarayanan@bain.com
- ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDM9kAjDCitQvl6oMLab4yK7LgylUV9lo/M/U8uqi5r8LPbo/qia7Oaj+Qo2LSs+Hsesr/mdKNjFle5DRb91k/Ns4GX2V8QyBcKwBK0Davjmj57X+4ZfnmC4HjZ/gzyYe1hV4Vjy4IBE/JSArSP3hI8tCAgN00tNLxJ5AJnYzdgfKpVa+zI54cdrJTPhko12mEyOih62xOWeHT16Y7jGPIaOzPo2YTFM+omwEm7TfyxuRLxaDN0d/ZxlKPi/+vBcuAOItrjA6DJ7lnYwk3cXubCKgHP3uc0MeBBX74S58zpoHlAp6XdePKOoBwam1/aYm+7zq+9GJyDGV25iD9bDwSn+0oNexJFRgQxwFdIwVUk4Iyq6OocUNLX8vrI1Qr6kXIchDVS922LGf+Z5aai89wqaxLLB+U4+dNTzb6zKMtQSMdGrgFZt0N5j/aMuRE5rkfWoeiCT8DmekuxA6NDaF76CYgcIsEaOsCTuNjwrpWBQnQ82r20tfegagT3Y38xBp9PLbFHbM45HkjAsOyT6QqXh8C6XofZJa/QL25uCTHQBBxZCVtYPccs6Sjh4u7zJ3cAH1E7tWgpzwFeBrgBMLIJ0Atwbp4fCqAm1XHyaLaeuVceFk3YXbCUK8DmN0FApkzlxpeCwHQ2ZLUOF5HExYSC2IoHYbYJl8oyhwG8Bwe9hQ== karthik.rajgopal@hotmail.com
- ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICOEkEHB+WPV42E9izurjWdTrBAHpgDxK5JcdzhkmN7T karthik.rajgopal@hotmail.com
- ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKu2Oi3apJsaZmO3dubl+8ZUA7ivXJdhzILPucA8F2ag karthik.rajgopal@hotmail.com
runcmd:
- curl -fsSL https://get.docker.com | sh
- usermod -aG docker deploy
- systemctl enable --now docker
- ufw default deny incoming
- ufw default allow outgoing
- ufw logging off
- ufw allow 22/tcp
- ufw --force enable
apt:
conf: 'APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Download-Upgradeable-Packages "1";
APT::Periodic::AutocleanInterval "7";
APT::Periodic::Unattended-Upgrade "0";
Unattended-Upgrade::Automatic-Reboot "false";
'
write_files:
- path: /etc/logrotate.d/00-cloud-init-global
owner: root:root
permissions: '0644'
content: "/var/log/*.log {\n weekly\n rotate 7\n compress\n su root adm\n create\n missingok\n}\n"
power_state:
mode: reboot
message: Rebooting
timeout: 1
condition: true
Local testing with Multipass
Requires Multipass installed. Pass
cloud_init=mi directly to mp.launch(). Use docker=True in
multi_init()
if your app needs Docker pre-installed (adds ~2 min for install +
reboot).
mp = Multipass()
mp.rm(_vm,purge=True) # idempotent cleanup
vm = mp.launch(_vm, image='24.04', cpus=1, memory='1G', disk='10G', cloud_init=mi)
ip = mp.ip(vm.name)
print(f'VM at {ip}, key: {vm.key}')
deploy_mp(_vm, src='./myapp')
mp.rm(_vm)
Creating testvm Configuring testvm Starting testvm Waiting for initialization to complete
launch failed: The following errors occurred:
timed out waiting for initialization to complete
CalledProcessError: Command '['multipass', 'launch', '24.04', '-n', 'testvm', '-c', '1', '-m', '1G', '-d', '10G', '--cloud-init', '-']' returned non-zero exit status 2.
[31m---------------------------------------------------------------------------[39m
[31mCalledProcessError[39m Traceback (most recent call last)
[36mCell[39m[36m [39m[32mIn[6][39m[32m, line 4[39m
[32m 1[39m [38;5;66;03m#| eval: False[39;00m
[32m 2[39m mp = Multipass()
[32m 3[39m mp.rm(_vm,purge=[38;5;28;01mTrue[39;00m) [38;5;66;03m# idempotent cleanup[39;00m
[32m----> [39m[32m4[39m vm = mp.launch(_vm, image=[33m'24.04'[39m, cpus=[32m1[39m, memory=[33m'1G'[39m, disk=[33m'10G'[39m, cloud_init=mi)
[32m 5[39m ip = mp.ip(vm.name)
[32m 6[39m print(f'VM at {ip}, key: {vm.key}')
[32m 7[39m deploy_mp(_vm, src=[33m'./myapp'[39m)
[36mFile [39m[32m~/code/personal/orgs/vpseasy/vpseasy/core.py:29[39m, in [36mMultipass.launch[39m[34m(self, name, image, cpus, memory, disk, cloud_init, mounts)[39m
[32m 27[39m [38;5;28;01mfor[39;00m hp, vp [38;5;129;01min[39;00m (mounts [38;5;129;01mor[39;00m {}).items(): args += [[33m'[39m[33m--mount[39m[33m'[39m, [33mf[39m[33m'[39m[38;5;132;01m{[39;00mhp[38;5;132;01m}[39;00m[33m:[39m[38;5;132;01m{[39;00mvp[38;5;132;01m}[39;00m[33m'[39m]
[32m 28[39m [38;5;28;01mif[39;00m cloud_init: args += [[33m'[39m[33m--cloud-init[39m[33m'[39m, [33m'[39m[33m-[39m[33m'[39m]
[32m---> [39m[32m29[39m [30;43msubprocess[39;49m[30;43m.[39;49m[30;43mrun[39;49m[30;43m([39;49m[30;43margs[39;49m[30;43m,[39;49m[30;43m [39;49m[30;43minput[39;49m[30;43m=[39;49m[30;43mcloud_init[39;49m[30;43m.[39;49m[30;43myaml[39;49m[30;43m [39;49m[30;43;01mif[39;49;00m[30;43m [39;49m[30;43mcloud_init[39;49m[30;43m [39;49m[30;43;01melse[39;49;00m[30;43m [39;49m[30;43;01mNone[39;49;00m[30;43m,[39;49m[30;43m [39;49m[30;43mtext[39;49m[30;43m=[39;49m[30;43;01mTrue[39;49;00m[30;43m,[39;49m[30;43m [39;49m[30;43mcheck[39;49m[30;43m=[39;49m[30;43;01mTrue[39;49;00m[30;43m)[39;49m
[32m 30[39m [38;5;28;01mreturn[39;00m AttrDict(name=name, key=cloud_init.key [38;5;28;01mif[39;00m cloud_init [38;5;28;01melse[39;00m [38;5;28;01mNone[39;00m)
[36mFile [39m[32m~/Library/Application Support/uv/python/cpython-3.13.1-macos-aarch64-none/lib/python3.13/subprocess.py:577[39m, in [36mrun[39m[34m(input, capture_output, timeout, check, *popenargs, **kwargs)[39m
[32m 575[39m retcode = process.poll()
[32m 576[39m [38;5;28;01mif[39;00m check [38;5;129;01mand[39;00m retcode:
[32m--> [39m[32m577[39m [38;5;28;01mraise[39;00m CalledProcessError(retcode, process.args,
[32m 578[39m output=stdout, stderr=stderr)
[32m 579[39m [38;5;28;01mreturn[39;00m CompletedProcess(process.args, retcode, stdout, stderr)
[31mCalledProcessError[39m: Command '['multipass', 'launch', '24.04', '-n', 'testvm', '-c', '1', '-m', '1G', '-d', '10G', '--cloud-init', '-']' returned non-zero exit status 2.
Provision on Hetzner
Set HCLOUD_TOKEN in your environment.
vps_init()
auto-generates an SSH key pair when pub_keys=None.
hz = Hetzner() # reads HCLOUD_TOKEN
ci = vps_init('myapp-prod', pub_keys)
svr = hz.create('myapp-prod', cloud_init=ci, ssh_keys=hz.key_names(), location='hel1')
print(f'Provisioning at {svr.ip}')
Deploy
wait_ssh()
blocks until SSH is up.
deploy()
rsyncs your Compose stack and brings it up.
wait_ssh(svr.ip, tout=300)
assert chk_cloud_init(svr.ip) == 'done'
assert chk_docker(svr.ip)
deploy('./myapp', svr.ip)
Install agent skill
Copies SKILL.md to .agents/skills/vpseasy/ (project-local) and
~/.claude/skills/vpseasy/ (global Claude Code).
mv_skill_md(dry_run=True) # preview; pass dry_run=False to actually install
API reference
| Symbol | Description |
|---|---|
load_pub_keys(paths=None) |
Read ~/.ssh/id_*.pub → list of strings |
gen_key(slug, key_dir=None) |
Generate ed25519 pair → AttrDict(key, pub, pub_str) |
multi_init(hostname, pub_keys, ...) |
Multipass cloud-init YAML → AttrDict(yaml, key) |
vps_init(hostname, pub_keys, ...) |
Production cloud-init YAML → AttrDict(yaml, key) |
Multipass |
Launch / list / exec / delete local Ubuntu VMs |
deploy_mp(name, src, path, build) |
Sync dir + docker compose up in Multipass VM |
Hetzner |
Create / list / delete Hetzner Cloud servers |
wait_ssh(host, u, k, tout) |
Poll until SSH accepts connections |
chk_cloud_init(host, u, k) |
Return cloud-init status string |
chk_docker(host, u, k) |
Verify Docker daemon running |
run_ssh(host, *cmds, ...) |
Run commands over SSH |
sync(host, src, path, ...) |
Rsync local dir to remote |
deploy(host, src, path, ...) |
sync + docker compose up -d |
mv_skill_md(dry_run, dir) |
Install agent SKILL.md |
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file vpseasy-0.0.2.tar.gz.
File metadata
- Download URL: vpseasy-0.0.2.tar.gz
- Upload date:
- Size: 14.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
bf35e6542af9c66816e6bdfeda9d741da1fcc3573e353e25aefb0af7ebdee464
|
|
| MD5 |
3bdc4da7af0e1d8ea402a6afc0e45088
|
|
| BLAKE2b-256 |
2959c4f6866b73c47c529aa1c9233f7989665d6de69970069706f94927f8b904
|
File details
Details for the file vpseasy-0.0.2-py3-none-any.whl.
File metadata
- Download URL: vpseasy-0.0.2-py3-none-any.whl
- Upload date:
- Size: 15.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4942f9cb95b90293f9429a2160c499dd8fac4bce1ee5950243b80ae21198f70b
|
|
| MD5 |
78a10f24f7e65d5191fb14d192e63116
|
|
| BLAKE2b-256 |
510a2bb12f21737a73b2569a71cee4078e966eb81ed18996b6e7cd765067b5cb
|