Skip to main content

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)
#cloud-config
hostname: testvm
preserve_hostname: false
packages:
- curl
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 AAAAC3NzaC1lZDI1NTE5AAAAIA8djqnLMaZGgYVtiPJCGtDutbid1rnHNuExJolxyXST 71293@MELMAC-71293
ci = vps_init('demo-prod')
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 AAAAC3NzaC1lZDI1NTE5AAAAIGe5xfUhopf+J8VLThytJWd1yO5ad8sWenfkoAtTtj9I 71293@MELMAC-71293
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).

import tempfile
_app = Path(tempfile.mkdtemp()) / 'myapp'
_app.mkdir()
(_app / 'docker-compose.yml').write_text('services:\n  app:\n    image: nginx:alpine\n')
41
mp = Multipass()
try: mp.rm(_vm, purge=True)
except: pass
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}')
Creating testvm  Configuring testvm  Starting testvm  Waiting for initialization to complete  Launched: testvm
VM at 192.168.2.56, key: /Users/71293/.ssh/testvm
deploy_mp(_vm, src=_app)
Resolved SSH key from name slug: /Users/71293/.ssh/testvm

Warning: Permanently added '192.168.2.56' (ED25519) to the list of known hosts.

Ensured remote path /srv/app exists and is writable by deploy
Resolved SSH key from name slug: /Users/71293/.ssh/testvm
Running rsync: rsync -az --delete -e ssh -o StrictHostKeyChecking=accept-new -i /Users/71293/.ssh/testvm /var/folders/kg/9vdw4mdd1fs58svgh4k1qhr09x7dqh/T/tmp8vrta37_/myapp/ deploy@192.168.2.56:/srv/app/
Rsync completed successfully
Resolved SSH key from name slug: /Users/71293/.ssh/testvm
mp.rm(_vm)

Provision and deploy on Hetzner

Set HCLOUD_TOKEN in your environment. hetzner_deploy() provisions the server, waits for cloud-init, and deploys in one call. It’s idempotent — re-running against an existing server just redeploys.

hz = Hetzner()
svr = hetzner_deploy('myapp-prod',_app, hz) # hz is not required
print(f'Deployed at {svr.name}, key: {svr.key}')
Server myapp-prod provisioning at 65.21.147.20 ...
SSH check succeeded: 
cloud-init status: running
cloud-init status: unknown
cloud-init status: unknown
cloud-init status: unknown
cloud-init status: unknown
cloud-init status: running
cloud-init status: running
cloud-init status: running
cloud-init status: running
cloud-init status: running
cloud-init status: running
cloud-init status: unknown
cloud-init status: unknown
cloud-init status: unknown
cloud-init status: done
Ensured remote path /srv/app exists and is writable by deploy
Running rsync: rsync -az --delete -e ssh -o StrictHostKeyChecking=accept-new -i /Users/71293/.ssh/myapp-prod /var/folders/kg/9vdw4mdd1fs58svgh4k1qhr09x7dqh/T/tmplfmfw3uz/myapp/ deploy@65.21.147.20:/srv/app/
Rsync completed successfully
Docker info: Client: Docker Engine - Community
 Version:    29.4.3
 Context:    default
 Debug Mode: false
 Plugins:
  buildx: Docker Buildx (Docker Inc.)
    Version:  v0.33.0
    Path:     /usr/libexec/docker/cli-plugins/docker-buildx
  compose: Docker Compose (Docker Inc.)
    Version:  v5.1.3
    Path:     /usr/libexec/docker/cli-plugins/docker-compose
  model: Docker Model Runner (Docker Inc.)
    Version:  v1.1.37
    Path:     /usr/libexec/docker/cli-plugins/docker-model

Server:
 Containers: 0
  Running: 0
  Paused: 0
  Stopped: 0
 Images: 0
 Server Version: 29.4.3
 Storage Driver: overlayfs
  driver-type: io.containerd.snapshotter.v1
 Logging Driver: json-file
 Cgroup Driver: systemd
 Cgroup Version: 2
 Plugins:
  Volume: local
  Network: bridge host ipvlan macvlan null overlay
  Log: awslogs fluentd gcplogs gelf journald json-file local splunk syslog
 CDI spec directories:
  /etc/cdi
  /var/run/cdi
 Swarm: inactive
 Runtimes: runc io.containerd.runc.v2
 Default Runtime: runc
 Init Binary: docker-init
 containerd version: 77c84241c7cbdd9b4eca2591793e3d4f4317c590
 runc version: v1.3.5-0-g488fc13e
 init version: de40ad0
 Security Options:
  apparmor
  seccomp
   Profile: builtin
  cgroupns
 Kernel Version: 6.8.0-111-generic
 Operating System: Ubuntu 24.04.4 LTS
 OSType: linux
 Architecture: x86_64
 CPUs: 2
 Total Memory: 3.73GiB
 Name: myapp-prod
 ID: a8429b9a-7a14-49fa-9ee8-c18e71b015ac
 Docker Root Dir: /var/lib/docker
 Debug Mode: false
 Experimental: false
 Insecure Registries:
  ::1/128
  127.0.0.0/8
 Live Restore Enabled: false
 Firewall Backend: iptables

 Image nginx:alpine Pulling 
 612c0c1df4c5 Pulling fs layer 0B
 aee4e54b3865 Pulling fs layer 0B
 781ff50d2644 Pulling fs layer 0B
 82736a35d0e7 Pulling fs layer 0B
 453da7dbc73e Pulling fs layer 0B
 583599bb7d38 Pulling fs layer 0B
 4a8b0b2a5b19 Pulling fs layer 0B
 6a0ac1617861 Pulling fs layer 0B
 e8fc446e336c Download complete 0B
 6192e1e6a438 Download complete 0B
 6a0ac1617861 Downloading 2.097MB
 781ff50d2644 Download complete 0B
 612c0c1df4c5 Downloading 4.194MB
 6a0ac1617861 Download complete 0B
 aee4e54b3865 Download complete 0B
 453da7dbc73e Download complete 0B
 4a8b0b2a5b19 Download complete 0B
 583599bb7d38 Download complete 0B
 6a0ac1617861 Extracting 1B
 82736a35d0e7 Download complete 0B
 612c0c1df4c5 Downloading 11.53MB
 6a0ac1617861 Extracting 1B
 612c0c1df4c5 Download complete 0B
 6a0ac1617861 Pull complete 0B
 82736a35d0e7 Extracting 1B
 781ff50d2644 Pull complete 0B
 aee4e54b3865 Pull complete 0B
 583599bb7d38 Pull complete 0B
 82736a35d0e7 Pull complete 0B
 453da7dbc73e Pull complete 0B
 4a8b0b2a5b19 Pull complete 0B
 612c0c1df4c5 Extracting 1B
 612c0c1df4c5 Extracting 1B
 612c0c1df4c5 Extracting 1B
 612c0c1df4c5 Extracting 1B
 612c0c1df4c5 Pull complete 0B
 Image nginx:alpine Pulled 
 Network app_default Creating 
 Network app_default Created 
 Container app-app-1 Creating 
 Container app-app-1 Created 
 Container app-app-1 Starting 

docker compose deployed with build
Deployed at myapp-prod, key: /Users/71293/.ssh/myapp-prod

 Container app-app-1 Started 
o = lambda: [s['name'] for s in hz.servers()]
print('servers: ', o())
hz.delete('myapp-prod')
print('Deleted server.')
print('servers', o())
servers:  ['vedicreader-cx32-hel', 'myapp-prod']
Deleted server.
servers ['vedicreader-cx32-hel']

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)
Would copy to: ['.agents/skills/vpseasy/SKILL.md', '/Users/71293/.claude/skills/vpseasy/SKILL.md']

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
hetzner_deploy(name, src, ...) Full pipeline: provision → wait → deploy (idempotent)
wait_ssh(host, u, k, tout) Poll until SSH accepts connections
wait_ready(host, u, k, tout) Poll SSH then cloud-init until done
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

vpseasy-0.0.3.tar.gz (16.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

vpseasy-0.0.3-py3-none-any.whl (17.6 kB view details)

Uploaded Python 3

File details

Details for the file vpseasy-0.0.3.tar.gz.

File metadata

  • Download URL: vpseasy-0.0.3.tar.gz
  • Upload date:
  • Size: 16.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.3

File hashes

Hashes for vpseasy-0.0.3.tar.gz
Algorithm Hash digest
SHA256 9f0762960a7ff065278739b5d1314824a903bfa2d2ea439916742cddecadb2f8
MD5 74b6673f50c2202d6f471ef7322c65e9
BLAKE2b-256 4ce5f791491ca3bb3ea036360fb63f8797659b2966b0d8e322fea9e5e70b3af6

See more details on using hashes here.

File details

Details for the file vpseasy-0.0.3-py3-none-any.whl.

File metadata

  • Download URL: vpseasy-0.0.3-py3-none-any.whl
  • Upload date:
  • Size: 17.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.3

File hashes

Hashes for vpseasy-0.0.3-py3-none-any.whl
Algorithm Hash digest
SHA256 9386838bb64a3c49426fbf278166a16a4c3ec107fc21d4c23338500bebc67964
MD5 25f5d029eef8f752043fe4b70a18679e
BLAKE2b-256 f7e1a0749bb88aca90f563c9d9c566f4cb0fe14f2e7e580b3ebb555d819ac4fe

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page