Skip to main content

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

vpseasy-0.0.7.tar.gz (17.9 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.7-py3-none-any.whl (19.3 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: vpseasy-0.0.7.tar.gz
  • Upload date:
  • Size: 17.9 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.7.tar.gz
Algorithm Hash digest
SHA256 5ee0753cdb64cff6f5274bce11169293cad3a2a1ea9cfaf5b5fc9d3f9c3f0c9f
MD5 20699e5639a4fb92aea5fa8fdfa22098
BLAKE2b-256 dd140c7a756c23ac2d86bab1cfb17db0584f024cfffe0d41ef20ede6cc488ece

See more details on using hashes here.

File details

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

File metadata

  • Download URL: vpseasy-0.0.7-py3-none-any.whl
  • Upload date:
  • Size: 19.3 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.7-py3-none-any.whl
Algorithm Hash digest
SHA256 96317b54d5254bd5ec6719f205a79a61303bf37945debedca731fc336f1c86d8
MD5 642607897a57f2e172abcfc698e22efe
BLAKE2b-256 1885d605ddab82e69a9e861daa863709559d07a3b033c0b3b600e7b3506d0d1e

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