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.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


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.1.tar.gz (154.1 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.1-py3-none-any.whl (28.2 kB view details)

Uploaded Python 3

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

Hashes for fastops-0.0.1.tar.gz
Algorithm Hash digest
SHA256 67c1821ffab596557d1922dd9081295e4dacda1b782be4966a7d26deca22a716
MD5 ab0b684c19d77797716b560a81d699fe
BLAKE2b-256 bb50fee265932032ddc719c8f403608372c0b76978bcaf323b34f53763cea976

See more details on using hashes here.

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

Hashes for fastops-0.0.1-py3-none-any.whl
Algorithm Hash digest
SHA256 346d01075c4531bdcf273ad5e91680878bca76fca2c29035091278852e8d0e10
MD5 4e8e970ca3ebfdacbb5dca37b4b537e3
BLAKE2b-256 5c0098f0cc530e1904dfb4d8e28632631d3ed400b4d6168f2b4438681bab1918

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