vps_init()
builds a hardened cloud-init YAML for production: UFW firewall,
fail2ban, unattended upgrades, Docker, and your SSH keys — in one call.
multi_init()
is the local equivalent for Multipass: same Docker setup, no UFW or
fail2ban (faster VM boot).
# Load your local public keys (auto-detects ~/.ssh/id_*.pub)
pub_keys = load_pub_keys()
# Production-ready cloud-init: UFW, fail2ban, Docker, your SSH key
yaml = vps_init('myserver', pub_keys)
print(yaml)Both functions accept pkgs and cmds for extra packages and run
commands. docker=False skips the Docker install entirely.
# Add nginx, run a custom post-boot script
yaml = vps_init('myserver', pub_keys,
pkgs=['nginx'],
cmds=['echo done > /tmp/setup.txt'])Before spending money on a real VPS, test your cloud-init and deployment against a local Ubuntu VM using Multipass.
multi_init()
generates a Multipass-friendly cloud-init (no UFW, faster boot).
Multipass
wraps the CLI — launch, exec, transfer, delete.
mp = Multipass()
pub_keys = load_pub_keys()
# Write cloud-init to a temp file (Multipass requires a path)
ci = Path(tempfile.mktemp(suffix='.yaml'))
ci.write_text(multi_init('testvm', pub_keys, docker=True))
# Launch a local Ubuntu 24.04 VM
mp.launch('testvm', image='24.04', cpus=1, memory='1G', disk='10G', cloud_init=ci)
ci.unlink()
ip = mp.ip('testvm')
print(f'VM running at {ip}')
# Sync and run your Docker Compose app inside the VM
deploy_mp('testvm', src='./myapp')
# Clean up when done
mp.rm('testvm')Hetzner
wraps the hcloud Python
SDK — no CLI binary or
config files. Set HCLOUD_TOKEN in your environment and you’re ready.
export HCLOUD_TOKEN=your_token_herehz = Hetzner() # reads HCLOUD_TOKEN from env
# See what SSH keys are registered in your Hetzner account
print(hz.key_names())
# List running servers
print(hz.servers())pub_keys = load_pub_keys()
cloud_init_yaml = vps_init('myapp-prod', pub_keys)
# Create a cx23 server in Helsinki — returns {ip: response}
result = hz.create(
name='myapp-prod',
image='ubuntu-24.04',
server_type='cx23', # ~€4/month at time of writing
location='hel1', # hel1, fsn1, nbg1, ash, hil, sin
cloud_init=cloud_init_yaml,
ssh_keys=hz.key_names(),
)
ip = next(iter(result))
print(f'Server provisioning at {ip} — cloud-init running in background')After provisioning, cloud-init runs asynchronously. Use
wait_ssh()
to block until the server is reachable,
chk_cloud_init()
to confirm bootstrap completed, then
deploy() to
rsync your Compose stack and bring it up.
SSH_KEY = '~/.ssh/id_ed25519' # private key matching the pub key you injected
# Block until SSH is up (cloud-init takes 1-3 min on a fresh VPS)
wait_ssh(ip, k=SSH_KEY, tout=300)
print('SSH ready')
# Confirm cloud-init finished successfully
status = chk_cloud_init(ip, k=SSH_KEY)
print(f'cloud-init: {status}') # 'done' = all good
# Confirm Docker is running and the deploy user can use it
print(f'docker: {chk_docker(ip, k=SSH_KEY)}') # True = ready
# Rsync ./myapp → /srv/app on the server, then docker compose up -d --build
deploy('./myapp', ip, key=SSH_KEY, path='/srv/app')# Run an arbitrary command, capture output
out = run_ssh(ip, 'systemctl status nginx', key=SSH_KEY, capture=True)
# Run multiple commands in one SSH session
run_ssh(ip, 'cd /srv/app', 'git pull', 'docker compose restart', key=SSH_KEY)
# Rsync a directory (trailing slash = send contents, not the dir itself)
sync('./myapp/', '/srv/app', ip, key=SSH_KEY, exclude=['.git', '__pycache__'])SSH_KEY = os.path.expanduser('~/.ssh/id_ed25519')
APP_DIR = './myapp'
# 1. Load local SSH keys
pub_keys = load_pub_keys()
# 2. Generate production cloud-init
ci_yaml = vps_init('myapp-prod', pub_keys)
# 3. Provision on Hetzner
hz = Hetzner()
result = hz.create('myapp-prod', cloud_init=ci_yaml,
ssh_keys=hz.key_names(), location='hel1')
ip = next(iter(result))
print(f'Provisioning {ip}...')
# 4. Wait for SSH + verify bootstrap
wait_ssh(ip, k=SSH_KEY, tout=300)
assert chk_cloud_init(ip, k=SSH_KEY) == 'done', 'cloud-init failed!'
assert chk_docker(ip, k=SSH_KEY), 'Docker not ready!'
# 5. Deploy app
deploy(APP_DIR, ip, key=SSH_KEY)
print(f'App live at http://{ip}')run_ssh and
sync are the
low-level primitives that
deploy
builds on — useful when you need finer control.
Here’s the complete flow — from zero to a running app:
| Symbol | Description |
|---|---|
vps_init(hostname, pub_keys, ...) |
Production cloud-init YAML (UFW, fail2ban, Docker) |
multi_init(hostname, pub_keys, ...) |
Local Multipass cloud-init YAML (no UFW) |
load_pub_keys(paths=None) |
Read public keys from ~/.ssh/id_*.pub |
Multipass |
Launch/list/delete local Ubuntu VMs |
deploy_mp(name, src, ...) |
Sync dir + docker compose up inside a Multipass VM |
Hetzner |
Create/list/delete Hetzner Cloud servers |
wait_ssh(host, ...) |
Poll until SSH accepts connections |
chk_cloud_init(host, ...) |
Return cloud-init status string |
chk_docker(host, ...) |
Verify Docker daemon is running |
run_ssh(host, *cmds, ...) |
Run commands on a remote host |
sync(src, dst_path, host, ...) |
Rsync local path to remote host |
deploy(src, host, ...) |
sync + docker compose up -d |
# Latest release
pip install vpseasy
# Or from source
pip install git+https://github.com/vedicreader/vpseasy.gitRequirements: Python 3.10+, hcloud, fastcloudinit, dockeasy,
fastcore. Multipass and rsync only needed for local-VM and deploy
workflows.