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
- A commit, tag, or manual deployment event on a service repository triggers a Gitea webhook to Drone.
- Drone reads
.drone.ymlfrom that repository and dispatches the pipeline to a specific runner, selected via thenode:field (e.g.cont: testpicks the runner labelledcont:test, which runs on that specific container server). - A
reusestep checks license/copyright compliance (fsfe/reuseimage). - The
deploystep runs as adocker:29container. 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 matchingDOCKER_HOST/XDG_RUNTIME_DIRenvironment variables. - Inside that step, the pipeline itself runs
docker compose down,docker compose pull, anddocker 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. - A companion service,
docker2caddy, watches for new/changed containers on each container server and generates Caddy reverse-proxy virtual host configuration automatically, based onproxy.host/proxy.portlabels set in the service'sdocker-compose.yml. Caddy also obtains the TLS certificate. - A separate
push-to-docsstep (independent of the deploy step) pushes any Markdown documentation in the repo todocs.fsfe.orgover SSH, using a secret keyed to a docs-bot private key. - Host provisioning is handled entirely out of band by Ansible playbooks unrelated to Drone itself:
baseline(generic hardening/monitoring/backup applied to most hosts) andcontainer-server(sets up the rootless Docker daemon a container server needs). Thedroneplaybook 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 composecontrol 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.mdexplicitly recommends pairing this with eitherDisable forks(no CI runs from forked branches) orProtected(.drone.ymlmust 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_secretdirectly 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.ymland has that repo marked Trusted can run arbitrarydocker compose/dockercommands 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.ymldirectly 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.