Skip to main content

Idempotent server provisioning: Docker Swarm, Traefik and Cloudflare Tunnel, with rollback, backups and AI-explained alerts.

Project description

forje-cloud

PyPI Python License: MIT Dependencies Platform Status

A production server, from a bare Debian box, in one command.

Containers, HTTPS, backups, monitoring and hardening — provisioned, verified, and documented. Every step checks what already exists and leaves it alone.

pip install forje-cloud
forje-cloud setup

What you get

A server that publishes applications over HTTPS without opening a single inbound port.

Docker Swarm Encrypted secrets, rolling updates, room to add machines.
Traefik Routes hostnames to containers, using labels.
Cloudflare Tunnel A cloudflared container holds one outbound connection that carries all inbound traffic. The firewall stays closed.
Wildcard DNS + TLS *.example.com resolves and is certificated once. Every application deployed afterwards is a routing label and nothing else.
Local image registry Build on the machine, deploy from the machine.
Backups to Cloudflare R2 Volumes, databases and images — off the machine, supervised, with retention.
AI-explained monitoring Logs are classified; anything serious is explained by Claude or GPT and delivered to Telegram.
Hardening Firewall, intrusion blocking, key-only SSH.
Tamper-evident audit log Every entry carries the hash of the one before it.
Self-documenting Every run writes an INFRASTRUCTURE.md describing the machine as it is.

Why

Provisioning scripts assume a clean machine. Real machines are never clean.

Docker is installed, but not in Swarm mode. A reverse proxy is running, but as a plain container. A tunnel was set up months ago and nobody wants to touch it. The deploy user exists but is not in the docker group.

Running a bootstrap script on that machine is a gamble: it either fails on the first step that was already done, or it redoes the step and takes down whatever was working.

forje-cloud checks before it acts. Every step answers "does this already exist, and is it correct?" before touching anything.


How traffic reaches an application

Nothing listens on a public port. A container named cloudflared runs on the host network and opens a single outbound connection to Cloudflare. Inbound requests travel back down that connection.

browser ──HTTPS──> Cloudflare ──tunnel──> cloudflared ──HTTP──> Traefik ──> app
                       │                       │
                  TLS ends here          outbound only,
                                        no port is opened

TLS terminates at Cloudflare. Inside the machine everything speaks plain HTTP, so Traefik never holds a certificate, never contacts a certificate authority, and never needs port 443.

Provisioning wires this together in three parts:

  1. The tunnel is created through the Cloudflare API, and its ingress rules are configured to forward to http://127.0.0.1:80 — the port Traefik publishes on the loopback address. A tunnel with no ingress rules authenticates, reports healthy, and routes nothing; configuring them is what makes it carry traffic.

  2. Two DNS records are created, both pointing at the tunnel: the apex (example.com) and the wildcard (*.example.com). The wildcard is the reason a new application needs no DNS work at all — any subdomain already resolves, and already has a certificate.

  3. The cloudflared container receives the connector token as a root-only file rather than a command-line flag, so docker inspect and ps cannot read it.

Deploying an application afterwards

Once the tunnel and the wildcard exist, publishing a new application is five labels. No DNS record, no certificate, no port.

services:
  myapp:
    image: 127.0.0.1:5000/myapp:20260711-1420
    networks: [edge]
    deploy:
      # Traefik reads these from the Swarm service, not the container.
      labels:
        - "traefik.enable=true"
        - "traefik.swarm.network=edge"
        - "traefik.http.routers.myapp.rule=Host(`myapp.example.com`)"
        - "traefik.http.routers.myapp.entrypoints=web"
        - "traefik.http.services.myapp.loadbalancer.server.port=8000"

networks:
  edge:
    external: true

docker stack deploy, and https://myapp.example.com is live.


Guided setup

forje-cloud setup

Every question explains what the value is for, in one line, without jargon. Every section can be skipped — skipping backups means no backups, not a crash. Nothing is executed until the configuration is shown and confirmed.

── Public access (Cloudflare) ────────────────────────────
   Cloudflare puts your apps online without opening any port on
   this machine. It also provides HTTPS certificates automatically.

 ? Publish this server through Cloudflare? [Y/n]

 ? Domain
   A domain you own, already added to Cloudflare. Example: example.com
   >

The other commands

forje-cloud status    # what this machine already has
forje-cloud plan      # what a run would change, without changing it
forje-cloud apply     # provision from flags and environment, no prompts
forje-cloud restore   # bring back volumes, databases and images
forje-cloud compose   # reconstruct a Compose file for every running workload
forje-cloud verify    # has the audit log been tampered with?
forje-cloud docs      # regenerate the infrastructure document

A run on an already-provisioned machine:

 • hostname        already 'example-host'
 • deploy-user     account 'deploy' exists
 • docker          Docker version 28.0.0
 ✔ docker-group    added 'deploy' to the docker group
 ✔ swarm           initialized (advertise address 192.0.2.10)
 • network         overlay 'edge' exists
 ▲ traefik         running traefik:v2.11, expected traefik:v3.3

already present · configured now · exists but diverges


Drift is not absence

Most tools treat "wrong" and "missing" as the same thing, and overwrite both.

They are not the same thing. Traefik v2 and v3 read different label syntax: v3 uses a dedicated swarm provider and reads traefik.swarm.network, where v2 used docker with swarmMode: true and read traefik.docker.network. The two are not interchangeable — v3 labels on a v2 proxy are ignored, which leaves an application running but unrouted, with no error anywhere.

So a proxy on the wrong major version is not a gap to fill. It is a running system whose every routing label would have to be rewritten at once. That is a migration, and a migration is a decision.

forje-cloud reports drift and stops.


