Files
repo2cicd2deploy/fsfe-current-deployment.md

9.0 KiB

FSFE's current deployment architecture (Drone + Docker socket)

This documents the deployment mechanism actually in use by the FSFE System Hackers today, for comparison with the Woodpecker-based concept and PoC in this repository. Sources: fsfe-system-hackers/minimal-docker (docker-deployments.md, .drone.yml, docker-compose.yml), fsfe-system-hackers/drone, and fsfe-system-hackers/baseline.

Note: minimal-docker also contains a .woodpecker.yml, apparently from FSFE's own separate, in-progress exploration of migrating their CI engine from Drone to Woodpecker. That file is a like-for-like port of the same Docker-socket-mounting pattern described below onto Woodpecker as the runner — it does not adopt the build/deploy separation, Zot registry, or SOPS-encrypted-secrets design from concept.md/plan.md in this repository. The two efforts are independent; this document describes FSFE's actual current (Drone) architecture, not their Woodpecker migration.

Architecture

flowchart TD
    Dev["Developer"] -->|push| Gitea["Gitea (git.fsfe.org)<br/>Git repositories"]

    Gitea -->|webhook| Drone["Drone CI server<br/>(drone.fsfe.org)"]

    Drone -->|selects runner by<br/>node: label, e.g. cont:test| Runner["Drone runner<br/>on target container server"]

    Runner --> Reuse["REUSE compliance check"]
    Reuse --> DeployStep["deploy step<br/>image: docker:29"]

    DeployStep -->|mounts rootless<br/>Docker socket as volume| Socket["/run/user/1001/docker.sock<br/>on the container server"]

    DeployStep -->|docker compose down<br/>docker compose pull<br/>docker compose up --build -d| Socket

    Socket --> Running["Running service container<br/>on the container server"]

    Running -->|labels: proxy.host,<br/>proxy.port| Caddy["docker2caddy +Caddy<br/>watches Docker events"]
    Caddy -->|generates vhost,<br/>obtains TLS cert| Public["Public HTTPS endpoint"]

    Runner --> DocsStep["push-to-docs step<br/>docs-centralizer"]
    DocsStep -->|SSH, private key<br/>from_secret| DocsSite["docs.fsfe.org"]

    subgraph Provisioning["Provisioning (separate, out of band)"]
        Baseline["baseline playbook<br/>(hardening, monitoring, backup)"]
        ContainerServer["container-server playbook<br/>(rootless Docker daemon)"]
        DroneAnsible["drone playbook<br/>(Drone server + runners)"]
    end

    Baseline -.->|provisions| Socket
    ContainerServer -.->|provisions| Socket
    DroneAnsible -.->|provisions| Drone

How it works, step by step

  1. A commit, tag, or manual deployment event on a service repository triggers a Gitea webhook to Drone.
  2. Drone reads .drone.yml from that repository and dispatches the pipeline to a specific runner, selected via the node: field (e.g. cont: test picks the runner labelled cont:test, which runs on that specific container server).
  3. A reuse step checks license/copyright compliance (fsfe/reuse image).
  4. The deploy step runs as a docker:29 container. It has the rootless Docker socket of the target container server bind-mounted directly into the build container (/run/user/1001/docker.sock), plus the matching DOCKER_HOST/XDG_RUNTIME_DIR environment variables.
  5. Inside that step, the pipeline itself runs docker compose down, docker compose pull, and docker compose up --build -d. This builds the image (if needed) and starts the container directly against the container server's Docker daemon, from inside the CI job.
  6. A companion service, docker2caddy, watches for new/changed containers on each container server and generates Caddy reverse-proxy virtual host configuration automatically, based on proxy.host/proxy.port labels set in the service's docker-compose.yml. Caddy also obtains the TLS certificate.
  7. A separate push-to-docs step (independent of the deploy step) pushes any Markdown documentation in the repo to docs.fsfe.org over SSH, using a secret keyed to a docs-bot private key.
  8. Host provisioning is handled entirely out of band by Ansible playbooks unrelated to Drone itself: baseline (generic hardening/monitoring/backup applied to most hosts) and container-server (sets up the rootless Docker daemon a container server needs). The drone playbook provisions the Drone server and its runners, each runner labelled to match a specific container server.

