Skip to main content

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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 import Dockerfile
from fastops import Compose, appfile
from fastops 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 import dns_record

dns_record('example.com', 'myapp', '1.2.3.4', proxied=True)

Step 4: test locally first (requires Multipass)

from fastops import launch_docker_vm, vm_ip, exec_, transfer, delete
from fastops 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 import vps_init, create, deploy
from fastops 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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

fastops-0.0.2.tar.gz (154.2 kB view details)

Uploaded Source

Built Distribution

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

fastops-0.0.2-py3-none-any.whl (28.3 kB view details)

Uploaded Python 3

File details

Details for the file fastops-0.0.2.tar.gz.

File metadata

  • Download URL: fastops-0.0.2.tar.gz
  • Upload date:
  • Size: 154.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.0

File hashes

Hashes for fastops-0.0.2.tar.gz
Algorithm Hash digest
SHA256 8af4a5d3d32c7d492b384aac94c34bb03198963f0fc607058b5aa9f0a13ddc10
MD5 cc2cff89687640414b78025ed56b0ac8
BLAKE2b-256 0f2eba0da89d9f1c4426cc14333ccd92671268f11bb0c7bb6a65ff9467eaca7f

See more details on using hashes here.

File details

Details for the file fastops-0.0.2-py3-none-any.whl.

File metadata

  • Download URL: fastops-0.0.2-py3-none-any.whl
  • Upload date:
  • Size: 28.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.0

File hashes

Hashes for fastops-0.0.2-py3-none-any.whl
Algorithm Hash digest
SHA256 5b1a26e5cfa2c072286527ea4c98ca7e9bbedd9edac3a36d423d45abf08de342
MD5 7f9b578a7b2412e98a77b488e43f686c
BLAKE2b-256 9de0434a09ff25fb5620220c98bf2b8bd9e1d37dcbf793ce16011892f007d6bc

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