Backups

A backup stored on the machine it protects is not a backup. Archives are uploaded to Cloudflare R2, off the machine.

Three things are captured, because restoring any two of them is not enough:

  • Volumes — archived with zstd, which is faster and smaller than gzip.
  • Databases — dumped with each engine's own tool. Postgres, MySQL, MariaDB, MongoDB and Redis are recognised by image family, so postgres:16-alpine and pgvector/pgvector are both handled. Copying live database files instead can catch them mid-write, producing an archive that will not restore.
  • Images — the local registry. Without them, a rebuilt machine has stack files referencing images that exist nowhere, and every application must be rebuilt from source before it can start.

The service is supervised: a failed cycle is retried, and if the process dies Swarm restarts it. The schedule is configurable — daily by default, with retention in days.

Every backup carries a manifest naming what it holds, so restore reads it rather than inferring intent from filenames.

Restoring

forje-cloud restore

Pick a backup, then pick what to bring back — by number, by range, or all. Recovering a machine that ran eight workloads means dozens of volumes, and restoring them one command at a time is where mistakes happen.

The Compose file nobody kept

forje-cloud compose

Servers accumulate stacks deployed from files that were never committed anywhere. This reads the running services back out of Swarm and reconstructs a single Compose file covering every workload — images, volumes, networks, secrets, routing labels.

A copy is written into every backup. After a total loss: restore the archives, deploy the file, and the machine is back.


Monitoring

Container logs are read on a schedule. Lines are matched against patterns and scored for severity — out-of-memory kills, disk exhaustion, crash loops, certificate failures, authentication attempts.

Only findings above the threshold are escalated. Those go to Claude or GPT, which answers three questions: what happened, what likely caused it, and what to do about it. The answer arrives on Telegram.

Raw logs never leave the machine. Only the classified findings are sent, which are smaller, cheaper, and do not carry whatever a log line happened to contain.

Identical alerts are suppressed for an hour, so one incident produces one alert rather than fifty.


An audit log you can trust

Every entry carries the hash of the entry before it.

$ forje-cloud verify
✖ audit log   entry #4 (step=firewall) was modified: hash does not match

Editing or deleting any past record breaks the chain. This does not prevent tampering; it makes tampering evident.


It documents itself

Every run writes INFRASTRUCTURE.md: a diagram of how traffic reaches an application, what is running, what an application must declare to be deployed here, and what must never appear in its Compose file.

Written from the results of the run, so it describes the machine as it is.


Safety

Closing SSH requires a working tunnel. The step refuses otherwise. Closing the only way in, with no working alternative, cannot be undone — the channel needed to revert is the one being closed.

Destructive steps require confirmation. Always explicit, never implied.

Credentials are never written down. The sudo password is passed on stdin, never in argv (visible through ps) and never in the environment (visible through /proc). Service credentials become Docker secrets, encrypted at rest. Nothing is written to the audit log or the generated document.

Sudo is scoped. The deploy account gets NOPASSWD for an enumerated list of commands, never ALL.

Configuration is validated before it is loaded. sshd -t and visudo -cf run first. An invalid file is reverted, not applied.


Requirements

  • A Debian-based Linux machine — Debian or Ubuntu. VPS or bare metal.
  • Python 3.9 or newer.
  • Root, or an account with sudo.
  • Optional, for public access: a domain on Cloudflare.
  • Optional, for backups: a Cloudflare R2 bucket.
  • Optional, for alerts: a Telegram bot and an Anthropic or OpenAI API key.

No dependencies

Standard library only. A tool that provisions a machine should not require the machine to be provisioned first.


Status of this release

v0.1.0 is published for review, not for production use.

It ships with a passing test suite and code that is complete and, as far as the suite reaches, correct. What it has not yet had is a full run against a real VPS, from a clean install through to a working tunnel. Until that has happened, calling it ready would be a claim this release cannot support.

So it is published as what it is: a first cut, open for inspection, while validation on real hardware is underway.

v0.1.1 will be the first release intended for real use. It follows shortly.

Read the code. Run the tests. Send corrections. Just don't point it at a machine that matters yet.


Contributing

Contributions are welcome and actively wanted — corrections, criticism of the design, or an account of what happened when the tool met a real machine.

A public repository is not yet available, so the way in is email:

Patches, findings and hard questions all land in the same place.


License

MIT © Matheus Rodrigues Trindade

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

forje_cloud-0.1.0.tar.gz (59.4 kB view details)

Uploaded Source

Built Distribution

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

forje_cloud-0.1.0-py3-none-any.whl (66.2 kB view details)

Uploaded Python 3

File details

Details for the file forje_cloud-0.1.0.tar.gz.

File metadata

  • Download URL: forje_cloud-0.1.0.tar.gz
  • Upload date:
  • Size: 59.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.3

File hashes

Hashes for forje_cloud-0.1.0.tar.gz
Algorithm Hash digest
SHA256 4a1d13feb5398633fd5f0ddd08b88dd6e98e6d24eafb166e298be013816f311c
MD5 221962d2686749476f25100a7447f4b1
BLAKE2b-256 19e1e88392ef095150ba1528e472afb669d2b778db17cf524ab9b4eb21bfcca8

See more details on using hashes here.

File details

Details for the file forje_cloud-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: forje_cloud-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 66.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.3

File hashes

Hashes for forje_cloud-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 093b8f06925414b7cbffcf8a3fc2c3ff64bfaee80dddef71c9916aed360e7bc9
MD5 76f8efaad245ca4ff5a1e7fbb4f52c56
BLAKE2b-256 9dc2b3ddb412137abae93e3812ed64ebab5473217a4f4a9435a7c0001d9aea5e

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