# 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 ```mermaid flowchart TD Dev["Developer"] -->|push| Gitea["Gitea (git.fsfe.org)
Git repositories"] Gitea -->|webhook| Drone["Drone CI server
(drone.fsfe.org)"] Drone -->|selects runner by
node: label, e.g. cont:test| Runner["Drone runner
on target container server"] Runner --> Reuse["REUSE compliance check"] Reuse --> DeployStep["deploy step
image: docker:29"] DeployStep -->|mounts rootless
Docker socket as volume| Socket["/run/user/1001/docker.sock
on the container server"] DeployStep -->|docker compose down
docker compose pull
docker compose up --build -d| Socket Socket --> Running["Running service container
on the container server"] Running -->|labels: proxy.host,
proxy.port| Caddy["docker2caddy +Caddy
watches Docker events"] Caddy -->|generates vhost,
obtains TLS cert| Public["Public HTTPS endpoint"] Runner --> DocsStep["push-to-docs step
docs-centralizer"] DocsStep -->|SSH, private key
from_secret| DocsSite["docs.fsfe.org"] subgraph Provisioning["Provisioning (separate, out of band)"] Baseline["baseline playbook
(hardening, monitoring, backup)"] ContainerServer["container-server playbook
(rootless Docker daemon)"] DroneAnsible["drone playbook
(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.