├── README.md └── qrcode.png /README.md: -------------------------------------------------------------------------------- 1 | ![QR code for this page](qrcode.png) 2 | 3 | # Go + Docker = ♥ 4 | 5 | Hi! Here are a few interesting and fun things to do with Go and Docker. 6 | 7 | 8 | ## Go without `go` 9 | 10 | ... And by that, we mean "Go without installing `go`". 11 | 12 | Most of you certainly have the Go compiler and toolchain installed, 13 | so you might be wondering "what's the point?"; but there are 14 | a few scenarios where this can still be very useful. 15 | 16 | * You still have this old Go 1.2 on your machine (that you can't 17 | or won't upgrade), and you have to work on this codebase that 18 | requires a newer version of the toolchain. 19 | * You want to play with cross compilation features of Go 1.5 20 | (for instance, to make sure that you can create OS X binaries 21 | from a Linux system). 22 | * You want to have multiple versions of Go side-by-side, but don't 23 | want to completely litter your system. 24 | * You want to be 100% sure that your project and all its dependencies 25 | download, build, and run fine on a clean system. 26 | 27 | If any of this is relevant to you, then let's call Docker to the rescue! 28 | 29 | 30 | ### Compiling a program in a container 31 | 32 | When you have installed Go, you can do `go get -v github.com/user/repo` 33 | to download, build, and install a library. (The `-v` flag is just 34 | here for verbosity, you can remove it if you prefer your 35 | toolchain to be swift and silent!) 36 | 37 | You can also do `go get github.com/user/repo/...` (yes, that's 38 | three dots) to download, build, and install all the things in 39 | that repo (including libraries and binaries). 40 | 41 | We can do that in a container! 42 | 43 | Try this: 44 | 45 | ```bash 46 | docker run golang go get -v github.com/golang/example/hello/... 47 | ``` 48 | 49 | This will pull the `golang` image (unless you have it already; 50 | then it will start right away), and create a container based on 51 | that image. In that container, `go` will download a little 52 | "hello world" example, build it, and install it. But it will 53 | install it in the container ... So how do we run that program now? 54 | 55 | 56 | ### Running our program in a container 57 | 58 | One solution is to *commit* the container that we just built, 59 | i.e. "freeze" it into a new image: 60 | 61 | ```bash 62 | docker commit $(docker ps -lq) awesomeness 63 | ``` 64 | 65 | Note: `docker ps -lq` outputs the ID (and only the ID!) of 66 | the last container that was executed. If you are the only 67 | uesr on your machine, and if you haven't created another 68 | container since the previous command, that container 69 | should be the one in which we just built the "hello world" 70 | example. 71 | 72 | Now, we can run our program in a container based on 73 | the image that we just built: 74 | 75 | ```bash 76 | docker run awesomeness hello 77 | ``` 78 | 79 | The output should be `Hello, Go examples!`. 80 | 81 | 82 | #### Bonus points 83 | 84 | When creating the image with `docker commit`, you can 85 | use the `--change` flag to specify arbitrary [Dockerfile]( 86 | https://docs.docker.com/engine/reference/builder/) commands. 87 | For instance, you could use a `CMD` or `ENTRYPOINT` command 88 | so that `docker run awesomeness` automatically executes 89 | `hello`. 90 | 91 | 92 | ### Running in a throwaway container 93 | 94 | What if we don't want to create an extra image just to 95 | run this Go program? 96 | 97 | We got you covered: 98 | 99 | ```bash 100 | docker run --rm golang sh -c \ 101 | "go get github.com/golang/example/hello/... && exec hello" 102 | ``` 103 | 104 | Wait a minute, what are all those bells and whistles? 105 | 106 | * `--rm` tells to the Docker CLI to automatically issue a 107 | `docker rm` command once the container exits. That way, 108 | we don't leave anything behind ourselves. 109 | * We chain together the build step (`go get`) and the 110 | execution step (`exec hello`) using the shell logical 111 | operator `&&`. If you're not a shell aficionado, `&&` 112 | means "and". It will run the first part `go get...`, 113 | and if (and only if!) that part is successful, it will run 114 | the second part (`exec hello`). If you wonder why this 115 | is like that: it works like a lazy `and` evaluator, 116 | which needs to evaluate the right hand side 117 | only if the left hand side evaluates to `true`. 118 | * We pass our commands to `sh -c`, because if we were to 119 | simply do `docker run golang "go get ... && hello"`, 120 | Docker would try to execute the program named `go SPACE get 121 | SPACE etc.` and that wouldn't work. So instead, we start 122 | a shell and instruct the shell to execute the command 123 | sequence. 124 | * We use `exec hello` instead of `hello`: this will replace 125 | the current process (the shell that we started) with the 126 | `hello` program. This ensures that `hello` will be PID 1 127 | in the container, instead of having the shell as PID 1 128 | and `hello` as a child process. This is totally useless 129 | for this tiny example, but when we will run more useful 130 | programs, this will allow them to receive external signals 131 | properly, since external signals are delivered to PID 1 of 132 | the container. What kind of signal, you might be wondering? 133 | A good example is `docker stop`, which sends `SIGTERM` to 134 | PID 1 in the container. 135 | 136 | 137 | ### Installing on our system 138 | 139 | OK, so what if we want to run the compiled program on our 140 | system, instead of in a container? 141 | 142 | We could copy the compiled binary out of the container. 143 | Note, however, that this will work only if our container 144 | architecture matches our host architecture; in other words, 145 | if we run Docker on Linux. (I'm leaving out people who 146 | might be running Windows Containers!) 147 | 148 | The easiest way to get the binary out of the container 149 | is to map the `$GOPATH/bin` directory to a local directory. 150 | In the `golang` container, `$GOPATH` is `/go`. So we can do 151 | the following: 152 | 153 | ```bash 154 | docker run -v /tmp/bin:/go/bin \ 155 | golang go get github.com/golang/example/hello/... 156 | /tmp/bin/hello 157 | ``` 158 | 159 | If you are on Linux, you should see the `Hello, Go examples!` message. 160 | But if you are, for instance, on a Mac, you will probably see: 161 | 162 | ``` 163 | -bash: 164 | /tmp/test/hello: cannot execute binary file 165 | ``` 166 | 167 | What can we do about it? 168 | 169 | 170 | ### Cross-compilation 171 | 172 | Go 1.5 comes with [outstanding out-of-the-box cross-compilation abilities]( 173 | http://dave.cheney.net/2015/08/22/cross-compilation-with-go-1-5), so if your 174 | container operating system and/or architecture doesn't match your system's, 175 | it's no problem at all! 176 | 177 | To enable cross-compilation, you need to set `GOOS` and/or `GOARCH`. 178 | 179 | For instance, assuming that you are on a 64 bits Mac: 180 | 181 | ```bash 182 | docker run -e GOOS=darwin -e GOARCH=amd64 -v /tmp/crosstest:/go/bin \ 183 | golang go get github.com/golang/example/hello/... 184 | ``` 185 | 186 | The output of cross-compilation is not directly in `$GOPATH/bin`, 187 | but in `$GOPATH/bin/$GOOS_$GOARCH`. In other words, to run the 188 | program, you have to execute `/tmp/crosstest/darwin_amd64/hello`. 189 | 190 | 191 | ### Installing straight to the `$PATH` 192 | 193 | If you are on Linux, you can even install directly to your system 194 | `bin` directories: 195 | 196 | ```bash 197 | docker run -v /usr/local/bin:/go/bin \ 198 | golang get github.com/golang/example/hello/... 199 | ``` 200 | 201 | However, on a Mac, trying to use `/usr` as a volume will not 202 | mount your Mac's filesystem to the container. It will mount 203 | the `/usr` directory of the Moby VM (the small Linux VM 204 | hidden behind the Docker whale icon in your toolbar). 205 | 206 | You can, however, use `/tmp` or something in your home 207 | directory, and then copy it from there. 208 | 209 | 210 | ## Building lean images 211 | 212 | The Go binaries that we produced with this technique are 213 | *statically linked*. This means that they embed all the code 214 | that they need to run, including all dependencies. This 215 | contrasts with *dynamically linked* programs, which don't 216 | contain some basic libraries (like the "libc") and use a system-wide 217 | copy which is resolved at run time. 218 | 219 | This means that we can drop our Go compiled program in 220 | a container, *without anything else*, and it should work. 221 | 222 | Let's try this! 223 | 224 | 225 | ### The `scratch` image 226 | 227 | There is a special image in the Docker ecosystem: `scratch`. 228 | This is an empty image. It doesn't need to be created or 229 | downloaded, since by definition, it is empty. 230 | 231 | Let's create a new, empty directory for our new Go lean image. 232 | 233 | In this new directory, create the following Dockerfile: 234 | 235 | ```dockerfile 236 | FROM scratch 237 | COPY ./hello /hello 238 | ENTRYPOINT ["/hello"] 239 | ``` 240 | 241 | This means: 242 | * start *from scratch* (an empty image), 243 | * add the `hello` file to the root of the image, 244 | * define this `hello` program to be the default thing to execute 245 | when starting this container. 246 | 247 | Then, produce our `hello` binary as follows: 248 | 249 | ```bash 250 | docker run -v $(pwd):/go/bin --rm \ 251 | golang go get github.com/golang/example/hello/... 252 | ``` 253 | 254 | Note: we don't need to set `GOOS` and `GOARCH` here, because 255 | precisely, we want a binary that will run *in a Docker container*, 256 | not on our host system. So leave those variables alone! 257 | 258 | Then, we can build the image: 259 | 260 | ```bash 261 | docker build -t hello . 262 | ``` 263 | 264 | And test it: 265 | 266 | ```bash 267 | docker run hello 268 | ``` 269 | 270 | (This should display `Hello, Go examples!`.) 271 | 272 | Last but not least, check the image's size: 273 | 274 | ```bash 275 | docker images hello 276 | ``` 277 | 278 | If we did everything right, this image should be about 2 MB. Not bad! 279 | 280 | 281 | ### Building something without pushing to GitHub 282 | 283 | Of course, if we had to push to GitHub each time we wanted to compile, 284 | we would waste a lot of time. 285 | 286 | When you want to work on a piece of code and build it within a container, 287 | you can mount a local directory to `/go` in the `golang` container, so that the 288 | `$GOPATH` is persisted across invocations: `docker run -v $HOME/go:/go golang ...`. 289 | 290 | But you can also mount local directories to specific paths, to "override" some 291 | packages (the ones that you have edited locally). Here is a complete example: 292 | 293 | ```bash 294 | # Adapt the two following environment variables if you are not running on a Mac 295 | export GOOS=darwin GOARCH=amd64 296 | mkdir go-and-docker-is-love 297 | cd go-and-docker-is-love 298 | git clone git://github.com/golang/example 299 | cat example/hello/hello.go 300 | sed -i .bak s/olleH/eyB/ example/hello/hello.go 301 | docker run --rm \ 302 | -v $(pwd)/example:/go/src/github.com/golang/example \ 303 | -v $(pwd):/go/bin/${GOOS}_${GOARCH} \ 304 | -e GOOS -e GOARCH \ 305 | golang go get github.com/golang/example/hello/... 306 | ./hello 307 | # Should display "Bye, Go examples!" 308 | ``` 309 | 310 | 311 | ## The special case of the `net` package and CGo 312 | 313 | Before diving into real-world Go code, we have to confess something: 314 | we lied a little bit about the static binaries. If you are using CGo, 315 | or if you are using the `net` package, the Go linker will generate 316 | a dynamic binary. In the case of the `net` package (which a *lot* 317 | of useful Go programs out there are using indeed!), the main culprit 318 | is the DNS resolver. Most systems out there have a fancy, modular name 319 | resolution system (like the *Name Service Switch*) which relies on 320 | plugins which are, technically, dynamic libraries. By default, 321 | Go will try to use that; and to do so, it will produce dynamic 322 | libraries. 323 | 324 | How do we work around that? 325 | 326 | 327 | ### Re-using another distro's libc 328 | 329 | One solution is to use a base image that *has* the essential 330 | libraries needed by those Go programs to function. Almost any 331 | "regular" Linux distro based on the GNU libc will do the trick. 332 | So instead of `FROM scratch`, you would use `FROM debian` or 333 | `FROM fedora`, for instance. The resulting image will be much 334 | bigger now; but at least, the bigger bits will be shared with 335 | other images on your system. 336 | 337 | Note: you *cannot* use Alpine 338 | in that case, since Alpine is using the musl library instead 339 | of the GNU libc. 340 | 341 | 342 | ### Bring your own libc 343 | 344 | Another solution is to surgically extract the files needed, 345 | and place them in your container with `COPY`. The resulting 346 | container will be small. However, this extraction process 347 | leaves the author with the uneasy impression of a really 348 | dirty job, and they would rather not go into more details. 349 | 350 | If you want to see for yourself, look around `ldd` and the 351 | Name Service Switch plugins mentioned earlier. 352 | 353 | 354 | ### Producing static binaries with `netgo` 355 | 356 | We can also instruct Go to *not* use the system's libc, and 357 | substitute Go's `netgo` library, which comes with a native 358 | DNS resolver. 359 | 360 | To use it, just add `-tags netgo -installsuffix netgo` to 361 | the `go get` options. 362 | 363 | - `-tags netgo` instructs the toolchain to use netgo. 364 | * `-installsuffix netgo` will make sure that the resulting 365 | libraries (if any) are placed in a different, non-default 366 | directory. This will avoid conflicts between code built 367 | with and without netgo, if you do multiple `go get` 368 | (or `go build`) invocations. If you build in containers 369 | like we have shown so far, this is not strictly necessary, 370 | since there will be no other Go code compiled in this 371 | container, ever; but it's a good idea to get used to it, 372 | or at least know that this flag exists. 373 | 374 | 375 | ## The special case of SSL certificates 376 | 377 | There is one more thing that you have to worry about if 378 | your code has to validate SSL certificates; for instance 379 | if it will connect to external APIs over HTTPS. In that 380 | case, you need to put the root certificates in your 381 | container too, because Go won't bundle those into your 382 | binary. 383 | 384 | 385 | ### Installing the SSL certificates 386 | 387 | Three again, there are multiple options available, but 388 | the easiest one is to use a package from an existing 389 | distribution. 390 | 391 | Alpine is a good candidate here because it's so tiny. 392 | The following `Dockerfile` will give you a base image 393 | that is small, but has an up-to-date bundle of root 394 | certificates: 395 | 396 | ```dockerfile 397 | FROM alpine:3.4 398 | RUN apk add --no-cache ca-certificates apache2-utils 399 | ``` 400 | 401 | Check it out; the resulting image is only 6 MB! 402 | 403 | Note: the `--no-cache` option tells `apk` (the Alpine 404 | package manager) to get the list of available packages 405 | from Alpine's distribution mirrors, without saving it 406 | to disk. You might have seen Dockerfiles doing something 407 | like `apt-get update && apt-get install ... && rm -rf /var/cache/apt/*`; 408 | this achieves something equivalent (i.e. not leave package 409 | caches in the final image) with a single flag. 410 | 411 | *As an added bonus,* putting your application in a container 412 | based on the Alpine image gives you access to a ton of really 413 | useful tools: now you can drop a shell into your container 414 | and poke around while it's running, if you need to! 415 | 416 | 417 | ## Rewriting simple micro-services in Go 418 | 419 | Still with us? Great! Now that we know how to build simple Go programs 420 | and put them in lean containers, we propose to take an existing application 421 | and rewrite it in Go to see the insane space savings that we can achieve. 422 | 423 | Don't panic, the application will be pretty simple: it is a demo app 424 | part of a Docker orchestration workshop. It is built around a microservices 425 | architecture. One service is in Ruby, two are in Python, one is in node... 426 | But each of them is less than 50 lines of code. 427 | 428 | 429 | ### Discovering the application 430 | 431 | To see what the application is like, just clone the code and run it 432 | with compose: 433 | 434 | ```bash 435 | git clone git://github.com/jpetazzo/orchestration-workshop 436 | cd orchestration-workshop/dockercoins 437 | docker-compose up -d 438 | ``` 439 | 440 | The build will take a few minutes (or a while, if the internet connection 441 | is slow); then, once the application is running, point your browser 442 | to port 8000 of your Docker installation. If you are on Linux, or using 443 | Docker Mac, that will be http://localhost:8000; with some older betas 444 | of Docker Mac, you might have to use http://docker.local:8000/; and 445 | if you are using Docker Machine, 446 | use `docker-machine ip` to see the IP address of your Docker VM.. 447 | 448 | The app should show a performance graph that will be around 3-4 units 449 | per second. 450 | 451 | You can scale the application with `docker-compose scale worker=3`. 452 | Try with higher values. You will see that the performance peaks at 10 453 | hashes per second. 454 | 455 | 456 | ### Analyzing the performance problem 457 | 458 | If you want to go the long route, you can check the [complete slides 459 | of the orchestration workshop](http://view.dckr.info:8080/), but if 460 | you want to dive in and start writing Go immediately, check the next 461 | paragraph! 462 | 463 | 464 | ### What's wrong with this application? 465 | 466 | The `rng` service is using Python's default WSGI server, which is 467 | mono-process, mono-request, mono-everything. It can only handle 468 | one request at a time, and it sleeps 0.1s before serving each 469 | request, so that's why the performance graph peaks at 10 hashes/s. 470 | 471 | 472 | ### Solving the performance problem 473 | 474 | We want to rewrite the `rng` service in Go! If you want to do some 475 | reverse engineering, go ahead, check the repo, find out where 476 | the source code lives, and try to guess what the `rng` service 477 | does and how to reimplement it in Go. If you want better intructions 478 | (or if you can't find out because the code is too obscure), go 479 | to the next paragraph! 480 | 481 | 482 | ### The `rng` service 483 | 484 | The `rng` service (Random Number Generator) is a simple web service 485 | that generates random data. It expects requests to `/` 486 | and generates a random binary payload of size ``. 487 | For instance, when the application is running, you can execute 488 | the following command: 489 | 490 | ```bash 491 | docker run --net dockercoins_default centos curl http://rng/10 492 | ``` 493 | 494 | This will print 10 random bytes on the terminal (binary data, 495 | so this might garble your terminal, and you probably won't see 496 | exactly 10 characters since some of them might be non printable). 497 | 498 | We need to reimplement this service in Go! 499 | 500 | 501 | ### A few hints 502 | 503 | We recommend that you check the following packages: 504 | - https://golang.org/pkg/math/rand/ 505 | - https://golang.org/pkg/net/http/ 506 | - https://golang.org/pkg/time/ (if you want to play ball and 507 | sleep 0.1s before serving each request!) 508 | 509 | If you do it "right", you will either see the same performance 510 | (if you handle one request at a time) or a performance gain 511 | (if you handle concurrent requests). 512 | 513 | 514 | ### Building lean containers with Docker Compose 515 | 516 | Note that instead of just building with `docker-compose build`, now 517 | you need to build in two steps: 518 | * first, compile your Go binaries; 519 | * then, build your Docker images using those binaries. 520 | 521 | ## Hacking on Docker itself 522 | 523 | If you want to work use your Go skills to improve Docker itself, here are a few 524 | suggestions of GitHub issues targeted for Docker 1.12 that could use your help. 525 | These range from the beginner level to the intermediate level. 526 | 527 | - networking/swarm mode: Make gossip timeout configurable: https://github.com/docker/docker/issues/24557 528 | - Proposal: Replace secrets with "join tokens": https://github.com/docker/docker/issues/24430 529 | - `docker swarm join` should take multiple addresses: https://github.com/docker/docker/issues/24085 530 | - Improve cli help for --update-delay flag: https://github.com/docker/docker/issues/24083 531 | - [1.12-rc2] daemon.ID should be replaced with swarm NodeID: https://github.com/docker/docker/issues/24095 532 | - Allow forcing all swarm tasks to reschedule for a service: https://github.com/docker/docker/issues/24469 533 | - docker swarm update without arguments should show usage: https://github.com/docker/docker/issues/24352 534 | - Increase integration test coverage of swarm mode: https://github.com/docker/docker/issues/24240 535 | - Swarm Integration Test Suite is Poorly Documented: https://github.com/docker/docker/issues/24564 536 | 537 | Of course, there are plenty of other things to work on. You can look at 538 | Docker's issue tracker on GitHub for ideas: https://github.com/docker/docker/issues 539 | 540 | Feel free to ask us for help or advice! 541 | -------------------------------------------------------------------------------- /qrcode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jpetazzo/go-docker-/3a988e62807d3ee34d40355572ff68083a008b4c/qrcode.png --------------------------------------------------------------------------------