Trust model

  • The CI job has direct, first-class access to the target host's Docker daemon. There is no separation between "build" and "deploy" — the same pipeline step that might build an image is also the one wielding full docker compose control over the production host, because the socket is mounted straight into the build container.
  • Repositories must be marked "Trusted" in Drone to be allowed to mount the Docker socket at all. docker-deployments.md explicitly recommends pairing this with either Disable forks (no CI runs from forked branches) or Protected (.drone.yml must be cryptographically signed, or the run needs manual approval) — both are optional hardening choices left to each repository's maintainers, not enforced platform-wide.
  • Secrets are plain Drone secrets, configured per-repository in Drone's UI and referenced with from_secret directly in .drone.yml, typically injected as Docker build arguments and/or Compose environment variables. There is no separate encryption-at-rest mechanism for repository-committed secrets; the secret's plaintext only exists inside Drone's own secret store and the running container's environment.
  • No separate deployment-only credential or repository exists. Whoever can edit a service's .drone.yml and has that repo marked Trusted can run arbitrary docker compose/docker commands against that specific container server's Docker daemon, since the daemon itself is the shared resource being reached into, not a narrow, purpose-built deployment API.
  • Docker host selection is a Drone-native runner label (node: cont: test), not a request parameter validated against an allow-list; a repository's .drone.yml directly names the runner/host it wants to run on.

Concise comparison with this repository's Woodpecker/Zot/SOPS concept

Aspect FSFE today (Drone) This repo's concept.md / PoC (Woodpecker)
Build vs. deploy separation None — one pipeline step builds and deploys via a mounted Docker socket Separate: service pipeline only builds and publishes; a distinct central pipeline deploys
Docker socket exposure to CI Direct bind-mount of the target host's rootless Docker socket into the build container Never exposed; the central deploy pipeline reaches the target only via restricted SSH
Image/artifact registry None — images are built directly on the target host by docker compose up --build Zot (OCI registry with retention policy); images and deployment bundles are pushed as immutable, digest-addressable artifacts
Secret storage Plain per-repository Drone secrets, referenced directly via from_secret Repository-committed, SOPS/age-encrypted secrets.prod.env; only the central deploy pipeline holds the decryption key
Production host access control Implicit: whoever can edit a Trusted repo's .drone.yml can run any Docker command on that host Centralized: only the deploy pipeline holds SSH/registry/decryption credentials; service repos hold none of them
Host selection Runner label chosen directly in .drone.yml (node:) Logical alias (TARGET) resolved against a fixed, central hosts/ allow-list; unknown aliases fail closed
New-service onboarding New repo + Drone "Trusted" flag + optional fork/signature protections, still no built-in cross-repo isolation New repo only; central deployment repository needs no change per service
Reverse proxy / TLS docker2caddy watches Docker events and generates Caddy vhosts automatically from container labels Out of scope for the PoC; concept assumes an existing reverse-proxy/TLS setup per production host
Hardening already in place Optional per-repo Disable forks/Protected signing, rootless Docker daemon Restricted SSH (forced command), rootless Docker, per-repo target allow-list — all explicitly deferred, not yet implemented

Summary

FSFE's current Drone-based setup is simpler to reason about and has fewer moving parts, but it concentrates a large amount of trust in each service repository: a compromised or careless .drone.yml has direct, unmediated control over its container server's Docker daemon, and there is no registry or artifact layer between "build" and "running in production" at all — docker compose up --build does both at once, on the production host itself. The concept.md/PoC design trades that simplicity for an explicit trust boundary (build pipelines never hold production credentials) and an artifact layer (Zot) that makes a deployment a specific, immutable, previously-built thing rather than "whatever the build step produces this time" — at the cost of more infrastructure (a registry, a second pipeline, SOPS key management) and, as the PoC's bug list shows, real integration friction to get working reliably.