Skip to content

vedicreader/vpseasy

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

vpseasy

Cloud-init generation

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'])

Local testing with Multipass

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')

Provision on Hetzner

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_here
hz = 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')

Deploy your app

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}')

SSH helpers

run_ssh and sync are the low-level primitives that deploy builds on — useful when you need finer control.

Full lifecycle in one script

Here’s the complete flow — from zero to a running app:

API reference

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

Install

# Latest release
pip install vpseasy

# Or from source
pip install git+https://github.com/vedicreader/vpseasy.git

Requirements: Python 3.10+, hcloud, fastcloudinit, dockeasy, fastcore. Multipass and rsync only needed for local-VM and deploy workflows.

About

create resources, deploy code in prod and dev. supports hetzner and multipass

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors