Fossil SCM
Moved the sectio about elaborating the container runtime layer down into the section of the doc where we talk about other customizations. Its prior location was because it was a tangent off a prior point, but it's just as easy to jump down via hyperlink. Assorted other small improvements while in there.
Commit
301d4f21bcd2260024f70f9075cb5d1d991a7983cc276550a082ee72fdcc38bc
Parent
4ed98a9944814a4…
1 file changed
+159
-163
+159
-163
| --- www/containers.md | ||
| +++ www/containers.md | ||
| @@ -69,11 +69,11 @@ | ||
| 69 | 69 | The wrong way is to use the `Dockerfile COPY` command, because by baking |
| 70 | 70 | the repo into the image at build time, it will become one of the image’s |
| 71 | 71 | base layers. The end result is that each time you build a container from |
| 72 | 72 | that image, the repo will be reset to its build-time state. Worse, |
| 73 | 73 | restarting the container will do the same thing, since the base image |
| 74 | -layers are immutable in Docker. This is almost certainly not what you | |
| 74 | +layers are immutable. This is almost certainly not what you | |
| 75 | 75 | want. |
| 76 | 76 | |
| 77 | 77 | The correct ways put the repo into the _container_ created from the |
| 78 | 78 | _image_, not in the image itself. |
| 79 | 79 | |
| @@ -114,11 +114,11 @@ | ||
| 114 | 114 | this with `chmod`: simply reload the browser, and Fossil will try again. |
| 115 | 115 | |
| 116 | 116 | |
| 117 | 117 | ### 2.2 <a id="bind-mount"></a>Storing the Repo Outside the Container |
| 118 | 118 | |
| 119 | -The simple storage method above has a problem: Docker containers are | |
| 119 | +The simple storage method above has a problem: containers are | |
| 120 | 120 | designed to be killed off at the slightest cause, rebuilt, and |
| 121 | 121 | redeployed. If you do that with the repo inside the container, it gets |
| 122 | 122 | destroyed, too. The solution is to replace the “run” command above with |
| 123 | 123 | the following: |
| 124 | 124 | |
| @@ -133,12 +133,12 @@ | ||
| 133 | 133 | Because this bind mount maps a host-side directory (`~/museum`) into the |
| 134 | 134 | container, you don’t need to `docker cp` the repo into the container at |
| 135 | 135 | all. It still expects to find the repository as `repo.fossil` under that |
| 136 | 136 | directory, but now both the host and the container can see that repo DB. |
| 137 | 137 | |
| 138 | -Instead of a bind mount, you could instead set up a separate [Docker | |
| 139 | -volume](https://docs.docker.com/storage/volumes/), at which point you | |
| 138 | +Instead of a bind mount, you could instead set up a separate | |
| 139 | +[volume](https://docs.docker.com/storage/volumes/), at which point you | |
| 140 | 140 | _would_ need to `docker cp` the repo file into the container. |
| 141 | 141 | |
| 142 | 142 | Either way, files in these mounted directories have a lifetime |
| 143 | 143 | independent of the container(s) they’re mounted into. When you need to |
| 144 | 144 | rebuild the container or its underlying image — such as to upgrade to a |
| @@ -183,152 +183,25 @@ | ||
| 183 | 183 | |
| 184 | 184 | ## 3. <a id="security"></a>Security |
| 185 | 185 | |
| 186 | 186 | ### 3.1 <a id="chroot"></a>Why Not Chroot? |
| 187 | 187 | |
| 188 | -Prior to 2023.03.26, the stock Fossil container made use of [the chroot | |
| 189 | -jail feature](./chroot.md) in order to wall away the shell and other | |
| 190 | -tools provided by [BusyBox](https://www.busybox.net/BusyBox.html). This | |
| 191 | -author made a living for years in the early 1990s using Unix systems | |
| 192 | -that offered less power, so there was a legitimate worry that if someone | |
| 193 | -ever figured out how to get a shell on one of these Fossil containers, | |
| 194 | -it would constitute a powerful island from which to attack the rest of | |
| 195 | -the network. | |
| 196 | - | |
| 197 | -The thing is, Fossil is self-contained, needing none of that power in | |
| 198 | -the main-line use cases. The only reason we included BusyBox in the | |
| 199 | -container at all was on the off chance that someone needed it for | |
| 200 | -debugging. | |
| 201 | - | |
| 202 | -That justification collapsed when we realized you could restore this | |
| 203 | -basic shell environment on an as-needed basis with a one-line change to | |
| 204 | -the `Dockerfile`, as we show in the next section. | |
| 205 | - | |
| 206 | - | |
| 207 | -### 3.2 <a id="run"></a>Swapping Out the Run Layer | |
| 208 | - | |
| 209 | -If you want a basic shell environment for temporary debugging of the | |
| 210 | -running container, that’s easily added. Simply change this line in the | |
| 211 | -`Dockerfile`… | |
| 212 | - | |
| 213 | - FROM scratch AS run | |
| 214 | - | |
| 215 | -…to this: | |
| 216 | - | |
| 217 | - FROM busybox AS run | |
| 218 | - | |
| 219 | -Rebuild, redeploy, and your Fossil container now has a BusyBox based | |
| 220 | -shell environment that you can get into via: | |
| 221 | - | |
| 222 | - $ docker exec -it -u fossil $(make container-version) sh | |
| 223 | - | |
| 224 | -(That command assumes you built the container via “`make container`” and | |
| 225 | -are therefore using its versioning scheme.) | |
| 226 | - | |
| 227 | -Another case where you might need to replace this bare-bones “`run`” | |
| 228 | -layer with something more functional is that you’re setting up [email | |
| 229 | -alerts](./alerts.md) and need some way to integrate with the host’s | |
| 230 | -[MTA]. There are a number of alternatives in that linked document, so | |
| 231 | -for the sake of discussion, we’ll say you’ve chosen [Method | |
| 232 | -2](./alerts.md#db), which requires a Tcl interpreter and its SQLite | |
| 233 | -extension to push messages into the outbound email queue DB, presumably | |
| 234 | -bind-mounted into the container. | |
| 235 | - | |
| 236 | -You can do that by replacing STAGEs 2 and 3 in the stock `Dockerfile` | |
| 237 | -with this: | |
| 238 | - | |
| 239 | -``` | |
| 240 | - ## --------------------------------------------------------------------- | |
| 241 | - ## STAGE 2: Pare that back to the bare essentials, plus Tcl. | |
| 242 | - ## --------------------------------------------------------------------- | |
| 243 | - FROM alpine AS run | |
| 244 | - ARG UID=499 | |
| 245 | - ENV PATH "/sbin:/usr/sbin:/bin:/usr/bin" | |
| 246 | - COPY --from=builder /tmp/fossil /bin/ | |
| 247 | - COPY tools/email-sender.tcl /bin/ | |
| 248 | - RUN set -x \ | |
| 249 | - && echo "fossil:x:${UID}:${UID}:User:/museum:/false" >> /etc/passwd \ | |
| 250 | - && echo "fossil:x:${UID}:fossil" >> /etc/group \ | |
| 251 | - && install -d -m 700 -o fossil -g fossil log museum \ | |
| 252 | - && apk add --no-cache tcl sqlite-tcl | |
| 253 | -``` | |
| 254 | - | |
| 255 | -Build it and test that it works like so: | |
| 256 | - | |
| 257 | -``` | |
| 258 | - $ make container-run && | |
| 259 | - echo 'puts [info patchlevel]' | | |
| 260 | - docker exec -i $(make container-version) tclsh | |
| 261 | - 8.6.12 | |
| 262 | -``` | |
| 263 | - | |
| 264 | -You should remove the `PATH` override in the “RUN” | |
| 265 | -stage, since it’s written for the case where everything is in `/bin`. | |
| 266 | -With these additions, we need the longer `PATH` shown above to have | |
| 267 | -ready access to them all. | |
| 268 | - | |
| 269 | -Another useful case to consider is that you’ve installed a [server | |
| 270 | -extension](./serverext.wiki) and you need an interpreter for that | |
| 271 | -script. The first option above won’t work except in the unlikely case that | |
| 272 | -it’s written for one of the bare-bones script interpreters that BusyBox | |
| 273 | -ships.(^BusyBox’s `/bin/sh` is based on the old 4.4BSD Lite Almquist | |
| 274 | -shell, implementing little more than what POSIX specified in 1989, plus | |
| 275 | -equally stripped-down versions of `awk` and `sed`.) | |
| 276 | - | |
| 277 | -Let’s say the extension is written in Python. While you could handle it | |
| 278 | -the same way we do with the Tcl example above, Python is more | |
| 279 | -popular, giving us more options. Let’s inject a Python environment into | |
| 280 | -the stock Fossil container via a suitable “[distroless]” image instead: | |
| 281 | - | |
| 282 | -``` | |
| 283 | - ## --------------------------------------------------------------------- | |
| 284 | - ## STAGE 2: Pare that back to the bare essentials, plus Python. | |
| 285 | - ## --------------------------------------------------------------------- | |
| 286 | - FROM cgr.dev/chainguard/python:latest | |
| 287 | - USER root | |
| 288 | - ARG UID=499 | |
| 289 | - ENV PATH "/sbin:/usr/sbin:/bin:/usr/bin" | |
| 290 | - COPY --from=builder /tmp/fossil /bin/ | |
| 291 | - COPY --from=builder /bin/busybox.static /bin/busybox | |
| 292 | - RUN [ "/bin/busybox", "--install", "/bin" ] | |
| 293 | - RUN set -x \ | |
| 294 | - && echo "fossil:x:${UID}:${UID}:User:/museum:/false" >> /etc/passwd \ | |
| 295 | - && echo "fossil:x:${UID}:fossil" >> /etc/group \ | |
| 296 | - && install -d -m 700 -o fossil -g fossil log museum | |
| 297 | -``` | |
| 298 | - | |
| 299 | -You will also have to add `busybox-static` to the APK package list in | |
| 300 | -STAGE 1 for the `RUN` script at the end of that stage to work, since the | |
| 301 | -[Chainguard Python image][cgimgs] lacks a shell, on purpose. The need to | |
| 302 | -install root-level binaries is why we change `USER` temporarily here. | |
| 303 | - | |
| 304 | -Build it and test that it works like so: | |
| 305 | - | |
| 306 | -``` | |
| 307 | - $ make container-run && | |
| 308 | - docker exec -i $(make container-version) python --version | |
| 309 | - 3.11.2 | |
| 310 | -``` | |
| 311 | - | |
| 312 | -The compensation for the hassle of using Chainguard over something more | |
| 313 | -general purpose like Alpine + “`apk add python`” | |
| 314 | -is huge: we no longer leave a package manager sitting around inside the | |
| 315 | -container, waiting for some malefactor to figure out how to abuse it. | |
| 316 | - | |
| 317 | -Beware that there’s a limit to this über-jail’s ability to save you when | |
| 318 | -you go and provide a more capable OS layer like this. The container | |
| 319 | -layer should stop an attacker from accessing any files out on the host | |
| 320 | -that you haven’t explicitly mounted into the container’s namespace, but | |
| 321 | -it can’t stop them from making outbound network connections or modifying | |
| 322 | -the repo DB inside the container. | |
| 323 | - | |
| 324 | -[cgimgs]: https://github.com/chainguard-images/images/tree/main/images | |
| 325 | -[distroless]: https://www.chainguard.dev/unchained/minimal-container-images-towards-a-more-secure-future | |
| 326 | -[MTA]: https://en.wikipedia.org/wiki/Message_transfer_agent | |
| 327 | - | |
| 328 | - | |
| 329 | -### 3.3 <a id="caps"></a>Dropping Unnecessary Capabilities | |
| 188 | +Prior to 2023.03.26, the stock Fossil container relied on [the chroot | |
| 189 | +jail feature](./chroot.md) to wall away the shell and other tools | |
| 190 | +provided by [BusyBox]. It included that as a bare-bones operating system | |
| 191 | +inside the container on the off chance that someone might need it for | |
| 192 | +debugging, but the thing is, Fossil is self-contained, needing none of | |
| 193 | +that power in the main-line use cases. | |
| 194 | + | |
| 195 | +Our weak “you might need it” justification collapsed when we realized | |
| 196 | +you could restore this basic shell environment with a one-line change to | |
| 197 | +the `Dockerfile`, as shown [below](#run). | |
| 198 | + | |
| 199 | +[BusyBox]: https://www.busybox.net/BusyBox.html | |
| 200 | + | |
| 201 | + | |
| 202 | +### 3.2 <a id="caps"></a>Dropping Unnecessary Capabilities | |
| 330 | 203 | |
| 331 | 204 | The example commands above create the container with [a default set of |
| 332 | 205 | Linux kernel capabilities][defcap]. Although Docker strips away almost |
| 333 | 206 | all of the traditional root capabilities by default, and Fossil doesn’t |
| 334 | 207 | need any of those it does take away, Docker does leave some enabled that |
| @@ -443,12 +316,12 @@ | ||
| 443 | 316 | |
| 444 | 317 | |
| 445 | 318 | ## 4. <a id="static"></a>Extracting a Static Binary |
| 446 | 319 | |
| 447 | 320 | Our 2-stage build process uses Alpine Linux only as a build host. Once |
| 448 | -we’ve got everything reduced to the two key static binaries — Fossil and | |
| 449 | -BusyBox — we throw all the rest of it away. | |
| 321 | +we’ve got everything reduced to a single static Fossil binary, | |
| 322 | +we throw all the rest of it away. | |
| 450 | 323 | |
| 451 | 324 | A secondary benefit falls out of this process for free: it’s arguably |
| 452 | 325 | the easiest way to build a purely static Fossil binary for Linux. Most |
| 453 | 326 | modern Linux distros make this [surprisingly difficult][lsl], but Alpine’s |
| 454 | 327 | back-to-basics nature makes static builds work the way they used to, |
| @@ -466,11 +339,11 @@ | ||
| 466 | 339 | at about 6 MiB. (It’s built stripped.) |
| 467 | 340 | |
| 468 | 341 | [lsl]: https://stackoverflow.com/questions/3430400/linux-static-linking-is-dead |
| 469 | 342 | |
| 470 | 343 | |
| 471 | -## 5. <a id="args"></a>Container Build Arguments | |
| 344 | +## 5. <a id="custom" name="args"></a>Customization Points | |
| 472 | 345 | |
| 473 | 346 | ### <a id="pkg-vers"></a> 5.1 Fossil Version |
| 474 | 347 | |
| 475 | 348 | The default version of Fossil fetched in the build is the version in the |
| 476 | 349 | checkout directory at the time you run it. You could override it to get |
| @@ -486,21 +359,21 @@ | ||
| 486 | 359 | $ make container-image DBFLAGS='--build-arg FSLVER=version-2.20' |
| 487 | 360 | ``` |
| 488 | 361 | |
| 489 | 362 | While you could instead use the generic |
| 490 | 363 | “`release`” tag here, it’s better to use a specific version number |
| 491 | -since Docker caches downloaded files and tries to | |
| 364 | +since container builders cache downloaded files, hoping to | |
| 492 | 365 | reuse them across builds. If you ask for “`release`” before a new |
| 493 | 366 | version is tagged and then immediately after, you might expect to get |
| 494 | 367 | two different tarballs, but because the underlying source tarball URL |
| 495 | 368 | remains the same when you do that, you’ll end up reusing the |
| 496 | -old tarball from your Docker cache. This will occur | |
| 369 | +old tarball from cache. This will occur | |
| 497 | 370 | even if you pass the “`docker build --no-cache`” option. |
| 498 | 371 | |
| 499 | 372 | This is why we default to pulling the Fossil tarball by checkin ID |
| 500 | 373 | rather than let it default to the generic “`trunk`” tag: so the URL will |
| 501 | -change each time you update your Fossil source tree, forcing Docker to | |
| 374 | +change each time you update your Fossil source tree, forcing the builder to | |
| 502 | 375 | pull a fresh tarball. |
| 503 | 376 | |
| 504 | 377 | |
| 505 | 378 | ### 5.2 <a id="uids"></a>User & Group IDs |
| 506 | 379 | |
| @@ -517,11 +390,11 @@ | ||
| 517 | 390 | $ make container-image \ |
| 518 | 391 | DBFLAGS='--build-arg UID=501' |
| 519 | 392 | ``` |
| 520 | 393 | |
| 521 | 394 | This is particularly useful if you’re putting your repository on a |
| 522 | -Docker volume since the IDs “leak” out into the host environment via | |
| 395 | +separate volume since the IDs “leak” out into the host environment via | |
| 523 | 396 | file permissions. You may therefore wish them to mean something on both |
| 524 | 397 | sides of the container barrier rather than have “499” appear on the host |
| 525 | 398 | in “`ls -l`” output. |
| 526 | 399 | |
| 527 | 400 | |
| @@ -557,10 +430,132 @@ | ||
| 557 | 430 | |
| 558 | 431 | ``` |
| 559 | 432 | $ make CENGINE=podman container-run |
| 560 | 433 | ``` |
| 561 | 434 | |
| 435 | + | |
| 436 | +### 5.3 <a id="run"></a>Elaborating the Run Layer | |
| 437 | + | |
| 438 | +If you want a basic shell environment for temporary debugging of the | |
| 439 | +running container, that’s easily added. Simply change this line in the | |
| 440 | +`Dockerfile`… | |
| 441 | + | |
| 442 | + FROM scratch AS run | |
| 443 | + | |
| 444 | +…to this: | |
| 445 | + | |
| 446 | + FROM busybox AS run | |
| 447 | + | |
| 448 | +Rebuild, redeploy, and your Fossil container will have a [BusyBox]-based | |
| 449 | +shell environment that you can get into via: | |
| 450 | + | |
| 451 | + $ docker exec -it -u fossil $(make container-version) sh | |
| 452 | + | |
| 453 | +(That command assumes you built it via “`make container`” and are | |
| 454 | +therefore using its versioning scheme.) | |
| 455 | + | |
| 456 | +Another case where you might need to replace this bare-bones “`run`” | |
| 457 | +layer with something more functional is that you’re setting up [email | |
| 458 | +alerts](./alerts.md) and need some way to integrate with the host’s | |
| 459 | +[MTA]. There are a number of alternatives in that linked document, so | |
| 460 | +for the sake of discussion, we’ll say you’ve chosen [Method | |
| 461 | +2](./alerts.md#db), which requires a Tcl interpreter and its SQLite | |
| 462 | +extension to push messages into the outbound email queue DB, presumably | |
| 463 | +bind-mounted into the container. | |
| 464 | + | |
| 465 | +You can do that by replacing STAGEs 2 and 3 in the stock `Dockerfile` | |
| 466 | +with this: | |
| 467 | + | |
| 468 | +``` | |
| 469 | + ## --------------------------------------------------------------------- | |
| 470 | + ## STAGE 2: Pare that back to the bare essentials, plus Tcl. | |
| 471 | + ## --------------------------------------------------------------------- | |
| 472 | + FROM alpine AS run | |
| 473 | + ARG UID=499 | |
| 474 | + ENV PATH "/sbin:/usr/sbin:/bin:/usr/bin" | |
| 475 | + COPY --from=builder /tmp/fossil /bin/ | |
| 476 | + COPY tools/email-sender.tcl /bin/ | |
| 477 | + RUN set -x \ | |
| 478 | + && echo "fossil:x:${UID}:${UID}:User:/museum:/false" >> /etc/passwd \ | |
| 479 | + && echo "fossil:x:${UID}:fossil" >> /etc/group \ | |
| 480 | + && install -d -m 700 -o fossil -g fossil log museum \ | |
| 481 | + && apk add --no-cache tcl sqlite-tcl | |
| 482 | +``` | |
| 483 | + | |
| 484 | +Build it and test that it works like so: | |
| 485 | + | |
| 486 | +``` | |
| 487 | + $ make container-run && | |
| 488 | + echo 'puts [info patchlevel]' | | |
| 489 | + docker exec -i $(make container-version) tclsh | |
| 490 | + 8.6.12 | |
| 491 | +``` | |
| 492 | + | |
| 493 | +You should remove the `PATH` override in the “RUN” | |
| 494 | +stage, since it’s written for the case where everything is in `/bin`. | |
| 495 | +With these additions, we need the longer `PATH` shown above to have | |
| 496 | +ready access to them all. | |
| 497 | + | |
| 498 | +Another useful case to consider is that you’ve installed a [server | |
| 499 | +extension](./serverext.wiki) and you need an interpreter for that | |
| 500 | +script. The first option above won’t work except in the unlikely case that | |
| 501 | +it’s written for one of the bare-bones script interpreters that BusyBox | |
| 502 | +ships.(^[BusyBox]’s `/bin/sh` is based on the old 4.4BSD Lite Almquist | |
| 503 | +shell, implementing little more than what POSIX specified in 1989, plus | |
| 504 | +equally stripped-down versions of `awk` and `sed`.) | |
| 505 | + | |
| 506 | +Let’s say the extension is written in Python. While you could handle it | |
| 507 | +the same way we do with the Tcl example above, Python is more | |
| 508 | +popular, giving us more options. Let’s inject a Python environment into | |
| 509 | +the stock Fossil container via a suitable “[distroless]” image instead: | |
| 510 | + | |
| 511 | +``` | |
| 512 | + ## --------------------------------------------------------------------- | |
| 513 | + ## STAGE 2: Pare that back to the bare essentials, plus Python. | |
| 514 | + ## --------------------------------------------------------------------- | |
| 515 | + FROM cgr.dev/chainguard/python:latest | |
| 516 | + USER root | |
| 517 | + ARG UID=499 | |
| 518 | + ENV PATH "/sbin:/usr/sbin:/bin:/usr/bin" | |
| 519 | + COPY --from=builder /tmp/fossil /bin/ | |
| 520 | + COPY --from=builder /bin/busybox.static /bin/busybox | |
| 521 | + RUN [ "/bin/busybox", "--install", "/bin" ] | |
| 522 | + RUN set -x \ | |
| 523 | + && echo "fossil:x:${UID}:${UID}:User:/museum:/false" >> /etc/passwd \ | |
| 524 | + && echo "fossil:x:${UID}:fossil" >> /etc/group \ | |
| 525 | + && install -d -m 700 -o fossil -g fossil log museum | |
| 526 | +``` | |
| 527 | + | |
| 528 | +You will also have to add `busybox-static` to the APK package list in | |
| 529 | +STAGE 1 for the `RUN` script at the end of that stage to work, since the | |
| 530 | +[Chainguard Python image][cgimgs] lacks a shell, on purpose. The need to | |
| 531 | +install root-level binaries is why we change `USER` temporarily here. | |
| 532 | + | |
| 533 | +Build it and test that it works like so: | |
| 534 | + | |
| 535 | +``` | |
| 536 | + $ make container-run && | |
| 537 | + docker exec -i $(make container-version) python --version | |
| 538 | + 3.11.2 | |
| 539 | +``` | |
| 540 | + | |
| 541 | +The compensation for the hassle of using Chainguard over something more | |
| 542 | +general purpose like Alpine + “`apk add python`” | |
| 543 | +is huge: we no longer leave a package manager sitting around inside the | |
| 544 | +container, waiting for some malefactor to figure out how to abuse it. | |
| 545 | + | |
| 546 | +Beware that there’s a limit to this über-jail’s ability to save you when | |
| 547 | +you go and provide a more capable OS layer like this. The container | |
| 548 | +layer should stop an attacker from accessing any files out on the host | |
| 549 | +that you haven’t explicitly mounted into the container’s namespace, but | |
| 550 | +it can’t stop them from making outbound network connections or modifying | |
| 551 | +the repo DB inside the container. | |
| 552 | + | |
| 553 | +[cgimgs]: https://github.com/chainguard-images/images/tree/main/images | |
| 554 | +[distroless]: https://www.chainguard.dev/unchained/minimal-container-images-towards-a-more-secure-future | |
| 555 | +[MTA]: https://en.wikipedia.org/wiki/Message_transfer_agent | |
| 556 | + | |
| 562 | 557 | |
| 563 | 558 | ## 6. <a id="light"></a>Lightweight Alternatives to Docker |
| 564 | 559 | |
| 565 | 560 | Those afflicted with sticker shock at seeing the size of a [Docker |
| 566 | 561 | Desktop][DD] installation — 1.65 GB here — might’ve immediately |
| @@ -567,13 +562,13 @@ | ||
| 567 | 562 | “noped” out of the whole concept of containers. The first thing to |
| 568 | 563 | realize is that when it comes to actually serving simple containers like |
| 569 | 564 | the ones shown above is that [Docker Engine][DE] suffices, at about a |
| 570 | 565 | quarter of the size. |
| 571 | 566 | |
| 572 | -Yet on a small server — say, a $4/month 10 GiB Digital Ocean droplet — | |
| 573 | -that’s still a big chunk of your storage budget. It takes 100:1 overhead | |
| 574 | -just to run a 4 MiB Fossil server container? Once again, I wouldn’t | |
| 567 | +Yet on a small server — say, a $4/month ten gig Digital Ocean droplet — | |
| 568 | +that’s still a big chunk of your storage budget. It takes ~60:1 overhead | |
| 569 | +merely to run a Fossil server container? Once again, I wouldn’t | |
| 575 | 570 | blame you if you noped right on out of here, but if you will be patient, |
| 576 | 571 | you will find that there are ways to run Fossil inside a container even |
| 577 | 572 | on entry-level cloud VPSes. These are well-suited to running Fossil; you |
| 578 | 573 | don’t have to resort to [raw Fossil service][srv] to succeed, |
| 579 | 574 | leaving the benefits of containerization to those with bigger budgets. |
| @@ -587,11 +582,11 @@ | ||
| 587 | 582 | --publish 127.0.0.1:9999:8080 |
| 588 | 583 | ``` |
| 589 | 584 | |
| 590 | 585 | The assumption is that there’s a reverse proxy running somewhere that |
| 591 | 586 | redirects public web hits to localhost port 9999, which in turn goes to |
| 592 | -port 8080 inside the container. This use of Docker/Podman port | |
| 587 | +port 8080 inside the container. This use of port | |
| 593 | 588 | publishing effectively replaces the use of the |
| 594 | 589 | “`fossil server --localhost`” option. |
| 595 | 590 | |
| 596 | 591 | For the nginx case, you need to add `--scgi` to these commands, and you |
| 597 | 592 | might also need to specify `--baseurl`. |
| @@ -616,17 +611,18 @@ | ||
| 616 | 611 | |
| 617 | 612 | |
| 618 | 613 | ### 6.1 <a id="nerdctl" name="containerd"></a>Stripping Docker Engine Down |
| 619 | 614 | |
| 620 | 615 | The core of Docker Engine is its [`containerd`][ctrd] daemon and the |
| 621 | -[`runc`][runc] container runner. Add to this the out-of-core CLI program | |
| 616 | +[`runc`][runc] container runtime. Add to this the out-of-core CLI program | |
| 622 | 617 | [`nerdctl`][nerdctl] and you have enough of the engine to run Fossil |
| 623 | 618 | containers. The big things you’re missing are: |
| 624 | 619 | |
| 625 | 620 | * **BuildKit**: The container build engine, which doesn’t matter if |
| 626 | - you’re building elsewhere and using a container registry as an | |
| 627 | - intermediary between that build host and the deployment host. | |
| 621 | + you’re building elsewhere and shipping the images to the target. | |
| 622 | + A good example is using a container registry as an | |
| 623 | + intermediary between the build and deployment hosts. | |
| 628 | 624 | |
| 629 | 625 | * **SwarmKit**: A powerful yet simple orchestrator for Docker that you |
| 630 | 626 | probably aren’t using with Fossil anyway. |
| 631 | 627 | |
| 632 | 628 | In exchange, you get a runtime that’s about half the size of Docker |
| @@ -788,11 +784,11 @@ | ||
| 788 | 784 | * The path in the host-side part of the `Bind` value must point at the |
| 789 | 785 | directory containing the `repo.fossil` file referenced in said |
| 790 | 786 | command so that `/museum/repo.fossil` refers to your repo out |
| 791 | 787 | on the host for the reasons given [above](#bind-mount). |
| 792 | 788 | |
| 793 | -That being done, we also need a generic systemd unit file called | |
| 789 | +That being done, we also need a generic `systemd` unit file called | |
| 794 | 790 | `/etc/systemd/system/[email protected]`, containing: |
| 795 | 791 | |
| 796 | 792 | ---- |
| 797 | 793 | |
| 798 | 794 | ``` |
| @@ -841,11 +837,11 @@ | ||
| 841 | 837 | ``` |
| 842 | 838 | |
| 843 | 839 | You would also need to un-drop the `CAP_NET_BIND_SERVICE` capability |
| 844 | 840 | to allow Fossil to bind to this low-numbered port. |
| 845 | 841 | |
| 846 | -We use systemd’s template file feature to allow multiple Fossil | |
| 842 | +We use the `systemd` template file feature to allow multiple Fossil | |
| 847 | 843 | servers running on a single machine, each on a different TCP port, |
| 848 | 844 | as when proxying them out as subdirectories of a larger site. |
| 849 | 845 | To add another project, you must first clone the base “machine” layer: |
| 850 | 846 | |
| 851 | 847 | ``` |
| @@ -996,11 +992,11 @@ | ||
| 996 | 992 | you’re serving Fossil from, you may need to know which assumptions |
| 997 | 993 | our container violates and the resulting consequences. |
| 998 | 994 | |
| 999 | 995 | Some of it we discussed above already, but there’s one big class of |
| 1000 | 996 | problems we haven’t covered yet. It stems from the fact that our stock |
| 1001 | -container starts a single static executable inside a barebones container | |
| 997 | +container starts a single static executable inside a bare-bones container | |
| 1002 | 998 | rather than “boot” an OS image. That causes a bunch of commands to fail: |
| 1003 | 999 | |
| 1004 | 1000 | * **`machinectl poweroff`** will fail because the container |
| 1005 | 1001 | isn’t running dbus. |
| 1006 | 1002 | |
| 1007 | 1003 |
| --- www/containers.md | |
| +++ www/containers.md | |
| @@ -69,11 +69,11 @@ | |
| 69 | The wrong way is to use the `Dockerfile COPY` command, because by baking |
| 70 | the repo into the image at build time, it will become one of the image’s |
| 71 | base layers. The end result is that each time you build a container from |
| 72 | that image, the repo will be reset to its build-time state. Worse, |
| 73 | restarting the container will do the same thing, since the base image |
| 74 | layers are immutable in Docker. This is almost certainly not what you |
| 75 | want. |
| 76 | |
| 77 | The correct ways put the repo into the _container_ created from the |
| 78 | _image_, not in the image itself. |
| 79 | |
| @@ -114,11 +114,11 @@ | |
| 114 | this with `chmod`: simply reload the browser, and Fossil will try again. |
| 115 | |
| 116 | |
| 117 | ### 2.2 <a id="bind-mount"></a>Storing the Repo Outside the Container |
| 118 | |
| 119 | The simple storage method above has a problem: Docker containers are |
| 120 | designed to be killed off at the slightest cause, rebuilt, and |
| 121 | redeployed. If you do that with the repo inside the container, it gets |
| 122 | destroyed, too. The solution is to replace the “run” command above with |
| 123 | the following: |
| 124 | |
| @@ -133,12 +133,12 @@ | |
| 133 | Because this bind mount maps a host-side directory (`~/museum`) into the |
| 134 | container, you don’t need to `docker cp` the repo into the container at |
| 135 | all. It still expects to find the repository as `repo.fossil` under that |
| 136 | directory, but now both the host and the container can see that repo DB. |
| 137 | |
| 138 | Instead of a bind mount, you could instead set up a separate [Docker |
| 139 | volume](https://docs.docker.com/storage/volumes/), at which point you |
| 140 | _would_ need to `docker cp` the repo file into the container. |
| 141 | |
| 142 | Either way, files in these mounted directories have a lifetime |
| 143 | independent of the container(s) they’re mounted into. When you need to |
| 144 | rebuild the container or its underlying image — such as to upgrade to a |
| @@ -183,152 +183,25 @@ | |
| 183 | |
| 184 | ## 3. <a id="security"></a>Security |
| 185 | |
| 186 | ### 3.1 <a id="chroot"></a>Why Not Chroot? |
| 187 | |
| 188 | Prior to 2023.03.26, the stock Fossil container made use of [the chroot |
| 189 | jail feature](./chroot.md) in order to wall away the shell and other |
| 190 | tools provided by [BusyBox](https://www.busybox.net/BusyBox.html). This |
| 191 | author made a living for years in the early 1990s using Unix systems |
| 192 | that offered less power, so there was a legitimate worry that if someone |
| 193 | ever figured out how to get a shell on one of these Fossil containers, |
| 194 | it would constitute a powerful island from which to attack the rest of |
| 195 | the network. |
| 196 | |
| 197 | The thing is, Fossil is self-contained, needing none of that power in |
| 198 | the main-line use cases. The only reason we included BusyBox in the |
| 199 | container at all was on the off chance that someone needed it for |
| 200 | debugging. |
| 201 | |
| 202 | That justification collapsed when we realized you could restore this |
| 203 | basic shell environment on an as-needed basis with a one-line change to |
| 204 | the `Dockerfile`, as we show in the next section. |
| 205 | |
| 206 | |
| 207 | ### 3.2 <a id="run"></a>Swapping Out the Run Layer |
| 208 | |
| 209 | If you want a basic shell environment for temporary debugging of the |
| 210 | running container, that’s easily added. Simply change this line in the |
| 211 | `Dockerfile`… |
| 212 | |
| 213 | FROM scratch AS run |
| 214 | |
| 215 | …to this: |
| 216 | |
| 217 | FROM busybox AS run |
| 218 | |
| 219 | Rebuild, redeploy, and your Fossil container now has a BusyBox based |
| 220 | shell environment that you can get into via: |
| 221 | |
| 222 | $ docker exec -it -u fossil $(make container-version) sh |
| 223 | |
| 224 | (That command assumes you built the container via “`make container`” and |
| 225 | are therefore using its versioning scheme.) |
| 226 | |
| 227 | Another case where you might need to replace this bare-bones “`run`” |
| 228 | layer with something more functional is that you’re setting up [email |
| 229 | alerts](./alerts.md) and need some way to integrate with the host’s |
| 230 | [MTA]. There are a number of alternatives in that linked document, so |
| 231 | for the sake of discussion, we’ll say you’ve chosen [Method |
| 232 | 2](./alerts.md#db), which requires a Tcl interpreter and its SQLite |
| 233 | extension to push messages into the outbound email queue DB, presumably |
| 234 | bind-mounted into the container. |
| 235 | |
| 236 | You can do that by replacing STAGEs 2 and 3 in the stock `Dockerfile` |
| 237 | with this: |
| 238 | |
| 239 | ``` |
| 240 | ## --------------------------------------------------------------------- |
| 241 | ## STAGE 2: Pare that back to the bare essentials, plus Tcl. |
| 242 | ## --------------------------------------------------------------------- |
| 243 | FROM alpine AS run |
| 244 | ARG UID=499 |
| 245 | ENV PATH "/sbin:/usr/sbin:/bin:/usr/bin" |
| 246 | COPY --from=builder /tmp/fossil /bin/ |
| 247 | COPY tools/email-sender.tcl /bin/ |
| 248 | RUN set -x \ |
| 249 | && echo "fossil:x:${UID}:${UID}:User:/museum:/false" >> /etc/passwd \ |
| 250 | && echo "fossil:x:${UID}:fossil" >> /etc/group \ |
| 251 | && install -d -m 700 -o fossil -g fossil log museum \ |
| 252 | && apk add --no-cache tcl sqlite-tcl |
| 253 | ``` |
| 254 | |
| 255 | Build it and test that it works like so: |
| 256 | |
| 257 | ``` |
| 258 | $ make container-run && |
| 259 | echo 'puts [info patchlevel]' | |
| 260 | docker exec -i $(make container-version) tclsh |
| 261 | 8.6.12 |
| 262 | ``` |
| 263 | |
| 264 | You should remove the `PATH` override in the “RUN” |
| 265 | stage, since it’s written for the case where everything is in `/bin`. |
| 266 | With these additions, we need the longer `PATH` shown above to have |
| 267 | ready access to them all. |
| 268 | |
| 269 | Another useful case to consider is that you’ve installed a [server |
| 270 | extension](./serverext.wiki) and you need an interpreter for that |
| 271 | script. The first option above won’t work except in the unlikely case that |
| 272 | it’s written for one of the bare-bones script interpreters that BusyBox |
| 273 | ships.(^BusyBox’s `/bin/sh` is based on the old 4.4BSD Lite Almquist |
| 274 | shell, implementing little more than what POSIX specified in 1989, plus |
| 275 | equally stripped-down versions of `awk` and `sed`.) |
| 276 | |
| 277 | Let’s say the extension is written in Python. While you could handle it |
| 278 | the same way we do with the Tcl example above, Python is more |
| 279 | popular, giving us more options. Let’s inject a Python environment into |
| 280 | the stock Fossil container via a suitable “[distroless]” image instead: |
| 281 | |
| 282 | ``` |
| 283 | ## --------------------------------------------------------------------- |
| 284 | ## STAGE 2: Pare that back to the bare essentials, plus Python. |
| 285 | ## --------------------------------------------------------------------- |
| 286 | FROM cgr.dev/chainguard/python:latest |
| 287 | USER root |
| 288 | ARG UID=499 |
| 289 | ENV PATH "/sbin:/usr/sbin:/bin:/usr/bin" |
| 290 | COPY --from=builder /tmp/fossil /bin/ |
| 291 | COPY --from=builder /bin/busybox.static /bin/busybox |
| 292 | RUN [ "/bin/busybox", "--install", "/bin" ] |
| 293 | RUN set -x \ |
| 294 | && echo "fossil:x:${UID}:${UID}:User:/museum:/false" >> /etc/passwd \ |
| 295 | && echo "fossil:x:${UID}:fossil" >> /etc/group \ |
| 296 | && install -d -m 700 -o fossil -g fossil log museum |
| 297 | ``` |
| 298 | |
| 299 | You will also have to add `busybox-static` to the APK package list in |
| 300 | STAGE 1 for the `RUN` script at the end of that stage to work, since the |
| 301 | [Chainguard Python image][cgimgs] lacks a shell, on purpose. The need to |
| 302 | install root-level binaries is why we change `USER` temporarily here. |
| 303 | |
| 304 | Build it and test that it works like so: |
| 305 | |
| 306 | ``` |
| 307 | $ make container-run && |
| 308 | docker exec -i $(make container-version) python --version |
| 309 | 3.11.2 |
| 310 | ``` |
| 311 | |
| 312 | The compensation for the hassle of using Chainguard over something more |
| 313 | general purpose like Alpine + “`apk add python`” |
| 314 | is huge: we no longer leave a package manager sitting around inside the |
| 315 | container, waiting for some malefactor to figure out how to abuse it. |
| 316 | |
| 317 | Beware that there’s a limit to this über-jail’s ability to save you when |
| 318 | you go and provide a more capable OS layer like this. The container |
| 319 | layer should stop an attacker from accessing any files out on the host |
| 320 | that you haven’t explicitly mounted into the container’s namespace, but |
| 321 | it can’t stop them from making outbound network connections or modifying |
| 322 | the repo DB inside the container. |
| 323 | |
| 324 | [cgimgs]: https://github.com/chainguard-images/images/tree/main/images |
| 325 | [distroless]: https://www.chainguard.dev/unchained/minimal-container-images-towards-a-more-secure-future |
| 326 | [MTA]: https://en.wikipedia.org/wiki/Message_transfer_agent |
| 327 | |
| 328 | |
| 329 | ### 3.3 <a id="caps"></a>Dropping Unnecessary Capabilities |
| 330 | |
| 331 | The example commands above create the container with [a default set of |
| 332 | Linux kernel capabilities][defcap]. Although Docker strips away almost |
| 333 | all of the traditional root capabilities by default, and Fossil doesn’t |
| 334 | need any of those it does take away, Docker does leave some enabled that |
| @@ -443,12 +316,12 @@ | |
| 443 | |
| 444 | |
| 445 | ## 4. <a id="static"></a>Extracting a Static Binary |
| 446 | |
| 447 | Our 2-stage build process uses Alpine Linux only as a build host. Once |
| 448 | we’ve got everything reduced to the two key static binaries — Fossil and |
| 449 | BusyBox — we throw all the rest of it away. |
| 450 | |
| 451 | A secondary benefit falls out of this process for free: it’s arguably |
| 452 | the easiest way to build a purely static Fossil binary for Linux. Most |
| 453 | modern Linux distros make this [surprisingly difficult][lsl], but Alpine’s |
| 454 | back-to-basics nature makes static builds work the way they used to, |
| @@ -466,11 +339,11 @@ | |
| 466 | at about 6 MiB. (It’s built stripped.) |
| 467 | |
| 468 | [lsl]: https://stackoverflow.com/questions/3430400/linux-static-linking-is-dead |
| 469 | |
| 470 | |
| 471 | ## 5. <a id="args"></a>Container Build Arguments |
| 472 | |
| 473 | ### <a id="pkg-vers"></a> 5.1 Fossil Version |
| 474 | |
| 475 | The default version of Fossil fetched in the build is the version in the |
| 476 | checkout directory at the time you run it. You could override it to get |
| @@ -486,21 +359,21 @@ | |
| 486 | $ make container-image DBFLAGS='--build-arg FSLVER=version-2.20' |
| 487 | ``` |
| 488 | |
| 489 | While you could instead use the generic |
| 490 | “`release`” tag here, it’s better to use a specific version number |
| 491 | since Docker caches downloaded files and tries to |
| 492 | reuse them across builds. If you ask for “`release`” before a new |
| 493 | version is tagged and then immediately after, you might expect to get |
| 494 | two different tarballs, but because the underlying source tarball URL |
| 495 | remains the same when you do that, you’ll end up reusing the |
| 496 | old tarball from your Docker cache. This will occur |
| 497 | even if you pass the “`docker build --no-cache`” option. |
| 498 | |
| 499 | This is why we default to pulling the Fossil tarball by checkin ID |
| 500 | rather than let it default to the generic “`trunk`” tag: so the URL will |
| 501 | change each time you update your Fossil source tree, forcing Docker to |
| 502 | pull a fresh tarball. |
| 503 | |
| 504 | |
| 505 | ### 5.2 <a id="uids"></a>User & Group IDs |
| 506 | |
| @@ -517,11 +390,11 @@ | |
| 517 | $ make container-image \ |
| 518 | DBFLAGS='--build-arg UID=501' |
| 519 | ``` |
| 520 | |
| 521 | This is particularly useful if you’re putting your repository on a |
| 522 | Docker volume since the IDs “leak” out into the host environment via |
| 523 | file permissions. You may therefore wish them to mean something on both |
| 524 | sides of the container barrier rather than have “499” appear on the host |
| 525 | in “`ls -l`” output. |
| 526 | |
| 527 | |
| @@ -557,10 +430,132 @@ | |
| 557 | |
| 558 | ``` |
| 559 | $ make CENGINE=podman container-run |
| 560 | ``` |
| 561 | |
| 562 | |
| 563 | ## 6. <a id="light"></a>Lightweight Alternatives to Docker |
| 564 | |
| 565 | Those afflicted with sticker shock at seeing the size of a [Docker |
| 566 | Desktop][DD] installation — 1.65 GB here — might’ve immediately |
| @@ -567,13 +562,13 @@ | |
| 567 | “noped” out of the whole concept of containers. The first thing to |
| 568 | realize is that when it comes to actually serving simple containers like |
| 569 | the ones shown above is that [Docker Engine][DE] suffices, at about a |
| 570 | quarter of the size. |
| 571 | |
| 572 | Yet on a small server — say, a $4/month 10 GiB Digital Ocean droplet — |
| 573 | that’s still a big chunk of your storage budget. It takes 100:1 overhead |
| 574 | just to run a 4 MiB Fossil server container? Once again, I wouldn’t |
| 575 | blame you if you noped right on out of here, but if you will be patient, |
| 576 | you will find that there are ways to run Fossil inside a container even |
| 577 | on entry-level cloud VPSes. These are well-suited to running Fossil; you |
| 578 | don’t have to resort to [raw Fossil service][srv] to succeed, |
| 579 | leaving the benefits of containerization to those with bigger budgets. |
| @@ -587,11 +582,11 @@ | |
| 587 | --publish 127.0.0.1:9999:8080 |
| 588 | ``` |
| 589 | |
| 590 | The assumption is that there’s a reverse proxy running somewhere that |
| 591 | redirects public web hits to localhost port 9999, which in turn goes to |
| 592 | port 8080 inside the container. This use of Docker/Podman port |
| 593 | publishing effectively replaces the use of the |
| 594 | “`fossil server --localhost`” option. |
| 595 | |
| 596 | For the nginx case, you need to add `--scgi` to these commands, and you |
| 597 | might also need to specify `--baseurl`. |
| @@ -616,17 +611,18 @@ | |
| 616 | |
| 617 | |
| 618 | ### 6.1 <a id="nerdctl" name="containerd"></a>Stripping Docker Engine Down |
| 619 | |
| 620 | The core of Docker Engine is its [`containerd`][ctrd] daemon and the |
| 621 | [`runc`][runc] container runner. Add to this the out-of-core CLI program |
| 622 | [`nerdctl`][nerdctl] and you have enough of the engine to run Fossil |
| 623 | containers. The big things you’re missing are: |
| 624 | |
| 625 | * **BuildKit**: The container build engine, which doesn’t matter if |
| 626 | you’re building elsewhere and using a container registry as an |
| 627 | intermediary between that build host and the deployment host. |
| 628 | |
| 629 | * **SwarmKit**: A powerful yet simple orchestrator for Docker that you |
| 630 | probably aren’t using with Fossil anyway. |
| 631 | |
| 632 | In exchange, you get a runtime that’s about half the size of Docker |
| @@ -788,11 +784,11 @@ | |
| 788 | * The path in the host-side part of the `Bind` value must point at the |
| 789 | directory containing the `repo.fossil` file referenced in said |
| 790 | command so that `/museum/repo.fossil` refers to your repo out |
| 791 | on the host for the reasons given [above](#bind-mount). |
| 792 | |
| 793 | That being done, we also need a generic systemd unit file called |
| 794 | `/etc/systemd/system/[email protected]`, containing: |
| 795 | |
| 796 | ---- |
| 797 | |
| 798 | ``` |
| @@ -841,11 +837,11 @@ | |
| 841 | ``` |
| 842 | |
| 843 | You would also need to un-drop the `CAP_NET_BIND_SERVICE` capability |
| 844 | to allow Fossil to bind to this low-numbered port. |
| 845 | |
| 846 | We use systemd’s template file feature to allow multiple Fossil |
| 847 | servers running on a single machine, each on a different TCP port, |
| 848 | as when proxying them out as subdirectories of a larger site. |
| 849 | To add another project, you must first clone the base “machine” layer: |
| 850 | |
| 851 | ``` |
| @@ -996,11 +992,11 @@ | |
| 996 | you’re serving Fossil from, you may need to know which assumptions |
| 997 | our container violates and the resulting consequences. |
| 998 | |
| 999 | Some of it we discussed above already, but there’s one big class of |
| 1000 | problems we haven’t covered yet. It stems from the fact that our stock |
| 1001 | container starts a single static executable inside a barebones container |
| 1002 | rather than “boot” an OS image. That causes a bunch of commands to fail: |
| 1003 | |
| 1004 | * **`machinectl poweroff`** will fail because the container |
| 1005 | isn’t running dbus. |
| 1006 | |
| 1007 |
| --- www/containers.md | |
| +++ www/containers.md | |
| @@ -69,11 +69,11 @@ | |
| 69 | The wrong way is to use the `Dockerfile COPY` command, because by baking |
| 70 | the repo into the image at build time, it will become one of the image’s |
| 71 | base layers. The end result is that each time you build a container from |
| 72 | that image, the repo will be reset to its build-time state. Worse, |
| 73 | restarting the container will do the same thing, since the base image |
| 74 | layers are immutable. This is almost certainly not what you |
| 75 | want. |
| 76 | |
| 77 | The correct ways put the repo into the _container_ created from the |
| 78 | _image_, not in the image itself. |
| 79 | |
| @@ -114,11 +114,11 @@ | |
| 114 | this with `chmod`: simply reload the browser, and Fossil will try again. |
| 115 | |
| 116 | |
| 117 | ### 2.2 <a id="bind-mount"></a>Storing the Repo Outside the Container |
| 118 | |
| 119 | The simple storage method above has a problem: containers are |
| 120 | designed to be killed off at the slightest cause, rebuilt, and |
| 121 | redeployed. If you do that with the repo inside the container, it gets |
| 122 | destroyed, too. The solution is to replace the “run” command above with |
| 123 | the following: |
| 124 | |
| @@ -133,12 +133,12 @@ | |
| 133 | Because this bind mount maps a host-side directory (`~/museum`) into the |
| 134 | container, you don’t need to `docker cp` the repo into the container at |
| 135 | all. It still expects to find the repository as `repo.fossil` under that |
| 136 | directory, but now both the host and the container can see that repo DB. |
| 137 | |
| 138 | Instead of a bind mount, you could instead set up a separate |
| 139 | [volume](https://docs.docker.com/storage/volumes/), at which point you |
| 140 | _would_ need to `docker cp` the repo file into the container. |
| 141 | |
| 142 | Either way, files in these mounted directories have a lifetime |
| 143 | independent of the container(s) they’re mounted into. When you need to |
| 144 | rebuild the container or its underlying image — such as to upgrade to a |
| @@ -183,152 +183,25 @@ | |
| 183 | |
| 184 | ## 3. <a id="security"></a>Security |
| 185 | |
| 186 | ### 3.1 <a id="chroot"></a>Why Not Chroot? |
| 187 | |
| 188 | Prior to 2023.03.26, the stock Fossil container relied on [the chroot |
| 189 | jail feature](./chroot.md) to wall away the shell and other tools |
| 190 | provided by [BusyBox]. It included that as a bare-bones operating system |
| 191 | inside the container on the off chance that someone might need it for |
| 192 | debugging, but the thing is, Fossil is self-contained, needing none of |
| 193 | that power in the main-line use cases. |
| 194 | |
| 195 | Our weak “you might need it” justification collapsed when we realized |
| 196 | you could restore this basic shell environment with a one-line change to |
| 197 | the `Dockerfile`, as shown [below](#run). |
| 198 | |
| 199 | [BusyBox]: https://www.busybox.net/BusyBox.html |
| 200 | |
| 201 | |
| 202 | ### 3.2 <a id="caps"></a>Dropping Unnecessary Capabilities |
| 203 | |
| 204 | The example commands above create the container with [a default set of |
| 205 | Linux kernel capabilities][defcap]. Although Docker strips away almost |
| 206 | all of the traditional root capabilities by default, and Fossil doesn’t |
| 207 | need any of those it does take away, Docker does leave some enabled that |
| @@ -443,12 +316,12 @@ | |
| 316 | |
| 317 | |
| 318 | ## 4. <a id="static"></a>Extracting a Static Binary |
| 319 | |
| 320 | Our 2-stage build process uses Alpine Linux only as a build host. Once |
| 321 | we’ve got everything reduced to a single static Fossil binary, |
| 322 | we throw all the rest of it away. |
| 323 | |
| 324 | A secondary benefit falls out of this process for free: it’s arguably |
| 325 | the easiest way to build a purely static Fossil binary for Linux. Most |
| 326 | modern Linux distros make this [surprisingly difficult][lsl], but Alpine’s |
| 327 | back-to-basics nature makes static builds work the way they used to, |
| @@ -466,11 +339,11 @@ | |
| 339 | at about 6 MiB. (It’s built stripped.) |
| 340 | |
| 341 | [lsl]: https://stackoverflow.com/questions/3430400/linux-static-linking-is-dead |
| 342 | |
| 343 | |
| 344 | ## 5. <a id="custom" name="args"></a>Customization Points |
| 345 | |
| 346 | ### <a id="pkg-vers"></a> 5.1 Fossil Version |
| 347 | |
| 348 | The default version of Fossil fetched in the build is the version in the |
| 349 | checkout directory at the time you run it. You could override it to get |
| @@ -486,21 +359,21 @@ | |
| 359 | $ make container-image DBFLAGS='--build-arg FSLVER=version-2.20' |
| 360 | ``` |
| 361 | |
| 362 | While you could instead use the generic |
| 363 | “`release`” tag here, it’s better to use a specific version number |
| 364 | since container builders cache downloaded files, hoping to |
| 365 | reuse them across builds. If you ask for “`release`” before a new |
| 366 | version is tagged and then immediately after, you might expect to get |
| 367 | two different tarballs, but because the underlying source tarball URL |
| 368 | remains the same when you do that, you’ll end up reusing the |
| 369 | old tarball from cache. This will occur |
| 370 | even if you pass the “`docker build --no-cache`” option. |
| 371 | |
| 372 | This is why we default to pulling the Fossil tarball by checkin ID |
| 373 | rather than let it default to the generic “`trunk`” tag: so the URL will |
| 374 | change each time you update your Fossil source tree, forcing the builder to |
| 375 | pull a fresh tarball. |
| 376 | |
| 377 | |
| 378 | ### 5.2 <a id="uids"></a>User & Group IDs |
| 379 | |
| @@ -517,11 +390,11 @@ | |
| 390 | $ make container-image \ |
| 391 | DBFLAGS='--build-arg UID=501' |
| 392 | ``` |
| 393 | |
| 394 | This is particularly useful if you’re putting your repository on a |
| 395 | separate volume since the IDs “leak” out into the host environment via |
| 396 | file permissions. You may therefore wish them to mean something on both |
| 397 | sides of the container barrier rather than have “499” appear on the host |
| 398 | in “`ls -l`” output. |
| 399 | |
| 400 | |
| @@ -557,10 +430,132 @@ | |
| 430 | |
| 431 | ``` |
| 432 | $ make CENGINE=podman container-run |
| 433 | ``` |
| 434 | |
| 435 | |
| 436 | ### 5.3 <a id="run"></a>Elaborating the Run Layer |
| 437 | |
| 438 | If you want a basic shell environment for temporary debugging of the |
| 439 | running container, that’s easily added. Simply change this line in the |
| 440 | `Dockerfile`… |
| 441 | |
| 442 | FROM scratch AS run |
| 443 | |
| 444 | …to this: |
| 445 | |
| 446 | FROM busybox AS run |
| 447 | |
| 448 | Rebuild, redeploy, and your Fossil container will have a [BusyBox]-based |
| 449 | shell environment that you can get into via: |
| 450 | |
| 451 | $ docker exec -it -u fossil $(make container-version) sh |
| 452 | |
| 453 | (That command assumes you built it via “`make container`” and are |
| 454 | therefore using its versioning scheme.) |
| 455 | |
| 456 | Another case where you might need to replace this bare-bones “`run`” |
| 457 | layer with something more functional is that you’re setting up [email |
| 458 | alerts](./alerts.md) and need some way to integrate with the host’s |
| 459 | [MTA]. There are a number of alternatives in that linked document, so |
| 460 | for the sake of discussion, we’ll say you’ve chosen [Method |
| 461 | 2](./alerts.md#db), which requires a Tcl interpreter and its SQLite |
| 462 | extension to push messages into the outbound email queue DB, presumably |
| 463 | bind-mounted into the container. |
| 464 | |
| 465 | You can do that by replacing STAGEs 2 and 3 in the stock `Dockerfile` |
| 466 | with this: |
| 467 | |
| 468 | ``` |
| 469 | ## --------------------------------------------------------------------- |
| 470 | ## STAGE 2: Pare that back to the bare essentials, plus Tcl. |
| 471 | ## --------------------------------------------------------------------- |
| 472 | FROM alpine AS run |
| 473 | ARG UID=499 |
| 474 | ENV PATH "/sbin:/usr/sbin:/bin:/usr/bin" |
| 475 | COPY --from=builder /tmp/fossil /bin/ |
| 476 | COPY tools/email-sender.tcl /bin/ |
| 477 | RUN set -x \ |
| 478 | && echo "fossil:x:${UID}:${UID}:User:/museum:/false" >> /etc/passwd \ |
| 479 | && echo "fossil:x:${UID}:fossil" >> /etc/group \ |
| 480 | && install -d -m 700 -o fossil -g fossil log museum \ |
| 481 | && apk add --no-cache tcl sqlite-tcl |
| 482 | ``` |
| 483 | |
| 484 | Build it and test that it works like so: |
| 485 | |
| 486 | ``` |
| 487 | $ make container-run && |
| 488 | echo 'puts [info patchlevel]' | |
| 489 | docker exec -i $(make container-version) tclsh |
| 490 | 8.6.12 |
| 491 | ``` |
| 492 | |
| 493 | You should remove the `PATH` override in the “RUN” |
| 494 | stage, since it’s written for the case where everything is in `/bin`. |
| 495 | With these additions, we need the longer `PATH` shown above to have |
| 496 | ready access to them all. |
| 497 | |
| 498 | Another useful case to consider is that you’ve installed a [server |
| 499 | extension](./serverext.wiki) and you need an interpreter for that |
| 500 | script. The first option above won’t work except in the unlikely case that |
| 501 | it’s written for one of the bare-bones script interpreters that BusyBox |
| 502 | ships.(^[BusyBox]’s `/bin/sh` is based on the old 4.4BSD Lite Almquist |
| 503 | shell, implementing little more than what POSIX specified in 1989, plus |
| 504 | equally stripped-down versions of `awk` and `sed`.) |
| 505 | |
| 506 | Let’s say the extension is written in Python. While you could handle it |
| 507 | the same way we do with the Tcl example above, Python is more |
| 508 | popular, giving us more options. Let’s inject a Python environment into |
| 509 | the stock Fossil container via a suitable “[distroless]” image instead: |
| 510 | |
| 511 | ``` |
| 512 | ## --------------------------------------------------------------------- |
| 513 | ## STAGE 2: Pare that back to the bare essentials, plus Python. |
| 514 | ## --------------------------------------------------------------------- |
| 515 | FROM cgr.dev/chainguard/python:latest |
| 516 | USER root |
| 517 | ARG UID=499 |
| 518 | ENV PATH "/sbin:/usr/sbin:/bin:/usr/bin" |
| 519 | COPY --from=builder /tmp/fossil /bin/ |
| 520 | COPY --from=builder /bin/busybox.static /bin/busybox |
| 521 | RUN [ "/bin/busybox", "--install", "/bin" ] |
| 522 | RUN set -x \ |
| 523 | && echo "fossil:x:${UID}:${UID}:User:/museum:/false" >> /etc/passwd \ |
| 524 | && echo "fossil:x:${UID}:fossil" >> /etc/group \ |
| 525 | && install -d -m 700 -o fossil -g fossil log museum |
| 526 | ``` |
| 527 | |
| 528 | You will also have to add `busybox-static` to the APK package list in |
| 529 | STAGE 1 for the `RUN` script at the end of that stage to work, since the |
| 530 | [Chainguard Python image][cgimgs] lacks a shell, on purpose. The need to |
| 531 | install root-level binaries is why we change `USER` temporarily here. |
| 532 | |
| 533 | Build it and test that it works like so: |
| 534 | |
| 535 | ``` |
| 536 | $ make container-run && |
| 537 | docker exec -i $(make container-version) python --version |
| 538 | 3.11.2 |
| 539 | ``` |
| 540 | |
| 541 | The compensation for the hassle of using Chainguard over something more |
| 542 | general purpose like Alpine + “`apk add python`” |
| 543 | is huge: we no longer leave a package manager sitting around inside the |
| 544 | container, waiting for some malefactor to figure out how to abuse it. |
| 545 | |
| 546 | Beware that there’s a limit to this über-jail’s ability to save you when |
| 547 | you go and provide a more capable OS layer like this. The container |
| 548 | layer should stop an attacker from accessing any files out on the host |
| 549 | that you haven’t explicitly mounted into the container’s namespace, but |
| 550 | it can’t stop them from making outbound network connections or modifying |
| 551 | the repo DB inside the container. |
| 552 | |
| 553 | [cgimgs]: https://github.com/chainguard-images/images/tree/main/images |
| 554 | [distroless]: https://www.chainguard.dev/unchained/minimal-container-images-towards-a-more-secure-future |
| 555 | [MTA]: https://en.wikipedia.org/wiki/Message_transfer_agent |
| 556 | |
| 557 | |
| 558 | ## 6. <a id="light"></a>Lightweight Alternatives to Docker |
| 559 | |
| 560 | Those afflicted with sticker shock at seeing the size of a [Docker |
| 561 | Desktop][DD] installation — 1.65 GB here — might’ve immediately |
| @@ -567,13 +562,13 @@ | |
| 562 | “noped” out of the whole concept of containers. The first thing to |
| 563 | realize is that when it comes to actually serving simple containers like |
| 564 | the ones shown above is that [Docker Engine][DE] suffices, at about a |
| 565 | quarter of the size. |
| 566 | |
| 567 | Yet on a small server — say, a $4/month ten gig Digital Ocean droplet — |
| 568 | that’s still a big chunk of your storage budget. It takes ~60:1 overhead |
| 569 | merely to run a Fossil server container? Once again, I wouldn’t |
| 570 | blame you if you noped right on out of here, but if you will be patient, |
| 571 | you will find that there are ways to run Fossil inside a container even |
| 572 | on entry-level cloud VPSes. These are well-suited to running Fossil; you |
| 573 | don’t have to resort to [raw Fossil service][srv] to succeed, |
| 574 | leaving the benefits of containerization to those with bigger budgets. |
| @@ -587,11 +582,11 @@ | |
| 582 | --publish 127.0.0.1:9999:8080 |
| 583 | ``` |
| 584 | |
| 585 | The assumption is that there’s a reverse proxy running somewhere that |
| 586 | redirects public web hits to localhost port 9999, which in turn goes to |
| 587 | port 8080 inside the container. This use of port |
| 588 | publishing effectively replaces the use of the |
| 589 | “`fossil server --localhost`” option. |
| 590 | |
| 591 | For the nginx case, you need to add `--scgi` to these commands, and you |
| 592 | might also need to specify `--baseurl`. |
| @@ -616,17 +611,18 @@ | |
| 611 | |
| 612 | |
| 613 | ### 6.1 <a id="nerdctl" name="containerd"></a>Stripping Docker Engine Down |
| 614 | |
| 615 | The core of Docker Engine is its [`containerd`][ctrd] daemon and the |
| 616 | [`runc`][runc] container runtime. Add to this the out-of-core CLI program |
| 617 | [`nerdctl`][nerdctl] and you have enough of the engine to run Fossil |
| 618 | containers. The big things you’re missing are: |
| 619 | |
| 620 | * **BuildKit**: The container build engine, which doesn’t matter if |
| 621 | you’re building elsewhere and shipping the images to the target. |
| 622 | A good example is using a container registry as an |
| 623 | intermediary between the build and deployment hosts. |
| 624 | |
| 625 | * **SwarmKit**: A powerful yet simple orchestrator for Docker that you |
| 626 | probably aren’t using with Fossil anyway. |
| 627 | |
| 628 | In exchange, you get a runtime that’s about half the size of Docker |
| @@ -788,11 +784,11 @@ | |
| 784 | * The path in the host-side part of the `Bind` value must point at the |
| 785 | directory containing the `repo.fossil` file referenced in said |
| 786 | command so that `/museum/repo.fossil` refers to your repo out |
| 787 | on the host for the reasons given [above](#bind-mount). |
| 788 | |
| 789 | That being done, we also need a generic `systemd` unit file called |
| 790 | `/etc/systemd/system/[email protected]`, containing: |
| 791 | |
| 792 | ---- |
| 793 | |
| 794 | ``` |
| @@ -841,11 +837,11 @@ | |
| 837 | ``` |
| 838 | |
| 839 | You would also need to un-drop the `CAP_NET_BIND_SERVICE` capability |
| 840 | to allow Fossil to bind to this low-numbered port. |
| 841 | |
| 842 | We use the `systemd` template file feature to allow multiple Fossil |
| 843 | servers running on a single machine, each on a different TCP port, |
| 844 | as when proxying them out as subdirectories of a larger site. |
| 845 | To add another project, you must first clone the base “machine” layer: |
| 846 | |
| 847 | ``` |
| @@ -996,11 +992,11 @@ | |
| 992 | you’re serving Fossil from, you may need to know which assumptions |
| 993 | our container violates and the resulting consequences. |
| 994 | |
| 995 | Some of it we discussed above already, but there’s one big class of |
| 996 | problems we haven’t covered yet. It stems from the fact that our stock |
| 997 | container starts a single static executable inside a bare-bones container |
| 998 | rather than “boot” an OS image. That causes a bunch of commands to fail: |
| 999 | |
| 1000 | * **`machinectl poweroff`** will fail because the container |
| 1001 | isn’t running dbus. |
| 1002 | |
| 1003 |