create resources, deploy code in prod and dev. supports hetzner and multipass
Project description
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 AAAAC3NzaC1lZDI1NTE5AAAAIG5vF0hxKfho9gZ9nWIp5GIq+UDkZTQ+/v1lgzp+bk5K 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 AAAAC3NzaC1lZDI1NTE5AAAAIH+XjqpWlA8Zcct/3Py1OasAupD8py5/oUlxI4359V8z 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 95.216.194.42 ...
SSH to host 95.216.194.42 check succeeded
cloud-init status: running
cloud-init status: running
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: 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/tmpo9ofr56s/myapp/ deploy@95.216.194.42:/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: 16846d84-4424-459d-925b-2df55ed21703
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
docker-compose check output: /srv/app/docker-compose.yml
docker compose ran with build →
Deployed at myapp-prod, key: /Users/71293/.ssh/myapp-prod
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']
Docker Compose helpers
Any app that dockeasy can build — FastHTML, FastAPI, Go, Rust, Node —
follows the same production Compose shape when deployed behind
Cloudflare Tunnel: an app service, a caddy reverse proxy, a
cloudflared tunnel container, a shared web network, and two named
volumes for Caddy state.
caddy_stack()
generates that structure from a domain and any dockeasy Dockerfile
object.
vols_to_binds()
converts absolute container paths to local bind mounts. The root=
argument saves all three files (Dockerfile, docker-compose.yml,
Caddyfile); without it the Compose object is returned without
writing anything.
d = Path(tempfile.mkdtemp())
df = fasthtml_app(pkgs=['sqlite3'], vols=['/app/data'], healthcheck='/health')
c = caddy_stack('myapp.example.com', df, vols=['/app/data'], root=d)
print(c)
services:
app:
build: .
volumes:
- ./data:/app/data
env_file:
- .env
restart: unless-stopped
networks:
- web
caddy:
image: caddy:2
depends_on:
- app
volumes:
- /private/var/folders/kg/9vdw4mdd1fs58svgh4k1qhr09x7dqh/T/tmp_dtwwgkg/Caddyfile:/etc/caddy/Caddyfile
- caddy_data:/data
- caddy_config:/config
networks:
- web
restart: unless-stopped
cloudflared:
image: cloudflare/cloudflared:latest
command: tunnel --no-autoupdate run --url http://caddy
environment:
- TUNNEL_TOKEN=${CF_TUNNEL_TOKEN}
networks:
- web
restart: unless-stopped
networks:
web: null
volumes:
caddy_data: null
caddy_config: null
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 |
vols_to_binds(vols) |
["/app/data"] -> ["./data:/app/data"] for Compose bind mounts |
caddy_stack(domain, df, ...) |
Compose file: app + caddy + cloudflared + web network + caddy volumes |
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.5.tar.gz.
File metadata
- Download URL: vpseasy-0.0.5.tar.gz
- Upload date:
- Size: 17.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0e3bb6c68b0709d9163134932e406683dd93f565b8e389884394a55da7cddef3
|
|
| MD5 |
5d3c15d391987c3c1961498034513d49
|
|
| BLAKE2b-256 |
6c04a600f85ba5d9f53423b16a332230710cd308ff5298d7909000f05f580cfe
|
File details
Details for the file vpseasy-0.0.5-py3-none-any.whl.
File metadata
- Download URL: vpseasy-0.0.5-py3-none-any.whl
- Upload date:
- Size: 19.2 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 |
68ccfeb10ecc71c979bbada32f9196efd41459ad86791be32bb8bd89dae00f65
|
|
| MD5 |
cf5792855bf68bc5f3a7e6e023f91183
|
|
| BLAKE2b-256 |
e35305f8dcfa29dc65dbc5ca57874ac5c0c915a8c27a58a404654bf2822a2c7c
|