End-to-end developer operations toolkit: Docker, VPS, DNS, Caddy, and CI/CD from Python
Project description
fastops
Install
pip install fastops
Requires Python 3.10+ and a docker (or podman) CLI on your PATH.
The Dockerfile Builder
Dockerfile
is an immutable, fluent builder — every method returns a new instance,
so you can safely branch, compose, or pass it to functions.
from fastops.core import Dockerfile
df = (Dockerfile()
.from_('python', '3.12-slim')
.workdir('/app')
.copy('requirements.txt', '.')
.run('pip install --no-cache-dir -r requirements.txt')
.copy('.', '.')
.expose(8080)
.cmd(['python', 'app.py']))
print(df)
Batteries included: apt_install and a built-in escape hatch
apt_install(*pkgs, y=False) chains apt-get update && apt-get install
for you. Any Dockerfile keyword not explicitly modelled is available via
__getattr__ — just call it as a method.
df_ubuntu = (Dockerfile()
.from_('ubuntu', '22.04')
.apt_install('curl', 'git', y=True)
.run('pip install uv'))
# Any unknown attribute becomes an instruction: df.KEYWORD(args)
df_scratch = (Dockerfile()
.from_('scratch')
.add('binary', '/binary')
.entrypoint(['/binary']))
print(df_ubuntu)
print()
print(df_scratch)
Multi-stage builds
Chain multiple from_() calls — the builder handles stage aliases
naturally.
df_ms = (Dockerfile()
.from_('golang:1.21', as_='builder')
.workdir('/src')
.copy('.', '.')
.run('go build -o /app')
.from_('alpine')
.copy('/app', '/app', from_='builder')
.cmd(['/app']))
print(df_ms)
Dockerfile.load(path) parses an existing file into the builder for
further chaining. df.save(path) writes it back and returns the Path.
Building and Running Images
These helpers require a running Docker daemon. They wrap docker build,
docker run, docker ps, etc. via subprocess.
from fastops.core import Dockerfile, run, test, containers, images, stop, logs, rm, rmi
df = Dockerfile().from_('python', '3.12-slim').cmd(['python', '-c', 'print("hi")'])
img = df.build(tag='myapp:latest') # saves Dockerfile + runs docker build
ok = test(img, 'python -c "import os"') # True if exit code 0
cid = run(img, detach=True, ports={8080: 8080}, name='myapp')
print(containers()) # ['myapp']
print(logs('myapp', n=5))
stop('myapp'); rm('myapp'); rmi(img)
Raw CLI access via dk
For anything not covered by helpers, dk (a
Docker()
singleton) dispatches any subcommand. kwargs become flags using the same
convention: single-char k=v → -k v, multi-char key=v → --key=v.
from fastops.core import dk
try:
print(dk.version())
except Exception as e:
print(f'Docker not running: {e}')
# Equivalent shell commands:
# dk.ps(format='{{.Names}}', a=True) → docker ps --format={{.Names}} -a
# dk.image('prune', f=True) → docker image prune -f
# dk.build('.', t='myapp', rm=True) → docker build . -t myapp --rm
Docker Compose
Compose
is a fluent builder for docker-compose.yml. Chain .svc(),
.network(), .volume(), then .save() to write or .up() to write
and start.
from fastops.compose import Compose
dc = (Compose()
.svc('db',
image='postgres:16',
env={'POSTGRES_PASSWORD': 'secret'},
volumes={'pgdata': '/var/lib/postgresql/data'})
.svc('redis', image='redis:7-alpine')
.svc('app',
build='.',
ports={8080: 8080},
env={'DATABASE_URL': 'postgresql://postgres:secret@db/app'},
depends_on=['db', 'redis'],
networks=['web'])
.network('web')
.volume('pgdata'))
print(dc)
appfile() — standard Python webapp Dockerfile
A one-liner for the most common Dockerfile pattern: copy requirements,
pip install, copy source, expose port, run main.py.
from fastops.compose import appfile
print(appfile(port=8080, image='python:3.12-slim'))
Use Compose.load(path) to round-trip an existing docker-compose.yml.
DockerCompose(path) wraps the CLI for running compose commands against
any file.
Reverse Proxying with Caddy
The caddy
module generates a Caddyfile and returns service kwargs for
Compose.svc().
Four topologies are supported, from simplest to most secure.
Plain Caddy — auto-TLS, ports 80 and 443
from fastops.caddy import caddyfile, caddy
import tempfile
# What the Caddyfile looks like:
print(caddyfile('myapp.example.com', port=8080))
with tempfile.TemporaryDirectory() as tmp:
dc = (Compose()
.svc('app', build='.', networks=['web'])
.svc('caddy', **caddy('myapp.example.com', port=8080,
conf=f'{tmp}/Caddyfile'))
.network('web')
.volume('caddy_data').volume('caddy_config'))
print(dc)
DNS-01 challenge — no port 80 required
Pass dns='cloudflare' or dns='duckdns' to use DNS-01 ACME. This lets
you get TLS certs on machines that have port 80 blocked.
print(caddyfile('myapp.example.com', port=8080, dns='cloudflare', email='me@example.com'))
Cloudflare tunnel — zero open ports
With cloudflared=True, Caddy listens on plain HTTP and cloudflared
tunnels traffic in. No ports need to be open on the host at all.
from fastops.caddy import cloudflared_svc
with tempfile.TemporaryDirectory() as tmp:
dc = (Compose()
.svc('app', build='.', networks=['web'])
.svc('caddy', **caddy('myapp.example.com', port=8080,
cloudflared=True, conf=f'{tmp}/Caddyfile'))
.svc('cloudflared', **cloudflared_svc(), networks=['web'])
.network('web')
.volume('caddy_data').volume('caddy_config'))
print(dc)
Full security stack — CrowdSec + Cloudflare tunnel
Add crowdsec=True to wire in the CrowdSec intrusion-detection bouncer.
The image is selected automatically based on the combination of
crowdsec
and dns.
from fastops.caddy import crowdsec
with tempfile.TemporaryDirectory() as tmp:
dc = (Compose()
.svc('app', build='.', networks=['web'])
.svc('caddy', **caddy('myapp.example.com', port=8080,
crowdsec=True, cloudflared=True,
conf=f'{tmp}/Caddyfile'))
.svc('crowdsec', **crowdsec())
.svc('cloudflared', **cloudflared_svc(), networks=['web'])
.network('web')
.volume('caddy_data').volume('caddy_config')
.volume('crowdsec-db').volume('crowdsec-config'))
print(dc)
Caddy image selection
crowdsec |
dns |
Image |
|---|---|---|
| False | None | caddy:2 |
| True | None | serfriz/caddy-crowdsec:latest |
| False | 'cloudflare' |
serfriz/caddy-cloudflare:latest |
| True | 'cloudflare' |
ghcr.io/buildplan/csdp-caddy:latest |
| False | 'duckdns' |
serfriz/caddy-duckdns:latest |
SWAG (nginx alternative)
If you prefer LinuxServer
SWAG (nginx + Certbot), use
swag(). It
generates an nginx site-conf and returns service kwargs for
Compose.svc().
from fastops.compose import swag, swag_conf
# nginx site-conf for proxying to app:8080
print(swag_conf('myapp.example.com', port=8080))
with tempfile.TemporaryDirectory() as tmp:
dc = (Compose()
.svc('app', build='.', networks=['web'])
.svc('swag', **swag('myapp.example.com', port=8080,
conf_path=f'{tmp}/proxy.conf'))
.network('web')
.volume('swag_config'))
print(dc)
Local Linux VMs with Multipass
The multipass module wraps the Multipass CLI
for spinning up ephemeral Ubuntu VMs. Ideal for testing deployment
scripts locally — no cloud account needed.
cloud_init_yaml — VM bootstrap config
from fastops.multipass import cloud_init_yaml
# Default: Docker pre-installed
print(cloud_init_yaml(docker=True, packages=['htop', 'tree']))
launch_docker_vm — the one-liner
launch_docker_vm()
is the convenience wrapper that pairs
cloud_init_yaml
with
launch.
It covers the most common use case: spin up a clean Ubuntu VM with
Docker ready to go.
from fastops.multipass import launch_docker_vm, vm_ip, exec_, transfer, delete, vms
# Spin up an Ubuntu VM with Docker pre-installed (takes ~60s first run)
vm = launch_docker_vm('test-vm', cpus=2, memory='2G')
print(vms(running=True)) # ['test-vm']
print(vm_ip('test-vm')) # '192.168.64.5'
exec_('test-vm', 'docker', 'ps') # run any command in the VM
transfer('./docker-compose.yml',
'test-vm:/home/ubuntu/docker-compose.yml') # copy files
delete('test-vm') # purge when done
launch()
accepts cloud_init as either a YAML string or a path to an existing
file — if it’s a string it writes a temp file, passes --cloud-init,
and cleans up automatically.
For raw CLI access use the mp singleton (same pattern as dk):
from fastops.multipass import mp
mp.info("test-vm") # → multipass info test-vm
mp.snapshot("test-vm", name="before-deploy")
Cloudflare DNS and Tunnels
The cloudflare module wraps the official Cloudflare Python
SDK for managing DNS
records and Zero Trust tunnels. Set CLOUDFLARE_API_TOKEN in your
environment.
pip install "fastops[cloudflare]"
# or: pip install cloudflare
dns_record — the one-liner
dns_record()
is the convenience wrapper: reads CLOUDFLARE_API_TOKEN from env, looks
up the zone, deletes any existing record with the same name and type,
then creates the new one.
from fastops.cloudflare import dns_record, CF
# Point myapp.example.com at a server IP
record = dns_record('example.com', 'myapp', '1.2.3.4', proxied=True)
# Full control via CF() for multi-step workflows
cf = CF() # reads CLOUDFLARE_API_TOKEN
# DNS
zid = cf.zone_id('example.com')
records = cf.dns_records(zid)
cf.delete_record(zid, records[0]['id'])
# Tunnels
tunnel = cf.create_tunnel('myapp-prod')
token = cf.tunnel_token(tunnel['id'])
# → pass token as CF_TUNNEL_TOKEN in your environment
App Dockerfiles
The apps module generates complete, production-ready Dockerfiles for
common stacks. All variants support uv-based installs, extra apt
packages, and optional healthchecks.
Python / FastHTML
from fastops.apps import python_app, fasthtml_app
# Generic single-stage Python app
print(python_app(port=8080, cmd=['uvicorn', 'main:app', '--host', '0.0.0.0']))
# FastHTML shortcut: uv + port 5001 + sensible defaults
print(fasthtml_app(port=5001, pkgs=['rclone'], volumes=['/app/data']))
FastAPI + React (two-stage)
from fastops.apps import fastapi_react
# Stage 1: Node builds the frontend; Stage 2: Python serves the API
print(fastapi_react(port=8000, frontend_dir='frontend'))
Go and Rust (two-stage → distroless)
from fastops.apps import go_app, rust_app
print(go_app(port=8080, go_version='1.22'))
print()
print(rust_app(port=8080, binary='myapp'))
Cache mounts for faster rebuilds
run_mount() adds RUN --mount=type=cache,... to any instruction,
keeping pip/uv/cargo/go caches across builds:
df = (Dockerfile().from_('python:3.12-slim')
.run_mount('uv sync --frozen --no-dev', target='/root/.cache/uv')
.run_mount('go mod download', target='/go/pkg/mod'))
VPS Provisioning
The vps module covers the full lifecycle from a blank cloud server to
a running Compose stack: cloud-init generation, Hetzner provisioning,
and SSH-based deployment. No cloud SDK required beyond the hcloud CLI
and ssh/rsync.
vps_init — cloud-init YAML
from fastops.vps import vps_init
# Full bootstrap: UFW, deploy user, Docker, Cloudflare tunnel
yaml = vps_init(
'prod-01',
pub_keys='ssh-rsa AAAA...',
docker=True,
packages=['git', 'htop'],
)
print(yaml[:500])
Hetzner provisioning
create() wraps
hcloud server create. Pass the cloud-init YAML string directly — it
handles the temp-file lifecycle automatically.
from fastops.vps import create, servers, server_ip, delete
ip = create(
'prod-01',
image='ubuntu-24.04',
server_type='cx22',
location='nbg1',
cloud_init=yaml,
ssh_keys=['my-laptop'],
)
print(servers()) # [{'name': 'prod-01', 'ip': '...', 'status': 'running'}]
Requires hcloud CLI and HCLOUD_TOKEN in env.
deploy — sync and start
deploy()
accepts a
Compose
object or a raw YAML string, rsyncs it to the server, and runs
docker compose up -d:
from fastops.vps import deploy
deploy(dc, ip, user='deploy', key='~/.ssh/id_ed25519', path='/srv/myapp')
# 1. mkdir -p /srv/myapp (via SSH)
# 2. rsync docker-compose.yml → prod-01:/srv/myapp/
# 3. docker compose up -d (via SSH)
For one-off commands use
run_ssh():
from fastops.vps import run_ssh
print(run_ssh(ip, 'docker ps', user='deploy', key='~/.ssh/id_ed25519'))
End-to-End: Deploy a Python App
Here is the complete workflow for deploying a Python webapp with Caddy TLS, a Cloudflare tunnel, and CrowdSec — starting from a blank Python file.
Step 1: generate the configs (pure Python, no daemon needed)
from fastops.core import Dockerfile
from fastops.compose import Compose, appfile
from fastops.caddy import caddy, cloudflared_svc, crowdsec
import tempfile
DOMAIN = 'myapp.example.com'
PORT = 8080
# Standard Python app Dockerfile
df = appfile(port=PORT)
with tempfile.TemporaryDirectory() as tmp:
dc = (Compose()
.svc('app', build='.', networks=['web'])
.svc('caddy', **caddy(DOMAIN, port=PORT,
crowdsec=True, cloudflared=True,
conf=f'{tmp}/Caddyfile'))
.svc('crowdsec', **crowdsec())
.svc('cloudflared', **cloudflared_svc(), networks=['web'])
.network('web')
.volume('caddy_data').volume('caddy_config')
.volume('crowdsec-db').volume('crowdsec-config'))
print('--- Dockerfile ---')
print(df)
print('\n--- docker-compose.yml ---')
print(dc)
Step 2: save and deploy (requires Docker daemon)
df.save('Dockerfile')
dc.save('docker-compose.yml')
dc.up() # writes file + runs docker compose up -d
Step 3: wire up DNS (requires CLOUDFLARE_API_TOKEN)
from fastops.cloudflare import dns_record
dns_record('example.com', 'myapp', '1.2.3.4', proxied=True)
Step 4: test locally first (requires Multipass)
from fastops.multipass import launch_docker_vm, vm_ip, exec_, transfer, delete
from fastops.cloudflare import dns_record
vm = launch_docker_vm('test-vm')
transfer('./docker-compose.yml', 'test-vm:/home/ubuntu/')
exec_('test-vm', 'docker', 'compose', 'up', '-d')
ip = vm_ip('test-vm')
dns_record('example.com', 'myapp', ip) # point DNS at the VM
delete('test-vm') # clean up
Step 5: provision and deploy to a real server (requires hcloud
CLI + HCLOUD_TOKEN)
from fastops.vps import vps_init, create, deploy
from fastops.cloudflare import dns_record
# Bootstrap a fresh Hetzner server
yaml = vps_init('prod-01', pub_keys=open('~/.ssh/id_ed25519.pub').read(),
docker=True)
ip = create('prod-01', server_type='cx22', location='nbg1',
cloud_init=yaml, ssh_keys=['my-laptop'])
# Point DNS at it
dns_record('example.com', 'myapp', ip, proxied=True)
# Sync Compose stack and start
deploy(dc, ip, key='~/.ssh/id_ed25519', path='/srv/myapp')
Podman Support
Set DOCKR_RUNTIME=podman to switch all CLI calls to podman. The
generated Dockerfiles and Compose YAML are runtime-agnostic.
export DOCKR_RUNTIME=podman
Credential-stripping
(_clean_cfg())
is skipped automatically for non-docker runtimes.
Project details
Release history Release notifications | RSS feed
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 fastops-0.0.1.tar.gz.
File metadata
- Download URL: fastops-0.0.1.tar.gz
- Upload date:
- Size: 154.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
67c1821ffab596557d1922dd9081295e4dacda1b782be4966a7d26deca22a716
|
|
| MD5 |
ab0b684c19d77797716b560a81d699fe
|
|
| BLAKE2b-256 |
bb50fee265932032ddc719c8f403608372c0b76978bcaf323b34f53763cea976
|
File details
Details for the file fastops-0.0.1-py3-none-any.whl.
File metadata
- Download URL: fastops-0.0.1-py3-none-any.whl
- Upload date:
- Size: 28.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
346d01075c4531bdcf273ad5e91680878bca76fca2c29035091278852e8d0e10
|
|
| MD5 |
4e8e970ca3ebfdacbb5dca37b4b537e3
|
|
| BLAKE2b-256 |
5c0098f0cc530e1904dfb4d8e28632631d3ed400b4d6168f2b4438681bab1918
|