Repository: eficode-academy/docker-katas
Branch: master
Commit: 9b6466577126
Files: 108
Total size: 146.3 KB
Directory structure:
gitextract_5i3fxtl4/
├── .gitignore
├── .phlow
├── CHEATSHEET.md
├── LICENSE
├── README.md
├── labs/
│ ├── 00-getting-started.md
│ ├── 01-hello-world.md
│ ├── 02-running-images.md
│ ├── 03-deletion.md
│ ├── 04-port-forward.md
│ ├── 05-executing.md
│ ├── 06-volumes.md
│ ├── 07-building-an-image.md
│ ├── 08-multi-stage-builds.md
│ ├── 09-multi-container.md
│ ├── README.md
│ ├── advanced/
│ │ ├── containers-on-default-bridge.md
│ │ ├── containers-on-docker-compose-network.md
│ │ ├── containers-on-host-network.md
│ │ ├── containers-on-none-network.md
│ │ ├── containers-on-user-defined-bridge.md
│ │ ├── joining-network-and-process-namespace-of-existing-containers.md
│ │ ├── systemd/
│ │ │ ├── Dockerfile
│ │ │ ├── README.md
│ │ │ └── docker-compose.yml
│ │ └── traefik/
│ │ ├── example1/
│ │ │ ├── README.md
│ │ │ ├── docker-compose.yml
│ │ │ └── traefik.toml
│ │ ├── example2/
│ │ │ ├── README.md
│ │ │ ├── proxy/
│ │ │ │ ├── docker-compose.yml
│ │ │ │ └── traefik.toml
│ │ │ └── web/
│ │ │ └── docker-compose.yml
│ │ └── example3/
│ │ ├── acme.json
│ │ ├── docker-compose.yml
│ │ └── traefik.toml
│ ├── building-an-image/
│ │ ├── Dockerfile
│ │ ├── app.py
│ │ └── requirements.txt
│ ├── docker-compose/
│ │ ├── .gitignore
│ │ ├── 00-getting-started.md
│ │ ├── 01-hello-world.md
│ │ ├── 02-port-forward.md
│ │ ├── 03-volumes.md
│ │ ├── 04-build-image.md
│ │ └── bottle/
│ │ ├── Dockerfile
│ │ ├── app.py
│ │ └── requirements.txt
│ ├── exercise-template.md
│ ├── extra-exercises/
│ │ ├── image-security.md
│ │ ├── nginx/
│ │ │ ├── docker-compose.yml
│ │ │ ├── html1/
│ │ │ │ └── index.html
│ │ │ ├── html2/
│ │ │ │ └── index.html
│ │ │ ├── network.md
│ │ │ └── nginx/
│ │ │ └── nginx.vh.default.conf
│ │ └── secrets.md
│ ├── image-best-practices.md
│ ├── multi-container/
│ │ ├── docker-compose.yaml
│ │ └── docker-compose_final.yaml
│ ├── multi-stage-build/
│ │ ├── Dockerfile
│ │ ├── go.mod
│ │ └── hello.go
│ ├── sharing-images.md
│ ├── volumes/
│ │ └── index.html
│ └── windows-docker/
│ ├── win1-windows-on-linux.md
│ ├── win2-windows-on-windows.md
│ └── win3-common-commands.md
└── trainer/
├── README.md
├── examples/
│ ├── basic-compose/
│ │ ├── Dockerfile
│ │ ├── README.md
│ │ ├── app.py
│ │ ├── docker-compose.yml
│ │ └── requirements.txt
│ ├── building-flask-on-different-os/
│ │ ├── Dockerfile
│ │ ├── Dockerfile-python
│ │ ├── Dockerfile-python-alpine
│ │ ├── README.md
│ │ ├── app.py
│ │ ├── build.sh
│ │ └── requirements.txt
│ ├── ci-tools/
│ │ └── README.md
│ ├── multi-stage-golang/
│ │ ├── Dockerfile
│ │ ├── README.md
│ │ └── hello.go
│ ├── multistage-java/
│ │ ├── Dockerfile
│ │ ├── Dockerfile-multistage
│ │ ├── README.md
│ │ ├── build.gradle
│ │ ├── gradle.properties
│ │ ├── micronaut-cli.yml
│ │ ├── settings.gradle
│ │ └── src/
│ │ ├── main/
│ │ │ ├── java/
│ │ │ │ └── example/
│ │ │ │ └── micronaut/
│ │ │ │ ├── Application.java
│ │ │ │ ├── HelloController.java
│ │ │ │ ├── ReadyController.java
│ │ │ │ └── RootController.java
│ │ │ └── resources/
│ │ │ ├── application.yml
│ │ │ └── logback.xml
│ │ └── test/
│ │ └── java/
│ │ └── example/
│ │ └── micronaut/
│ │ └── HelloControllerTest.java
│ ├── nextcloud/
│ │ ├── README.md
│ │ └── docker-compose.yml
│ ├── scratch/
│ │ ├── Dockerfile
│ │ ├── README.md
│ │ └── emptyfile
│ ├── security-run-as-root/
│ │ └── README.md
│ └── volume-python/
│ ├── README.md
│ └── main.py
└── experiments/
└── unicode/
├── README.md
├── Файлдокера
└── ఖాళీగా
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
# Created by https://www.gitignore.io/api/csharp
### Csharp ###
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
# User-specific files
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
# Visual Studio 2015 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUNIT
*.VisualState.xml
TestResult.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
**/Properties/launchSettings.json
*_i.c
*_p.c
*_i.h
*.ilk
*.meta
*.obj
*.pch
*.pdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*.log
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# JustCode is a .NET coding add-in
.JustCode
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# Visual Studio code coverage results
*.coverage
*.coveragexml
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# TODO: Uncomment the next line to ignore your web deploy settings.
# By default, sensitive information, such as encrypted password
# should be stored in the .pubxml.user file.
#*.pubxml
*.pubxml.user
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# The packages folder can be ignored because of Package Restore
**/packages/*
# except build/, which is used as an MSBuild target.
!**/packages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/packages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
# SQL Server files
*.mdf
*.ldf
*.ndf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/
# Typescript v1 declaration files
typings/
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# JetBrains Rider
.idea/
*.sln.iml
# CodeRush
.cr/
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
# Telerik's JustMock configuration file
*.jmconfig
# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs
# End of https://www.gitignore.io/api/csharp
.vscode/settings.json
================================================
FILE: .phlow
================================================
[default]
remote = origin
service = github
integration_branch = master
issue_url = https://api.github.com
delivery_branch_prefix = ready
================================================
FILE: CHEATSHEET.md
================================================
Source: https://docs.docker.com/get-started/docker_cheatsheet.pdf
## General
- [Docker cli](https://docs.docker.com/engine/reference/commandline/cli/) - **Docker CLI** is the command line interface for Docker
- [Docker Desktop](https://docs.docker.com/desktop) - **Docker Desktop** is available for Mac, Linux, and Windows
- `docker --help` - Get help with Docker. You can use `--help` on all subcommands
- `docker info` - Display system-wide information
- [Docker Docs](https://docs.docker.com) - Check out our docs for information on using Docker
## Containers
- `docker run --name <container_name> <image_name>` - Create and run a container from an image, with a custom name
- `docker run -p <host_port>:<container_port> <image_name>` - Run a container and publish a container’s port(s) to the host
- `docker run -d <image_name>` - Run a container in the background
- `docker start|stop <container_name> (or <container-id>)` - Start or stop an existing container
- `docker rm <container_name>` - Remove a stopped container
- `docker exec -it <container_name> sh` - Open a shell inside a running container
- `docker logs -f <container_name>` - Fetch and follow the logs of a container
- `docker inspect <container_name> (or <container_id>)` - Inspect a running container
- `docker ps` - List currently running containers
- `docker ps --all` - List all docker containers (running and stopped)
- `docker container stats` - View resource usage stats
## Images
- `docker build -t <image_name>` - Build an Image from a Dockerfile
- `docker build -t <image_name> . --no-cache` - Build an Image from a Dockerfile without the cache
- `docker images` - List local images
- `docker rmi <image_name>` - Delete an Image
- `docker image prune` - Remove all unused images
## Docker Registries
The default registry is [Docker Hub](https://hub.docker.com), but you can add more registries.
- `docker login -u <username>` - Login into Docker
- `docker push <username>/<image_name>` - Publish an image to Docker Hub
- `docker search <image_name>` - Search Hub for an image
- `docker pull <image_name>` - Pull an image from Docker Hub
- `docker tag <image_name>:<tag> <username>/<image_name>:<tag>` - Tag an image for a registry
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2018 Praqma
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
# Docker katas
[](https://console.cloud.google.com/cloudshell/editor?cloudshell_git_repo=https://github.com/eficode-academy/docker-katas.git)
This workshop will take you from "Hello Docker" to deploying a containerized web application to a server.
It's going to be a lot of fun!
## Prerequisites
You need to have access to a machine with docker installed.
There are many ways of getting that:
* Click the link above to get a Cloud shell from Google (require login)
* Docker installed on a linux box.
* Docker desktop installed on a Mac or Windows machine.
## Philosophy
There are a few things you should know about this tutorial before we begin.
This tutorial is designed to be **self-paced** to make the most of your time.
The exercises won't always tell you exactly what you need to do.
Instead, it will point you to the right resources (like documentation and blog posts) to find the answer.
Ready to begin?
---------------
Head over to [the first lab](labs/00-getting-started.md) to begin.
## Cheat sheet
For a quick reference of the most common docker commands, see [CHEATSHEET.md](CHEATSHEET.md).
================================================
FILE: labs/00-getting-started.md
================================================
# Getting started
In this section you will install docker.
## Terminology
Throughout these labs, you will see a lot of Docker-specific jargon which might be confusing to some. So before you go further, let's clarify some terminology that is used frequently in the Docker ecosystem.
*Docker Container*: An isolated, runnable environment that holds everything needed to run an application.
*Docker Image*: A lightweight, standalone package that contains all necessary code, libraries, and dependencies to run an application.
- *Docker daemon* - The background service running on the host that manages building, running and distributing Docker containers.
- *Docker client* - The command line tool that allows the user to interact with the Docker daemon.
- *Docker Hub* - A [docker registry](https://hub.docker.com/explore/) of Docker images. You can think of the registry as a directory of all available Docker images. You'll be using this later in this tutorial.
## Installing Docker
Depending on what OS you are running, installation is different, but head over to the [Get started](https://www.docker.com/get-started/) website and follow the instructions there.
================================================
FILE: labs/01-hello-world.md
================================================
# hello-world
## Learning Goals
The goal of this scenario is to make you run your first Docker container.
## Terminology
*Docker Container*: An isolated, runnable environment that holds everything needed to run an application.
*Docker Image*: A lightweight, standalone package that contains all necessary code, libraries, and dependencies to run an application.
## Exercise
Try running a command with Docker:
```
docker run hello-world
```
Your terminal output should look like this:
```
Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
78445dd45222: Pull complete
Digest: sha256:c5515758d4c5e1e838e9cd307f6c6a0d620b5e07e6f927b07d05f6d12a1ac8d7
Status: Downloaded newer image for hello-world:latest
Hello from Docker!
This message shows that your installation appears to be working correctly.
To generate this message, Docker took the following steps:
1. The Docker client contacted the Docker daemon.
2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
3. The Docker daemon created a new container from that image which runs the
executable that produces the output you are currently reading.
4. The Docker daemon streamed that output to the Docker client, which sent it
to your terminal.
To try something more ambitious, you can run an Ubuntu container with:
$ docker container run -it ubuntu bash
Share images, automate workflows, and more with a free Docker ID:
https://cloud.docker.com/
For more examples and ideas, visit:
https://docs.docker.com/engine/userguide/
```
This message shows that your installation appears to be working correctly.
_*Q: So what did this do?*_
Try to run `docker run hello-world` again.
Docker has now already downloaded the image locally, and can therefore execute the container straight away.
================================================
FILE: labs/02-running-images.md
================================================
# Running your first container from image
## Learning Goals
- Run an [Alpine Linux](http://www.alpinelinux.org/) container (a lightweight linux distribution) on your system and get a taste of the `docker run` command.
## Introduction
To get started with running your first container from an image, you'll first pull the Alpine Linux image, a lightweight Linux distribution, and then explore various commands to interact with it.
## Exercise
### Overview
- Pull the Alpine Linux image.
- List all images on your system.
- Run a Docker container based on the Alpine image.
- Explore various commands inside the container.
- Understand container naming and IDs.
### Step by step instructions
To get started, let's run the following in our terminal:
* `docker pull alpine`
The `pull` command fetches the alpine **image** from the **Docker registry** and saves it in your system. You can use the `docker image ls` command to see a list of all images on your system.
* `docker image ls`
Expected output (your list of images will look different):
``` bash
REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE
alpine latest c51f86c28340 4 weeks ago 1.109 MB
hello-world latest 690ed74de00f 5 months ago 960 B
```
## 1.1 docker run
Let's run a Docker **container** based on this image.
* `docker run alpine ls -l`
Expected output:
```bash
total 48
drwxr-xr-x 2 root root 4096 Mar 2 16:20 bin
drwxr-xr-x 5 root root 360 Mar 18 09:47 dev
drwxr-xr-x 13 root root 4096 Mar 18 09:47 etc
drwxr-xr-x 2 root root 4096 Mar 2 16:20 home
drwxr-xr-x 5 root root 4096 Mar 2 16:20 lib
......
......
```
When you run `docker run alpine`, you provided a command (`ls -l`), so Docker started the command specified and you saw the listing.
Try run the following:
* `docker run alpine echo "hello from alpine"`
Expected output:
``` bash
hello from alpine
```
<details>
<summary>More Details</summary>
In this case, the Docker client ran the `echo` command in our alpine container and then exited it. If you've noticed, all of that happened pretty quickly. Imagine booting up a virtual machine, running a command and then killing it. Now you know why they say containers are fast!
</details>
Try another command:
* `docker run alpine /bin/sh`
Wait, nothing happened! Is that a bug?
Well, no.
These interactive shells will exit after running any scripted commands, unless they are run in an interactive terminal - so for this example to not exit, you need to add the parameters `i` and `t`.
> :bulb: The flags `-it` are short for `-i -t` which again are the short forms of `--interactive` (Keep STDIN open) and `--tty` (Allocate a terminal).
* `docker run -it alpine /bin/sh`
You are inside the container shell and you can try out a few commands like `ls -l`, `uname -a` and others.
* Exit out of the container by giving the `exit` command.
Ok, now it's time to list our containers.
The `docker ps` command shows you all containers that are currently running.
* `docker ps`
Expected output:
```
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
```
Notice that you have no running containers. When you wrote `exit` in the shell, the primary process (`/bin/sh`) stopped. No containers are running, you see a blank line. Let's try a more useful variant, listing all containers, both stopped and started.
* `docker ps -a`
Expected output:
```
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
36171a5da744 alpine "/bin/sh" 5 minutes ago Exited (0) 2 minutes ago fervent_newton
a6a9d46d0b2f alpine "echo 'hello from alp" 6 minutes ago Exited (0) 6 minutes ago lonely_kilby
ff0a5c3750b9 alpine "ls -l" 8 minutes ago Exited (0) 8 minutes ago elated_ramanujan
c317d0a9e3d2 hello-world "/hello" 34 seconds ago Exited (0) 12 minutes ago stupefied_mcclintock
```
What you see above is a list of all containers that you ran. Notice that the `STATUS` column shows that these containers exited a few minutes ago.
## Naming your container
Take a look again at the output of the `docker ps -a`:
* `docker ps -a`
Expected output:
```
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
36171a5da744 alpine "/bin/sh" 5 minutes ago Exited (0) 2 minutes ago fervent_newton
a6a9d46d0b2f alpine "echo 'hello from alp" 6 minutes ago Exited (0) 6 minutes ago lonely_kilby
ff0a5c3750b9 alpine "ls -l" 8 minutes ago Exited (0) 8 minutes ago elated_ramanujan
c317d0a9e3d2 hello-world "/hello" 34 seconds ago Exited (0) 12 minutes ago stupefied_mcclintock
```
All containers have an **ID** and a **name**.
Both the ID and name is generated every time a new container spins up with a random seed for uniqueness.
> :bulb: Tip: If you want to assign a specific name to a container then you can use the `--name` option. That can make it easier for you to reference the container going forward.
## Summary
That concludes a whirlwind tour of the `docker run` command which would most likely be the command you'll use most often. It makes sense to spend some time getting comfortable with it. To find out more about `run`, use `docker run --help` to see a list of all flags it supports. As you proceed further, we'll see a few more variants of `docker run`.
================================================
FILE: labs/03-deletion.md
================================================
# Throw your container away
As containers are just a thin base layer on top of the host kernel, it is really fast to spin up a new instance if you crashed your old one.
Let's try to run an alpine container and delete the file system.
Spin up the container with `docker run -ti alpine`
and then list all the folders on the root level to see the whole distribution:
```
ls /
```
Expected output:
```
bin etc lib mnt root sbin sys usr
dev home media proc run srv tmp var
```
List the current user:
``` bash
whoami
```
Expected output:
```
root
```
List the current date:
``` bash
date
```
Expected output:
```
Wed Nov
```
> **Warning:** Make sure that you are inside your container. most likely you can see that by your command promt showing `/ #` instead of `ubuntu@inst1:~/docker-katas/labs$`
Now, delete the binaries that the system is build up of with:
```
rm -rf /bin
```
Try to navigate around to see how much of the OS is gone: try to run the `ls`, `whoami` and `date` commands.
They should all echo back that the binary is not found.
Exit out by pressing `Ctrl+d` and create a new instance of the Alpine image and look a bit around:
```
docker run -it alpine
```
In the container run:
```
ls /
```
Expected output:
```
bin etc lib mnt root sbin sys usr
dev home media proc run srv tmp var
```
Try to perform the same tasks as displayed above to see that you have a fresh new instance ready to go.
## Auto-remove a container after use
Every time you create a new container, it will take up some space, even though it usually is minimal.
To see what your containers are taking up of space try to run the `docker container ls -as` command.
```bash
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES SIZE
4b09b2fe1d8c alpine "/bin/sh" 7 seconds ago Exited (1) 1 second ago silly_jones 0B (virtual 3.97MB)
```
Here you can see that the alpine image itself takes 3.97MB, and the container itself takes 0B. When you begin to manipulate files in your container, the size of the container will rise.
If you are creating a lot of new containers eg. to test something, you can tell the Docker daemon to remove the container once stopped with the `--rm` option:
`docker run --rm -it alpine`
This will remove the container immediately after it is stopped.
## Cleaning up containers you do not use anymore
Containers are still persisted, even though they are stopped.
If you want to delete them from your server you can use the `docker rm` command.
`docker rm` can take either the `CONTAINER ID` or `NAME` as seen above.
Try to remove the `hello-world` container:
```
docker container ls -a
```
Expected output:
```
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
6a9246ff53cb hello-world "/hello" 18 seconds ago Exited (0) 16 seconds ago ecstatic_cray
```
Delete the container:
```
docker container rm ecstatic_cray
```
The name or ID specified is echoed back:
```
ecstatic_cray
```
The container is now gone when you execute a `ls -a` command.
> :bulb: **Tip:** As with Git, you can use any unique part of the container ID to refer to it.
### Deleting images
You deleted the container instance above, but not the image of hello-world itself. And as you are now on the verge to become a docker expert, you do not need the hello-world image anymore so let us delete it.
First off, list all the images you have downloaded to your computer:
```
docker image ls
```
Expected output:
```
REPOSITORY TAG IMAGE ID CREATED SIZE
alpine latest 053cde6e8953 9 days ago 3.97MB
hello-world latest 48b5124b2768 10 months ago 1.84kB
```
Here you can see the images downloaded as well as their size.
To remove the hello-world image use the `docker image rm` command together with the id of the docker image.
```
docker image rm 48b5124b2768
```
Expected output:
```
Untagged: hello-world:latest
Untagged: hello-world@sha256:c5515758d4c5e1e838e9cd307f6c6a0d620b5e07e6f927b07d05f6d12a1ac8d7
Deleted: sha256:48b5124b2768d2b917edcb640435044a97967015485e812545546cbed5cf0233
Deleted: sha256:98c944e98de8d35097100ff70a31083ec57704be0991a92c51700465e4544d08
```
What docker did here was to `untag` the image removing the references to the sha of the image. After the image has no references, it deletes the two layers the image itself is comprised of.
### Cleaning up
When building, running and rebuilding images, you download and store a lot of layers. These layers will not be deleted, as docker takes a very conservative approach to clean up.
Docker provides a `prune` command, taking all dangling containers/images/networks/volumes.
- `docker container prune`
- `docker image prune`
- `docker network prune`
- `docker volume prune`
The docker image prune command allows you to clean up unused images. By default, docker image prune only cleans up dangling images. A dangling image is one that is not tagged and is not referenced by any container. To remove all _unused_ resources, resources that are not directly used by any existing containers, use the `-a` switch as well.
If you want a general cleanup, then `docker system prune` is your friend.
## Summary
You have now seen the swiftness of creating a new container from an image, trash it, and create a new one on top of it.
You have learned to use `container rm` for deleting containers, `image rm` for images, `image ls` for listing the images and `container ls -a` to look at all the containers on your host.
================================================
FILE: labs/04-port-forward.md
================================================
# A basic webserver
Running arbitrary Linux commands inside a Docker container is fun, but let's do something more useful.
Pull down the `nginx` Docker image from the Docker Hub. This Docker image uses the [Nginx](http://nginx.org/) webserver to serve a static HTML website.
Start a new container from the `nginx` image that exposes port 80 from the container to port 8080 on your host. You will need to use the `-p` flag with the docker container run command.
> :bulb: Mapping ports between your host machine and your containers can get confusing.
> Here is the syntax you will use:
>
> ```
> docker run -p 8080:80 nginx
> ```
>
> The trick is to remember that **the host port always goes to the left**,
> and **the container port always goes to the right.**
> Remember it as traffic coming _from_ the host, _to_ the container.
Open a web browser and go to port 8080 on your host. The exact address will depend on how you're running Docker today:
- **Native Linux** - [http://localhost:8080](http://localhost:8080)
- **Cloud server** - Make sure firewall rules are configured to allow traffic on port 8080. Open browser and use the hostname (or IP) for your server.
Ex: [http://inst1.prefix.eficode.academy:8080](http://inst1.prefix.eficode.academy:8080) -
Alternatively open a new shell and issue `curl localhost:8080`
- **Google Cloud Shell** - Open Web Preview (upper right corner)
If you see a webpage saying "Welcome to nginx!" then you're done!
If you look at the console output from docker, you see nginx producing a line of text for each time a browser hits the webpage:
```
docker run -p 8080:80 nginx
```
Expected output:
```
172.17.0.1 - - [31/May/2017:11:52:48 +0000] "GET / HTTP/1.1" 200 612 "-" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:53.0) Gecko/20100101 Firefox/53.0" "-
```
Press **control + c** in your terminal window to stop your container.
## Running the webserver container in the background
When running a webserver like nginx, it is very useful to not run the container in the foreground of our terminal.
Instead we should make it run in the background, freeing up our terminal for other things.
Docker enables this with the `-d` parameter for the `run` command.
For example: `docker run -d -p 8080:80 nginx`
```
docker run -p 8080:80 -d nginx
```
Docker prints out the container ID and returns to the terminal.
Congratulations! You have just started a container in the background. :tada:
## Cleanup
Stop the container you just started.
Remember that your container ID is different from the one in the example.
```bash
docker stop 78c943461b49584ebdf841f36d113567540ae460387bbd7b2f885343e7ad7554
```
Docker prints out the ID of the stopped container.
```
78c943461b49584ebdf841f36d113567540ae460387bbd7b2f885343e7ad7554
```
================================================
FILE: labs/05-executing.md
================================================
# Executing processes in your container
It you want to examine a running container, but do not want to disturb the running process you can execute another process inside the container with `exec`.
This could be a shell, or a script of some sort. In that way you can debug an existing environment before starting a new up.
## Exercise
In this exercise, we want to change a file in an already running container, by executing a secondary process.
### Step by step
- Spin up a new NGINX container: `docker run -d -p 8080:80 nginx`
- Visit the webpage to make sure that NGINX have been setup correctly.
Step into a new container by executing a bash shell inside the container:
```
docker exec -it CONTAINERNAME bash
```
> :bulb: note that the CONTAINERNAME is the name of the NGINX container you just started.
Inside, we want to edit the `index.html` page, with a cli text editor called [nano](https://www.nano-editor.org/).
Because containers only have the bare minimum installed, we need to first install nano, and then use it:
> :bulb: From the [DockerHub description](https://hub.docker.com/_/nginx) we know that the standard place for HTML pages NGINX serves is in /usr/share/nginx/html
- install nano on the container: `apt-get update && apt-get install -y nano`
- Edit the index html page: `nano /usr/share/nginx/html/index.html`
- Save and exit nano by pressing: `CTRL + O` and `enter` to save and `CTRL + X` to exit Nano
- Revisit the page to check that your edition is in effect.
## Summary
You have tried to start a new process by the `exec` command in order to look around in a container, or to edit something.
You have also seen that terminating any of the the processes created with `docker exec` will not make the container stop.
================================================
FILE: labs/06-volumes.md
================================================
# Docker volumes
> _Hint: This lab only covers volumes on Docker for Linux. If you are on windows or mac, things can look different._
Containers should be ephemeral.
The whole idea is that you can start, stop and delete the containers without losing data.
But how can we do that for persistent workloads like databases?
We need a way to store data outside of the containers.
You have two different ways of mounting data from your container `bind mounts` and `volumes`.
### Bind mount
Is the simpler one to understand. It takes a host path, like `/data`, and mounts it inside your container eg. `/opt/app/data`.
The good thing about bind mount is that they are easy and allow you to connect directly to the host filesystem.
The downside is that you need to specify it at runtime, and path to mount might vary from host to host, which can be confusing when you want to run your containers on different hosts.
With bind mount you will also need to deal with backup, migration etc. in an tool outside the Docker ecosystem.
As an example, let's look at the [Nginx](https://hub.docker.com/_/nginx/) container.
The server itself is of little use, if it cannot access our web content on the host.
We need to create a mapping between the host system, and the container with the `-v` command:
```bash
docker run --name some-nginx -v /some/content:/usr/share/nginx/html:ro -d nginx
```
That will map whatever files are in the `/some/content` folder on the host to `/usr/share/nginx/html` in the container.
> The `:ro` attribute is making the host volume read-only, making sure the container can not edit the files on the host.
### A docker Volume
This is where you can use a `named` or `unnamed` volume to store the external data. The data will still be stored locally unless you have configured a storage driver for your system (Ops things, not covered here).
Volumes are entities inside docker, and can be created in three different ways.
- By explicitly creating it with the `docker volume create <volume_name>` command.
- By creating a named volume at container creation time with `docker container run -d -v DataVolume:/opt/app/data nginx`
- By creating an anonymous volume at container creation time with `docker container run -d -v /opt/app/data nginx`
In the next section, you will get to try all of them.
## Step-by-Step Instructions
### Bind mount
Try to do the following:
- `git clone` this repository down. :bulb: If you are at training the repository is already cloned on your training workstation.
- Navigate to the `labs/volumes/` directory, which contains a file we can try to serve: `index.html`.
- We need change `/some/content` to the right path, it must be an absolute path, starting from the root of the filesystem, (which in linux is `/`). You can use the command `pwd` (Print working directory) to display the path to where you are.
- Now try to run the container with the `labs/volumes` directory bind mounted.
This will give you a nginx server running, serving your static files... _But on which port?_
- Run a `docker ps` command to find out if it has any ports forwarded from the host.
Remember the [past exercise](04-port-forward.md) on port forwarding in Docker.
- Make it host the site on port 8080
<details>
<summary>How to do this?</summary>
The parameter `-p 8080:80` will map port 80 in the container to port 8080 on the host.
</details>
- Check that it is running by navigating to the hostname or IP with your browser, and on port 8080.
- Stop the container with `docker stop <container_name>`.
### Volumes
First off, lets try to make a data volume called `data`:
```bash
docker volume create data
```
Docker creates the volume and outputs the name of the volume created.
If you run `docker volume ls` you will have a list of all the volumes created and their driver:
```outputs
DRIVER VOLUME NAME
local data
```
Unlike the bind mount, you do not specify where the data is stored on the host.
In the volume API, like for almost all other of Docker’s APIs, there is an `inspect` command giving you low level details.
- run `docker volume inspect data` to see where the data is stored on the host.
```json
[
{
"Driver": "local",
"Labels": {},
"Mountpoint": "/var/lib/docker/volumes/data/_data",
"Name": "data",
"Options": {},
"Scope": "local"
}
]
```
You can see that the `data` volumes is mounted at `/var/lib/docker/volumes/data/_data` on the host.
> **Note** we will not go through the different drivers. For more info look at Dockers own [example](https://docs.docker.com/engine/admin/volumes/volumes/#use-a-volume-driver).
You can now use this data volume in all containers. Try to mount it to an nginx server with the `docker container run --rm --name www -d -p 8080:80 -v data:/usr/share/nginx/html nginx` command.
> **Note:** If the volume refer to is empty and we provide the path to a directory that contains data in the base image, that data will be copied into the volume.
Try now to look at the data stored in `/var/lib/docker/volumes/data/_data` on the host:
```
sudo ls /var/lib/docker/volumes/data/_data/
```
Expected output:
```
50x.html index.html
```
Those two files comes from the Nginx image and is the standard files the webserver has.
### Attaching multiple containers to a volume
Multiple containers can attach to the same volume with data.
Docker doesn't handle any file locking, so applications must account for the file locking themselves.
Let's try to go in and make a new html page for nginx to serve.
We do this by making a new ubuntu container that has the `data` volume attached to `/tmp`, and thereafter create a new html file with the `echo` command:
Start the container:
```
docker run -it --rm -v data:/tmp ubuntu bash
```
In the container run:
```
echo "<html><h1>hello world</h1></html>" > /tmp/hello.html
```
Verify the file was created by running in the container:
```
ls /tmp
```
Expected output:
```
hello.html 50x.html index.html
```
Head over to your newly created webpage at: `http://workstation-X.Y.eficode.academy:8080/hello.html` where X is your workstation number and Y is the prefix. It should be the same URL as your Workstation.
## cleanup
Exit out of your ubuntu server and execute a `docker stop www` to stop the nginx container.
Run a `docker ps` to make sure that no other containers are running.
```
docker ps
```
Expected output:
```
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
```
The data volume is still present, and will be there until you remove it with a `docker volume rm data` or make a general cleanup of all the unused volumes by running `docker volume prune`.
## Tips and tricks
As you have seen, the `-v` flag can both create a bind mount or name a volume depending on the syntax. If the first argument begins with a / or ~/ you're creating a bind mount. Remove that, and you're naming the volume. For example:
- `-v /path:/path/in/container` mounts the host directory, `/path` at the `/path/in/container`
- `-v path:/path/in/container` creates a volume named path with no relationship to the host.
### Sharing data
If you want to share volumes or bind mount between two containers, then use the `--volumes-from` option for the second container. The parameter maps the mapped volumes from the source container to the container being launched.
## More advanced docker commands
Before you go on, use the [Docker command line interface](https://docs.docker.com/engine/reference/commandline/cli/) documentation to try a few more commands:
- While your detached container is running, use the `docker ps` command to see what silly name Docker gave your container. **This is one command you're going to use often!**
- While your detached container is still running, look at its logs. Try following its logs and refreshing your browser.
- Stop your detached container, and confirm that it is stopped with the `ps` command.
- Start it again, wait 10 seconds for it to fire up, and stop it again.
- Then delete that container from your system.
> **NOTE:** When running most docker commands, you only need to specify the first few characters of a container's ID. For example, if a container has the ID `df4fd19392ba`, you can stop it with `docker stop df4`. You can also use the silly names Docker provides containers by default, such as `boring_bardeen`.
If you want to read more, I recommend [Digital Oceans](https://www.digitalocean.com/community/tutorials/how-to-share-data-between-docker-containers) guides to sharing data through containers, as well as Dockers own article about [volumes](https://docs.docker.com/engine/admin/volumes).
## summary
Now you have tried to bind volumes to a container to connect the host to the container.
================================================
FILE: labs/07-building-an-image.md
================================================
# Constructing a docker image
Running images others made is useful, but if you want to use docker for your own application, chances are you want to construct an image on your own.
A [Dockerfile](https://docs.docker.com/reference/dockerfile/) is a text file containing a list of commands that the Docker daemon calls while creating an image. The Dockerfile contains all the information that Docker needs to know to run the app; a base Docker image to run from, location of your project code, any dependencies it has, and what commands to run at start-up.
It is a simple way to automate the image creation process. The best part is that the [commands](https://docs.docker.com/reference/dockerfile/) you write in a Dockerfile are _almost_ identical to their equivalent Linux commands. This means you don't really have to learn new syntax to create your own Dockerfiles.
## Dockerfile commands summary
Here's a quick summary of some basic commands we will use in our Dockerfile.
> As a rule of thumb, all commands in CAPITAL LETTERS are intended for the docker engine. E.g.
- FROM # base image
- RUN # run a command
- ADD and COPY # copy files into the image
- CMD # run a command at start-up, but can be overridden
- EXPOSE # expose a port
- ENTRYPOINT # run a command at start-up
<details>
<summary>Click to see the full list of commands</summary>
Details:
- `FROM` is always the first item in the Dockerfile. It is a requirement that the Dockerfile starts with the `FROM` command. Images are created in layers, which means you can use another image as the base image for your own. The `FROM` command defines your base layer. As argument, it takes the name of the image. Optionally, you can add the Docker Hub username of the maintainer and image version, in the format `username/imagename:version`.
- `RUN` is used to build up the image you're creating. For each `RUN` command, Docker will run the command then create a new layer of the image. This way you can roll back your image to previous states easily. The syntax for a `RUN` instruction is to place the full text of the shell command after the `RUN` (e.g., `RUN mkdir /user/local/foo`). This will automatically run in a `/bin/sh` shell. You can define a different shell like this: `RUN /bin/bash -c 'mkdir /user/local/foo'`
- `COPY` copies local files into the container. The files need to be in the same folder (or a sub folder) as the Dockerfile itself. An example is copying the requirements for a python app into the container: `COPY requirements.txt /usr/src/app/`.
- `ADD` should only be used if you want to copy and unpack a tar file into the image. In any other case, use `COPY`. `ADD` can also be used to add a file directly from an URL; consider whether this is good practice.
- `CMD` defines the commands that will run on the image at start-up. Unlike a `RUN`, this does not create a new layer for the image, but simply runs the command. There can only be one `CMD` in a Dockerfile. If you need to run multiple commands, the best way to do that is to have the `CMD` run a script. `CMD` requires that you tell it where to run the command, unlike `RUN`. So example `CMD` commands would be:
```dockerfile
CMD ["python3", "./app.py"]
CMD ["/bin/bash", "echo", "Hello World"]
```
- `EXPOSE` creates a hint for users of an image that provides services on ports. It is included in the information which can be retrieved via `$ docker inspect <container-id>`.
> **Note:** The `EXPOSE` command does not actually make any ports accessible to the host! Instead, this requires
> publishing ports by means of the `-p` or `-P` flag when using `$ docker run`.
- `ENTRYPOINT` configures a command that will run no matter what the user specifies at runtime.
> :bulb: this is not the full list of commands, but the ones you will be using in the exercise. For a full list, see the [Dockerfile reference](https://docs.docker.com/engine/reference/builder/).
</details>
## Write a Dockerfile
We want to create a Docker image with a Python web app.
As mentioned above, all user images are based on a _base image_. We will build our own Python image based on [Ubuntu](https://hub.docker.com/_/ubuntu/). We'll do that using a **Dockerfile**.
> :bulb: If you want to learn more about Dockerfiles, check out [Best practices for writing Dockerfiles](https://docs.docker.com/engine/userguide/eng-image/dockerfile_best-practices/).
1. We have made a small boilerplate file and app for you in the [/building-an-image](./building-an-image/) folder, so head over there and add content to the Dockerfile as described below
We'll start by specifying our base image, using the `FROM` keyword:
```docker
FROM ubuntu:22.04
```
1. The next step is usually to write the commands of copying the files and installing the dependencies. But first we will install the Python pip package to the ubuntu linux distribution. This will not just install the pip package but any other dependencies too, which includes the python interpreter. Add the following [RUN](https://docs.docker.com/engine/reference/builder/#run) commands next:
```docker
RUN apt-get update -y
RUN apt-get install -y python3 python3-pip python3-dev build-essential
```
1. Let's add the files that make up the Flask Application.
Install all Python requirements for our app to run. This will be accomplished by adding the lines:
```docker
COPY requirements.txt /usr/src/app/
RUN pip3 install --no-cache-dir -r /usr/src/app/requirements.txt
```
Copy the application app.py into our image by using the [COPY](https://docs.docker.com/engine/reference/builder/#copy) command.
```docker
COPY app.py /usr/src/app/
```
1. Specify the port number which needs to be exposed. Since our flask app is running on `5000` that's what we'll expose.
```docker
EXPOSE 5000
```
> :bulb: The `EXPOSE` instruction does not actually publish the port.
> It functions as a type of documentation between the person who builds the
> image and the person who runs the container,
> about which ports are intended to be published.
> You need the `-p`/`-P` command to actually open the host ports.
1. The last step is the command for running the application which is simply - `python3 ./app.py`. Use the [CMD](https://docs.docker.com/engine/reference/builder/#cmd) command to do that:
```docker
CMD ["python3", "/usr/src/app/app.py"]
```
The primary purpose of `CMD` is to tell the container which command it should run by default when it is started.
1. Verify your Dockerfile.
Our `Dockerfile` is now ready. This is how the file should look:
```docker
# The base image
FROM ubuntu:22.04
# Install python and pip
RUN apt-get update -y
RUN apt-get install -y python3 python3-pip python3-dev build-essential
# Install Python modules needed by the Python app
COPY requirements.txt /usr/src/app/
RUN pip3 install --no-cache-dir -r /usr/src/app/requirements.txt
# Copy files required for the app to run
COPY app.py /usr/src/app/
# Declare the port number the container should expose
EXPOSE 5000
# Run the application
CMD ["python3", "/usr/src/app/app.py"]
```
### Build the image
Now that you have your `Dockerfile`, you can build your image. The `docker build` command does the heavy-lifting of creating a docker image from a `Dockerfile`.
The `docker build` command is quite simple - it takes an optional tag name with the `-t` flag, and the location of the directory containing the `Dockerfile` - the `.` indicates the current directory:
```
docker build -t myfirstapp .
```
Expected output (at the end of the run):
```
[+] Building 79.5s (11/11) FINISHED docker:default
=> [internal] load build definition from Dockerfile 0.1s
=> => transferring dockerfile: 583B 0.0s
=> [internal] load metadata for docker.io/library/ubuntu:22.04 1.6s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [1/6] FROM docker.io/library/ubuntu:22.04@sha256:0e5e4a57c2499249aafc3b40fcd541e9a456aab7296681a3994d631587203f97 4.9s
=> => resolve docker.io/library/ubuntu:22.04@sha256:0e5e4a57c2499249aafc3b40fcd541e9a456aab7296681a3994d631587203f97 0.0s
=> => sha256:6414378b647780fee8fd903ddb9541d134a1947ce092d08bdeb23a54cb3684ac 29.54MB / 29.54MB 1.2s
=> => sha256:0e5e4a57c2499249aafc3b40fcd541e9a456aab7296681a3994d631587203f97 6.69kB / 6.69kB 0.0s
=> => sha256:3d1556a8a18cf5307b121e0a98e93f1ddf1f3f8e092f1fddfd941254785b95d7 424B / 424B 0.0s
=> => sha256:97271d29cb7956f0908cfb1449610a2cd9cb46b004ac8af25f0255663eb364ba 2.30kB / 2.30kB 0.0s
=> => extracting sha256:6414378b647780fee8fd903ddb9541d134a1947ce092d08bdeb23a54cb3684ac 3.3s
=> [internal] load build context 0.0s
=> => transferring context: 469B 0.0s
=> [2/6] RUN apt-get update -y 9.1s
=> [3/6] RUN apt-get install -y python3 python3-pip python3-dev build-essential 54.2s
=> [4/6] COPY requirements.txt /usr/src/app/ 0.1s
=> [5/6] RUN pip3 install --no-cache-dir -r /usr/src/app/requirements.txt 4.6s
=> [6/6] COPY app.py /usr/src/app/ 0.1s
=> exporting to image 4.6s
=> => exporting layers 4.6s
=> => writing image sha256:3c7c10734be5cfd548d5dba13ef5bf788fcc7e2050d1e8ecf4979801fee45ce9 0.0s
=> => naming to docker.io/library/myfirstapp
```
If you don't have the `ubuntu:22.04` image, the client will first pull the image and then create your image. If you do have it, your output on running the command will look different from mine.
If everything went well, your image should be ready! Run `docker image ls` and see if your image (`myfirstapp`) shows.
### Run your image
The next step in this section is to run the image and see if it actually works.
```
docker run -p 8080:5000 --name myfirstapp myfirstapp
```
Expected output:
```
* Running on http://0.0.0.0:5000/ (Press CTRL+C to quit)
```
> :bulb: remember that the application is listening on port 5000 on the Docker virtual network, but on the host it is listening on port 8080.
Head over to `http://<IP>:8080` or your server's URL and your app should be live.
## EXTRA Images and layers
When dealing with docker images, a layer, or image layer, is a change on an image. Every time you run one of the commands RUN, COPY or ADD in your Dockerfile it adds a new layer, causes the image to change to the new layer. You can think of it as staging changes when you're using Git: You add a file's change, then another one, then another one...
Consider the following Dockerfile:
```dockerfile
FROM ubuntu:22.04
RUN apt-get update -y
RUN apt-get install -y python3 python3-pip python3-dev build-essential
COPY requirements.txt /usr/src/app/
RUN pip3 install --no-cache-dir -r /usr/src/app/requirements.txt
COPY app.py /usr/src/app/
EXPOSE 5000
CMD ["python3", "/usr/src/app/app.py"]
```
First, we choose a starting image: `ubuntu:22.04`, which in turn has many layers.
We add another layer on top of our starting image, running an update on the system. After that yet another for installing the python ecosystem.
Then, we tell docker to copy the requirements to the container. That's another layer.
The concept of layers comes in handy at the time of building images. Because layers are intermediate images, if you make a change to your Dockerfile, docker will build only the layer that was changed and the ones after that. This is called layer caching.
Each layer is build on top of it's parent layer, meaning if the parent layer changes, the next layer does as well.
If you want to concatenate two layers (e.g. the update and install [which is a good idea](https://docs.docker.com/engine/userguide/eng-image/dockerfile_best-practices/#run)), then do them in the same RUN command:
```dockerfile
FROM ubuntu:22.04
RUN apt-get update && apt-get install -y \
python3 \
python3-pip \
python3-dev \
build-essential
COPY requirements.txt /usr/src/app/
RUN pip3 install --no-cache-dir -r /usr/src/app/requirements.txt
COPY app.py /usr/src/app/
EXPOSE 5000
CMD ["python3", "/usr/src/app/app.py"]
```
If you want to be able to use any cached layers from last time, they need to be run _before the update command_.
> NOTE:
> Once we build the layers, Docker will reuse them for new builds. This makes the builds much faster. This is great for continuous integration, where we want to build an image at the end of each successful build (e.g. in Jenkins). But the build is not only faster, the new image layers are also smaller, since intermediate images are shared between images.
Try to move the two `COPY` commands before for the `RUN` and build again to see it taking the cached layers instead of making new ones.
### Delete your image
If you make a `docker ps -a` command, you can now see a container with the name _myfirstapp_ from the image named _myfirstapp_.
```
docker ps -a
```
Expected output (you might have more containers):
```
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
fcfba2dfb8ee myfirstapp "python3 /usr/src/a..." About a minute ago Exited (0) 28 seconds ago myfirstapp
```
Make a `docker image ls` command to see that you have a docker image with the name `myfirstapp`
Try now to first:
- remove the container
- remove the image file as well with the `image rm` [command](https://docs.docker.com/engine/reference/commandline/image_rm/).
- make `docker image ls` again to see that it's gone.
## Instructions
There are constantly getting added new keywords to the Dockerfile. You can find a list of all the keywords [here](https://docs.docker.com/engine/reference/builder/).
## Summary
You learned how to write your own docker images in a `Dockerfile` with the use of the `FROM` command to choose base-images like Alpine or Ubuntu and keywords like `RUN` for executing commands,`COPY` to add resources to the container, and `CMD` to indicate what to run when starting the container.
You also learned that each of the keywords generates an image layer on top of the previous, and that everyone of the layers can be converted to a running container.
================================================
FILE: labs/08-multi-stage-builds.md
================================================
# Multi Stage Builds
## Task: create a tiny go-application container
In [multi-stage-build/hello.go](multi-stage-build/hello.go) we have created a small go application that prints `hello world` to your terminal.
You want to containerize it - that's easy!
You don't even have to have go installed, because you can just use a `base image` that has go!
The [Dockerfile](multi-stage-build/Dockerfile) is already created for you in the same folder.
## Exercise
- Try building the image with `docker build`
- Try to run it with `docker run`.
- You should see "Hello world!" printed to your terminal.
## Using Multi Stage Builds
The image we built has both the compiler and the compiled binary - which is too much: we only need the binary to run our application.
By utilizing multi-stage builds, we can separate the build stage (compiling) from the image we actually want to ship.
## Exercise
- try `docker image ls`.
- Could we make it smaller? We only need the compiler on build-time, since go is a statically compiled language.
- See the `Dockerfile` below, it has two `build stages`, wherein the latter stage is using the compiled artifact (the binary) from the first:
```Dockerfile
# build stage
FROM golang:1.19 AS builder
WORKDIR /app
COPY . /app
RUN go mod download && go mod verify
RUN cd /app && go build -o goapp
# final stage
FROM alpine
WORKDIR /app
COPY --from=builder /app/goapp /app/
ENTRYPOINT ./goapp
```
- Replace the original `Dockerfile` with the one above, and try building it with a different tag.
- When you run it it should still print "Hello world!" to your terminal.
- Try inspecting the size with `docker image ls`.
- Compare the size of the two images. The latter image should be much smaller, since it's just containing the go-application using `alpine` as the `base image`, and not the entire `golang`-suite of tools.
You can read more about this on: [Use multi-stage builds - docs.docker.com](https://docs.docker.com/develop/develop-images/multistage-build/)
You should see a great reduction in size, like in the example below:
```
REPOSITORY TAG IMAGE ID CREATED SIZE
hello golang 5311178b692a 23 seconds ago 805MB
hello multi-stage ba46dc3143ca 2 minutes ago 7.53MB
```
## Bonus Exercise
Since go is a statically compiled language, we can actually use `scratch` as the `base image`.
The `scratch` image is just an empty file system.
Hint: the "scratch" image has no shell, so in the Dockerfile you _also_ need to change the `ENTRYPOINT` from `shell form` to `exec form`.
See: `ENTRYPOINT` under [Dockerfile reference](https://docs.docker.com/engine/reference/builder/).
After building with your new Dockerfile, inspect the size of the images.
Your new image should be even smaller than the alpine-based image!
```Dockerfile
FROM golang:1.19 AS builder
WORKDIR /app
COPY . /app
RUN go mod download && go mod verify
RUN cd /app && go build -o goapp
FROM scratch
WORKDIR /app
COPY --from=builder /app/goapp /app/
ENTRYPOINT ["./goapp"]
```
================================================
FILE: labs/09-multi-container.md
================================================
# Multi container setups
## Task: host a Wordpress site
In this scenario, we are going to deploy the CMS system called Wordpress.
> WordPress is a free and open source blogging tool and a content management system (CMS) based on PHP and MySQL, which runs on a web hosting service.
So we need two containers:
- One container that can serve the Wordpress PHP files
- One container that can serve as a MySQL database for Wordpress.
Both containers already exists on the dockerhub: [Wordpress](https://hub.docker.com/_/wordpress/) and [Mysql](https://hub.docker.com/_/mysql/).
## Separate containers
> [!NOTE]
> The following two sections are only an example to show how you can run multi-container setups with `docker run`.
> Please skip ahead to [Using Docker compose](#using-docker-compose) to continue with the exercise.
To start a mysql container, issue the following command
```bash
docker run --name mysql-container --rm -p 3306:3306 -e MYSQL_ROOT_PASSWORD=wordpress -e MYSQL_DATABASE=wordpressdb -d mysql:5.7.36
```
Let's recap what this command does:
- `docker` invokes the docker engine
- `run` tells docker to run a new container off an image
- `--name mysql-container` gives the new container a name for better referencing
- `--rm` tells docker to remove the container after it is stopped
- `-p 3306:3306` mounts the host port 3306, to the containers port 3306.
- `-e MYSQL_ROOT_PASSWORD=wordpress` The `-e` option is used to inject environment variables into the container.
- `-e MYSQL_DATABASE=wordpressdb` denotes the name of the database created when mysql starts up.
- `-d` runs the container detached, in the background.
- `mysql` tells what container to actually run, here mysql:latest (:latest is the default if nothing else is specified)
MySQL is now exposing it's port 3306 on the host, and everybody can attach to it **so do not do this in production without proper security settings**.
We need to connect our wordpress container to the host's IP address.
You can either use the external IP address of your server, or the DNS name if you are at a training, e.g. `workstation-<num>.<prefix>.eficode.academy`.
After you have noted down the IP, spin up the wordpress container with the host IP as a variable:
```bash
docker run --name wordpress-container --rm -e WORDPRESS_DB_HOST=172.17.0.1 -e WORDPRESS_DB_PASSWORD=wordpress -e WORDPRESS_DB_USER=root -e WORDPRESS_DB_NAME=wordpressdb -p 8080:80 -d wordpress:5.7.2-apache
```
You can now browse to the IP:8080 and have your very own wordpress server running. Since port 3306 is the default MySQL port, wordpress will try to connect on that port by itself.
- Stop the two containers again `docker stop wordpress-container mysql-container`
## Making a container network
Even though we in two commands made the setup running in the above scenario, there are some problems here we can fix:
- We need to know the host IP to get them to talk to each other.
- And we have exposed the database to the outside world.
In order to connect multiple docker containers without binding them to the hosts network interface we need to create a docker network.
The `docker network` command securely connect and provide a channel to transfer information from one container to another.
First off make a new network for the containers to communicate through:
`docker network create if_wordpress`
Docker will return the `networkID` for the newly created network. You can reference it by name as well as the ID.
Now you need to connect the two containers to the network, by adding the `--network` option:
```bash
docker run --name mysql-container --rm --network if_wordpress -e MYSQL_ROOT_PASSWORD=wordpress -e MYSQL_DATABASE=wordpressdb -d mysql:5.7.36
af38acac52301a7c9689d708e6c3255704cdffb1972bcc245d67b02840983a50
docker run --name wordpress-container --rm --network if_wordpress -e WORDPRESS_DB_HOST=mysql-container -e WORDPRESS_DB_PASSWORD=wordpress -e WORDPRESS_DB_USER=root -e WORDPRESS_DB_NAME=wordpressdb -p 8080:80 -d wordpress:5.7.2-apache
fd4fd096c064094d7758cefce41d0f1124e78b86623160466973007cf0af8556
```
Notice the `WORDPRESS_DB_HOST` env variable. When you make a container join a network, it automatically gets the container name as DNS name as well, making it super easy to make containers discover each other. The DNS name is only visible inside the Docker network, which is also true for the `IP` address (usually an address starting with `172`) that is assigned to them. If you do not expose a port for a container, the container is only visible to Docker.
You have now deployed both containers into the network. Take a deeper look into the container network by issuing: `docker network inspect if_wordpress`.
```bash
docker network inspect if_wordpress
```
Expected output:
```json
[
{
"Name": "if_wordpress",
"Id": "04e073137ff8c71b9a040ba452c12517ebe5a520960a78bccb7b242b723b5a21",
"Created": "2017-11-28T17:20:37.83042658+01:00",
"Scope": "local",
"Driver": "bridge",
"EnableIPv6": false,
"IPAM": {
"Driver": "default",
"Options": {},
"Config": [
{
"Subnet": "172.18.0.0/16",
"Gateway": "172.18.0.1"
}
]
},
"Internal": false,
"Attachable": false,
"Ingress": false,
"Containers": {
"af38acac52301a7c9689d708e6c3255704cdffb1972bcc245d67b02840983a50": {
"Name": "mysql-container",
"EndpointID": "96b4befec46c788d1722d61664e68bfcbd29b5d484f1f004349163249d28a03d",
"MacAddress": "02:42:ac:12:00:02",
"IPv4Address": "172.18.0.2/16",
"IPv6Address": ""
},
"fb4dad5cd82b5b40ee4f7f5f0249ff4b7b4116654bab760719261574b2478b52": {
"Name": "wordpress-container",
"EndpointID": "2389930f52893e03a15fdc28ce59316619cb061e716309aa11a2716ef09cde17",
"MacAddress": "02:42:ac:12:00:03",
"IPv4Address": "172.18.0.3/16",
"IPv6Address": ""
}
},
"Options": {},
"Labels": {}
}
]
```
As, we have linked both the container now wordpress container can be accessed from browser using the address [http://localhost:8080](http://localhost:8080) and setup of wordpress can be done easily. MySQL is not accessible from the outside so security is much better than before.
### Cleanup
Close both of the containers down by issuing the following command:
```bash
docker stop wordpress-container mysql-container
```
## Using Docker compose
If you have started working with Docker and are building container images for your application services, you most likely have noticed that after a while you may end up writing long `docker container run` commands.
These commands, while very intuitive, can become cumbersome to write, especially if you are developing a multi-container applications and spinning up containers quickly.
[Docker Compose](https://docs.docker.com/compose/install/) is a “_tool for defining and running your multi-container Docker applications_”.
Your applications can be defined in a YAML file where all the options that you used in `docker run` are defined.
Compose also allows you to manage your application as a single entity rather than dealing with individual containers.
This file defines all of the containers and settings you need to launch your set of clusters. The properties map onto how you use the docker run commands, however, are now stored in source control and shared along with your code.
## Terminology
- `docker-compose.yml` The YAML file where all your configuration of your docker containers go.
- `docker compose` The cli tool that enables you to define and run multi-container applications with Docker
- `up` : creates and starts the services stated in the compose file
- `down` : stops and removes containers, networks, images, and volumes
- `restart` :
- `logs` : streams the acummulated logs from all the containers in the compose file
- `ps` : same as `docker ps`; shows you all containers that are currently running.
- `rm` : removes all the containers from the given compose file.
- `start` : starts the services
- `stop` : stops the services
The docker cli is used when managing individual containers on a docker engine.
It is the client command line to access the docker daemon api.
The docker compose cli together with the yaml files can be used to manage a multi-container application.
## Compose-erizing your wordpress
So we want to take advantage of docker compose to run our wordpress site.
In order to to this we need to:
1. Transform our setup into a docker-compose.yaml file
1. Invoke docker compose and watch the magic happen!
Head over to this labs folder:
`cd labs/multi-container`
Open the file `docker.compose.yaml` with a text editor:
`nano docker-compose.yaml`
You should see something like this:
```yaml
services:
# wordpress-container:
mysql-container:
image: mysql:5.7
ports:
- 3306:3306
environment:
MYSQL_ROOT_PASSWORD: wordpress
MYSQL_DATABASE: wordpressdb
```
This is the template we are building our compose file upon so let's drill this one down:
- `services` is the section where we put our containers
- `wordpress-container` is the section where we define our wordpress container
- `mysql-container` is the ditto of MySQL.
> For more information on docker compose yaml files, head over to the [documentation](https://docs.docker.com/compose/overview/).
The `services` part is equivalent to our `docker container run` command. Likewise there is a `network` and `volumes` section for those as well corresponding to `docker network create` and `docker volume create`.
Let's look the mysql-container part together, making you able to create the other container yourself. Look at the original command we made to spin up the container:
`docker container run --name mysql-container --rm -p 3306:3306 -e MYSQL_ROOT_PASSWORD=wordpress -e MYSQL_DATABASE=wordpressdb -d mysql:5.7.36`
The command gives out following information: a `name`, a `port` mapping, two `environment` variables and the `image` we want to run.
Now look at the docker compose example again:
- `mysql-container` defines the name of the container
- `image:wordpress` describes what image the container spins up from.
- `ports` defines a list of port mappings from host to container
- `environment` describes the `-e` variable made before in a yaml list
Instead of keeping sensitive information in the `docker-compose.yml` file, you can also use an [`.env`](https://docs.docker.com/compose/env-file/) file to keep all the environment variables. That way, it's easier to make a development environment and a production environment with the same `docker-compose.yml`.
```conf
MYSQL_ROOT_PASSWORD=wordpress
```
Try to spin up the container in detached mode:
```bash
docker compose up -d
Creating network "multicontainer_default" with the default driver
Creating multicontainer_mysql-container_1 ...
Creating multicontainer_mysql-container_1 ... done
```
Looking at the output you can see that it made a `docker network` named `multicontainer_default` as well as the MySQL container named `multicontainer_mysql-container_1`.
Issue a `docker container ls` as well as `docker network ls` to see that both the container and network are listed.
To shut down the container and network, issue a `docker compose down`
> **note**: The command docker compose down removes the containers and default network.
### Creating the wordpress container
You now have all the pieces of information to make the Wordpress container. We've copied the run command from before if you can't remember it by heart:
```bash
docker run --name wordpress-container --rm --network if_wordpress -e WORDPRESS_DB_HOST=mysql-container -e WORDPRESS_DB_PASSWORD=wordpress -e WORDPRESS_DB_USER=root -e WORDPRESS_DB_NAME=wordpressdb -p 8080:80 -d wordpress:5.7.2-apache
```
You must
- uncomment the `wordpress-container` part of the services section
- map the pieces of information from the docker container run command to the yaml format.
- remove MySQL port mapping to close that from outside reach.
When you made that, run `docker compose up -d` and access your wordpress site from [http://IP:8080](http://IP:8080)
> **Hint**: If you are stuck, look at the file docker-compose_final.yaml in the same folder.
================================================
FILE: labs/README.md
================================================
# Docker labs
In this folder are a lot of exercises. They are numbered in the way we think makes sence to introduce the concepts.
Below is a cheatsheet for many of the commands we will touch uppon in the lab.
```bash
docker build -t friendlyname . # Create image using this directory's Dockerfile
docker container run -p 4000:80 friendlyname # Run "friendlyname" mapping port 4000 to 80
docker container run -d -p 4000:80 friendlyname # Same thing, but in detached mode
docker container run -ti friendlyname # Run "friendlyname" in interactive mode
docker container ls # List all running containers
docker container ls -a # List all containers, even those not running
docker container exec -it <hash> bash # Interacts with container and executes bash
docker container stop <hash> # Gracefully stop the specified container
docker container kill <hash> # Force shutdown of the specified container
docker container rm <hash> # Remove specified container from this machine
docker container prune # Remove all stopped containers
docker volume create <name> # Creates a named volume with the default driver
docker volume inspect <name> # prints out details about the given volume entity
docker volume rm <name> # removes the given volume from the system
docker image ls -a # List all images on this machine
docker image rm <image id> # Remove specified image from this machine
docker image prune # Remove all 'dangling' images from this machine
docker image prune -a # Remove all images without at least one container associated to them
docker system prune # delete all unused data; containers, volumes and images w.o. containers
docker system df -v # presents a summary of the space used by different docker objects
docker login # Log in this CLI session using your Docker credentials
docker tag <image> username/repository:tag # Tag <image> for upload to registry
docker push username/repository:tag # Upload tagged image to registry
docker run username/repository:tag # Run image from a registry
Ctrl + P, Ctrl + Q # Detach from container you're in, but keep it running
Ctrl + D # Detach from container you're in, and stop it, same as exit
```
================================================
FILE: labs/advanced/containers-on-default-bridge.md
================================================
# Exploration - Containers on default bridge network
In this exercise, you will explore various characterstics of the default network bridge, and the containers running in that network.
## You should investigate:
* What docker networks exist on the host network? (`docker network ls`)
* See what network interfaces exist on the host network? Do you see docker0?
* Inpsect docker0 bridge.
* What does docker0 network interface on the host look like? (IP/network address, NetMask, MAC address, etc?)
## Run couple of docker containers on the bridge network
Note: `praqma/network-multitool` is a small image with lots of network troubleshooting tools installed in it. It also runs a nginx web server on port 80 by default. Most of our examples will use this image in the *web* role. Just think of it as nginx image, with some extra bells and whistles.
```
$ docker run --name web \
-d praqma/network-multitool
$ docker run --name db \
-e MYSQL_ROOT_PASSWORD=secret \
-d mysql
```
### You should investigate:
* What are the IP addresses of the containers?
* What DNS resolver do these containers use?
* Can the containers on the default bridge network access each other by their names? IP addresses?
* Do you see any **veth** interfaces on the host?
* Compare MAC addresses of veth interfaces on the host and the **eth0** interfaces on each container.
* Inspect containers for their IP addresses, MAC addresses, etc.
* Explore what processes are listening on various network interfaces on the host.
* Explore what processes are listening on various network interfaces on the container.
# Useful commands:
* docker ps
* docker ls
* docker network ls
* docker inspect
* docker exec -it <container name|id> </some/shell>
* ip addr show
* netstat
================================================
FILE: labs/advanced/containers-on-docker-compose-network.md
================================================
# Exploration - Containers on docker compose bridge network
In this exercise, you will explore various characterstics of the docker compose network bridge, and the containers running in that network.
Spoiler: Network created by `docker compose` are same as user-defined networks. These networks and the containers running in these networks exihibit similar behavior.
## Create/run a multi-container `docker-compose.yml` application:
```
$ vi docker-compose.yml
services:
apache:
image: httpd:alpine
postgres:
image: postgres
environment:
- POSTGRES_PASSWORD=secret
$ docker compose up -d
$ docker compose ps
```
Note: `praqma/network-multitool` is a small image with lots of network troubleshooting tools installed in it. It also runs a nginx web server on port 80 by default. Most of our examples will use this image in the *web* role. Just think of it as nginx image, with some extra bells and whistles. You can replace Apache `httpd:alpine` image with `praqma/network-multitool` image in the above example, if you want to.
## You should investigate:
* What docker networks exist on the host network? (`docker network ls`)
* See what network interfaces exist on the host network? Do you see docker0?
* Do you see any other/additional network interface? (e.g br-123zbc456xyz)
* Inpsect docker0 bridge.
* Inpsect the newly created docker compose bridge (e.g br-123zbc456xyz).
* What does the docker compose network (bridge) interface on the host look like? (IP/network address, NetMask, MAC address, etc?)
* What are the IP addresses of the containers?
* What DNS resolver do these containers use?
* Can the containers on the docker compose bridge network access each other by their names? IP addresses?
* Do you see any **veth** interfaces on the host?
* Compare MAC addresses of veth interfaces on the host and the **eth0** interfaces on each container.
* Inspect containers for their IP addresses, MAC addresses, etc.
* Explore what processes are listening on various network interfaces on the host.
* Explore what processes are listening on various network interfaces on the container.
# Useful commands:
* docker ps
* docker ls
* docker network ls
* docker inspect
* docker exec -it <container name|id> </some/shell>
* ip addr show
* netstat
* iptables -L
* iptables -t nat -L
* iptables-save (This will not *save* any rules. It will just list them on the screen.)
================================================
FILE: labs/advanced/containers-on-host-network.md
================================================
# Exploration - Containers on host network
In this exercise, you will explore various characterstics of the **host network**, and the containers running in that network.
## Run a container in "host network" mode:
```
docker run --name nginx --network host -d nginx
```
Optionally, run another container in the "host network" mode.
```
docker run --name mysql --network host -e MYSQL_ROOT_PASSWORD=secret -d mysql
```
## You should investigate:
* What docker networks exist on the host? (`docker network ls`)
* See what network interfaces exist on the host? Do you see docker0?
* Do you see any new network interface(s)? (e.g br-123zbc456xyz)
* Do you see any new **veth** interfaces on the host?
* Do you see an **eth0** interface inside the container?
* What is the IP address of the container(s)?
* What DNS resolver do this container(s) use?
* Can a container on this network reach/access any other containers by their names? IP addresses?
* Inspect container(s) for their IP addresses, MAC addresses, etc.
* Explore what processes are listening on various network interfaces on the host.
* Explore what processes are listening on various network interfaces on the container.
# Useful commands:
* docker ps
* docker ls
* docker network ls
* docker inspect
* docker exec -it <container name|id> </some/shell>
* ip addr show
* netstat
* ps aux
* iptables -L
* iptables -t nat -L
* iptables-save (This will not *save* any rules. It will just list them on the screen.)
================================================
FILE: labs/advanced/containers-on-none-network.md
================================================
# Exploration - Containers on none network
In this exercise, you will explore various characterstics of the **none** network, and the containers running in that network.
## Run a container in "none network" mode:
```
docker run --name multitool --network none -p 80:80 -d praqma/network-multitool
```
Optionally, run another container in the "none network" mode.
```
docker run --name mysql --network none -e MYSQL_ROOT_PASSWORD=secret -d mysql
```
## You should investigate:
* What docker networks exist on the host? (`docker network ls`)
* See what network interfaces exist on the host network? Do you see docker0?
* Do you see any new network interface(s)? (e.g br-123zbc456xyz)
* Do you see any new **veth** interfaces on the host?
* Do you see an **eth0** interface inside the container?
* What is the IP address of the container(s)?
* What DNS resolver do this container(s) use?
* Can a container on this network reach/access any other containers by their names? IP addresses?
* Inspect container(s) for their IP addresses, MAC addresses, etc.
* Explore what processes are listening on various network interfaces on the host.
* Explore what processes are listening on various network interfaces on the container.
# Useful commands:
* docker ps
* docker ls
* docker network ls
* docker inspect
* docker exec -it <container name|id> </some/shell>
* ip addr show
* netstat
* ps aux
* iptables -L
* iptables -t nat -L
* iptables-save (This will not *save* any rules. It will just list them on the screen.)
================================================
FILE: labs/advanced/containers-on-user-defined-bridge.md
================================================
# Exploration - Containers on user-defined bridge network
In this exercise, you will explore various characterstics of the user-defined network bridge, and the containers running in that network.
## Create a user-defined docker network bridge:
```
docker network create mynet
```
## You should investigate:
* What docker networks exist on the host network? (`docker network ls`)
* See what network interfaces exist on the host network? Do you see docker0?
* Do you see any other/additional network interface? (e.g br-123zbc456xyz)
* Inspect docker0 bridge.
* Inspect the newly created bridge `mynet`.
* What does the new network (bridge) interface on the host look like? (IP/network address, NetMask, MAC address, etc?)
## Run couple of docker containers on the user-defined bridge network
Note: `praqma/network-multitool` is a small image with lots of network troubleshooting tools installed in it. It also runs a nginx web server on port 80 by default. Most of our examples will use this image in the *web* role. Just think of it as nginx image, with some extra bells and whistles.
Note: You may want to stop/remove container you created in the previous exercises before running the new ones shown below.
```
$ docker run --name=web --network=mynet \
-d praqma/network-multitool
$ docker run --name=db --network=mynet \
-e MYSQL_ROOT_PASSWORD=secret \
-d mysql
```
### You should investigate:
* What are the IP addresses of the containers?
* What DNS resolver do these containers use?
* Can the containers on the user-defined bridge network access each other by their names? IP addresses?
* Do you see any **veth** interfaces on the host?
* Compare MAC addresses of veth interfaces on the host and the **eth0** interfaces on each container.
* Inspect containers for their IP addresses, MAC addresses, etc.
* Explore what processes are listening on various network interfaces on the host.
* Explore what processes are listening on various network interfaces on the container.
## Explore IPTables magic happening inside the containers:
* Run another container on the user-defined network, with special privileges, and use that to explore the IPTables rules setup on the container.
* Optionally, explore the iptables rules on the host as well.
```
$ docker run --cap-add=NET_ADMIN --cap-add=NET_RAW \
--name multitool --network mynet \
-it praqma/network-multitool /bin/bash
$ iptables-save
$ netstat -ntlp
```
# Useful commands:
* docker ps
* docker ls
* docker network ls
* docker inspect
* docker exec -it <container name|id> </some/shell>
* ip addr show
* netstat
* iptables -L
* iptables -t nat -L
* iptables-save (This will not *save* any rules. It will just list them on the screen.)
================================================
FILE: labs/advanced/joining-network-and-process-namespace-of-existing-containers.md
================================================
# Learn how to join containers to network and process namespace of other containers
In this exercise, you will learn how to trouble-shoot and extract certain information from containers which are very limited in nature. Nginx and MySQL are good examples. They do not have much tools installed in them, and executing simple commands such as `ping`, `curl` or even `ps` takes a lot of effort.
## Run a mysql container on default network:
```
$ docker run --name mysql -e MYSQL_ROOT_PASSWORD=secret -d mysql
```
Note: This container runs on the default docker bridge network, so most of the things you already explored in previous exercises.
### Things you should explore:
* Can you `exec` into the container, and run these commands? `ifconfig`, `ip`, `ping`, `curl`, `ps`, `netstat`, `top`
* If not, what are your options? Would you choose to install these tools from various OS pacakges inside the container?
## Run a **multitool** container and make it join the network of mysql container:
```
$ docker run --name multitool --network container:mysql \
--rm -it praqma/network-multitool /bin/bash
```
### Things you should explore:
* Exec into the multitool container and see what is the IP address of this container?
* Exec into the multitool container and see what is the IP address of mysql container?
* Is there a corresponding veth interface on the host computer for this container?
* Is there a corresponding veth interface on the host computer for mysql container?
* Explore IP address, MAC, DNS , settings for this container.
* Explore IP address, MAC, DNS , settings for mysql container.
* What are similarities and differences between the network settings of the mysql container and the multitool container?
* Inspect docker network and mysql container - from the host.
* Can you see what processes are running in multitool container?
* Can you see what processes are running in mysql container?
## Run a multitool (busybox) container and make it join the process namespace of the mysql container:
```
$ docker run --name busybox --pid container:mysql \
--rm -it busybox /bin/sh
```
### Things you should explore:
* Exec into the multitool container and see what is the IP address of this container?
* Exec into the multitool container and see what is the IP address of mysql container?
* Is there a corresponding veth interface on the host computer for this container?
* Is there a corresponding veth interface on the host computer for mysql container?
* Explore IP address, MAC, DNS , settings for this container.
* Explore IP address, MAC, DNS , settings for mysql container.
* What are similarities and differences between the network settings of the mysql container and the multitool container?
* Inspect docker network and mysql container - from the host.
* Can you see what processes are running in multitool container?
* Can you see what processes are running in mysql container?
## Run a multitool (busybox) container and make it join the network **and** process namespace of the mysql container:
```
$ docker run --name busybox \
--network container:mysql \
--pid container:mysql \
--rm -it busybox /bin/sh
```
### Things you should explore:
* Exec into the multitool container and see what is the IP address of this container?
* Exec into the multitool container and see what is the IP address of mysql container?
* Is there a corresponding veth interface on the host computer for this container?
* Is there a corresponding veth interface on the host computer for mysql container?
* Explore IP address, MAC, DNS , settings for this container.
* Explore IP address, MAC, DNS , settings for mysql container.
* What are similarities and differences between the network settings of the mysql container and the multitool container?
* Inspect docker network and mysql container - from the host.
* Can you see what processes are running in multitool container?
* Can you see what processes are running in mysql container?
# Useful commands:
* docker ps
* docker ls
* docker network ls
* docker inspect
* docker exec -it <container name|id> </some/shell>
* ip addr show
* netstat
* ps aux
* iptables -L
* iptables -t nat -L
* iptables-save (This will not *save* any rules. It will just list them on the screen.)
================================================
FILE: labs/advanced/systemd/Dockerfile
================================================
FROM centos:7
CMD ["/usr/sbin/init"]
# Delete everything inside `/lib/systemd/system/sysinit.target.wants/`, except `systemd-tmpfiles-setup.service`
RUN ( cd /lib/systemd/system/sysinit.target.wants/; \
for i in *; do [ $i == systemd-tmpfiles-setup.service ] || rm -f $i; done ) \
&& rm -f /lib/systemd/system/multi-user.target.wants/* \
/etc/systemd/system/*.wants/* \
/lib/systemd/system/local-fs.target.wants/* \
/lib/systemd/system/sockets.target.wants/*udev* \
/lib/systemd/system/sockets.target.wants/*initctl* \
/lib/systemd/system/basic.target.wants/* \
/lib/systemd/system/anaconda.target.wants/* \
&& yum -y install postfix dovecot httpd \
&& yum clean all \
&& systemctl enable postfix dovecot httpd
# Mount the following at container runtime:
# -v /sys/fs/cgroup:/sys/fs/cgroup:ro
# --tmpfs /run
VOLUME [ "/sys/fs/cgroup" ]
# Expose necessary ports
EXPOSE 80 443 25 110 143 443 993 995
# Build and run instructions:
#############################
# docker build -t local/centos7:mailserver
# docker run -p 80:80 -p 25:25 \
# -v /sys/fs/cgroup:/sys/fs/cgroup:ro \
# --tmpfs /run \
# -d local/centos7:mailserver
================================================
FILE: labs/advanced/systemd/README.md
================================================
# Build and run instructions for systemd based containers:
## Build:
```
docker build -t local/centos7:mailserver
```
## Run:
```
docker run -p 80:80 -p 25:25 \
-v /sys/fs/cgroup:/sys/fs/cgroup:ro \
--tmpfs /run \
-d local/centos7:mailserver
```
# Build and run using docker compose:
There is a docker-compose.yml, which can be used to build and run this container image. Of-course a Dockerfile is a pre-requisite.
```
docker compose build
docker compose up -d
```
## Verify:
```
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
604a437cc577 systemd_mailserver "/usr/sbin/init" 2 minutes ago Up 2 minutes 0.0.0.0:25->25/tcp, 0.0.0.0:80->80/tcp, 0.0.0.0:110->110/tcp, 0.0.0.0:143->143/tcp, 0.0.0.0:443->443/tcp, 0.0.0.0:993->993/tcp, 0.0.0.0:995->995/tcp systemd_mailserver_1
```
================================================
FILE: labs/advanced/systemd/docker-compose.yml
================================================
services:
mailserver:
build: .
ports:
- "80:80"
- "443:443"
- "25:25"
- "110:110"
- "143:143"
- "993:993"
- "995:995"
volumes:
- /sys/fs/cgroup:/sys/fs/cgroup:ro
tmpfs:
- /run
================================================
FILE: labs/advanced/traefik/example1/README.md
================================================
Make sure you set up some sort of DNS / name resolution from your computer to the IP address where reverse proxy is running.
For example, your VM in the cloud has IP address `1.2.3.4` , and you have your reverse proxy running in that VM as a docker container. In that case, on your local / work computer, setup the following:
```
$ cat /etc/hosts
127.0.0.1 localhost
1.2.3.4 example.com www.example.com
```
**Note:** For LetsEncrypt to be able to give you certs, your public IP must be reachable through the DNS name that you are using in your labels/traefik rules, .e.g `example.com`.
================================================
FILE: labs/advanced/traefik/example1/docker-compose.yml
================================================
services:
traefik:
image: traefik:1.7
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ${PWD}/traefik.toml:/etc/traefik/traefik.toml
ports:
- 80:80
multitool:
image: praqma/network-multitool
labels:
- traefik.enable=true
- traefik.port=80
- traefik.frontend.rule=Host:example.com,www.example.com
================================================
FILE: labs/advanced/traefik/example1/traefik.toml
================================================
[docker]
endpoint = "unix:///var/run/docker.sock"
================================================
FILE: labs/advanced/traefik/example2/README.md
================================================
# Traefik proxy running as independent docker compose app:
In this example, Traefik runs as an independent docker compose application. For other applications requiring it's "proxy" services, they need to be on the same network as of the Traefik proxy. The best way is to make a docker bridge network ourselves, and connect all containers on that network.
## Create a docker bridge network - **services-network**:
```
docker network create services-network
```
## Start Traefik - frond-end / proxy:
Start the Traefik proxy service by briging up it's docker compose stack - which is compose of just one container!
```
cd proxy
docker compose up -d
cd ..
```
Note: Please go through the actual `docker-compose.yml` and `traefik.toml` files under the `proxy` directory.
Verify:
```
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
6665c2a9afb2 traefik:1.7 "/traefik" 7 minutes ago Up 7 minutes 0.0.0.0:80->80/tcp proxy_traefik_1
```
## Start the web server - back-end:
```
cd web
docker compose up -d
cd ..
```
Note: Please go through the actual `docker-compose.yml` file under the `web` directory.
Verify:
```
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
65b42f52017a praqma/network-multitool "/docker-entrypoint.…" 2 minutes ago Up 2 minutes 80/tcp, 443/tcp web_multitool_1
6665c2a9afb2 traefik:1.7 "/traefik" 7 minutes ago Up 7 minutes 0.0.0.0:80->80/tcp proxy_traefik_1
```
## Ensure DNS/name-resolution is in place:
For this example, you can setup the following in your `/etc/hosts` file:
```
$ cat /etc/hosts
127.0.0.1 localhost localhost.localdomain
. . .
127.0.0.1 example.com www.example.com
```
## Check with curl:
```
$ curl example.com
Praqma Network MultiTool (with NGINX) - 65b42f52017a - 172.20.0.3/16
$ curl www.example.com
Praqma Network MultiTool (with NGINX) - 65b42f52017a - 172.20.0.3/16
```
Notice that `curl localhost` will not work, because the proxy is listening on localhost, on port 80, expecting only the DNS names/URLs, which are configured as it's front-ends. It will ignore all other urls , and will show a `404` .
```
$ curl localhost
404 page not found
```
================================================
FILE: labs/advanced/traefik/example2/proxy/docker-compose.yml
================================================
services:
traefik:
image: traefik:1.7
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ${PWD}/traefik.toml:/etc/traefik/traefik.toml
ports:
- 80:80
networks:
- services-network
networks:
services-network:
external: true
# Note: "services-network" is a bridge network created manually on the docker host,
# using: `$ docker network create services-network`
# This network needs to exist before any of the compose services are able to use it.
================================================
FILE: labs/advanced/traefik/example2/proxy/traefik.toml
================================================
[docker]
endpoint = "unix:///var/run/docker.sock"
network = "services-network"
================================================
FILE: labs/advanced/traefik/example2/web/docker-compose.yml
================================================
services:
multitool:
image: praqma/network-multitool
labels:
- traefik.enable=true
- traefik.port=80
- traefik.frontend.rule=Host:example.com,www.example.com
networks:
- services-network
networks:
services-network:
external: true
================================================
FILE: labs/advanced/traefik/example3/acme.json
================================================
================================================
FILE: labs/advanced/traefik/example3/docker-compose.yml
================================================
services:
traefik:
image: traefik:1.7
labels:
- traefik.frontend.rule=Host:traefik.example.com
- traefik.port=8080
- traefik.enable=true
environment:
# You need to specify credentials for your DNS provider,
# "if" you are using DNS challenge in traefik.toml.
# You don't need these if you are using HTTP challenge.
#
AWS_ACCESS_KEY_ID: 'YOUR_AWS_ACCESS_KEY_ID_HERE'
AWS_SECRET_ACCESS_KEY: 'YOUR_AWS_SECRET_ACCESS_KEY_HERE'
AWS_DEFAULT_REGION: 'eu-central-1'
AWS_HOSTED_ZONE_ID: 'ABC123456789XYZ'
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ${PWD}/traefik.toml:/etc/traefik/traefik.toml
# acme.json need to have 0600 permissions.
- ${PWD}/acme.json:/acme.json
# The password file is generated with command:-
# "htpasswd -c -B traefik-dashboard-users.passwd admin"
- ${PWD}/traefik-dashboard-users.passwd:/traefik-dashboard-users.passwd
ports:
- 80:80
- 443:443
# The way this works, e.g. traefik dashboard having it's own label as "traefik.example.com",
# you do not need to enable port 8080.
# You will be able to reach Traefik dashboard through "https://traefik.example.com" ,
# without needing to specify 8080 in front of it.
# Though, you can enable it if you like to.
# - 8080:8080
nginx:
image: nginx:alpine
labels:
- traefik.frontend.rule=Host:example.com,www.example.com
- traefik.port=80
- traefik.enable=true
================================================
FILE: labs/advanced/traefik/example3/traefik.toml
================================================
defaultEntryPoints = ["http", "https"]
logLevel = "INFO"
[docker]
endpoint = "unix:///var/run/docker.sock"
exposedByDefault = false
# enabling api is not absolutely necessary, it is needed only if you need dashboard.
[api]
dashboard = true
entrypoint = "dashboard"
[entryPoints]
[entryPoints.http]
address = ":80"
[entryPoints.http.redirect]
entryPoint = "https"
[entryPoints.https]
address = ":443"
[entryPoints.https.tls]
[entryPoints.dashboard]
address = ":8080"
[entryPoints.dashboard.auth.basic]
# admin/secret
# users = ["admin:$2y$08$64hQda74gXS80mS63hN3xOFGVB9KA2vUOXtW.NDaBjX9pEHq7qdUa"]
users = ["kamran:$2y$05$oCAwtNymC1be9.uDq.LbG./1ENktYoGKU.KNo5o7.DL26FJVGCiY2"]
[acme]
email = "kaz@praqma.net"
storage = "/acme.json"
# CA server to use.
# Uncomment the 'caServer' line to use Let's Encrypt's staging server.
# Or, leave caServer line commented to goto/use production certificates.
#
caServer = "https://acme-staging-v02.api.letsencrypt.org/directory"
entryPoint = "https"
#########################################################
#
# Enable this section if you want to use DNS challenge.
# You will need to provide DNS credentials in your
# as environment variables in your docker-compose.yml.
#
#
# [acme.dnsChallenge]
# provider = "route53"
# delayBeforeCheck = 0
#
# [[acme.domains]]
# main = "*.example.com"
#
#########################################################
#########################################################
#
# Enable this section if you want to use HTTP challenge.
#
# onHostRule = true
# [acme.httpChallenge]
# entryPoint = "http"
#
#########################################################
================================================
FILE: labs/building-an-image/Dockerfile
================================================
# Dockerfile for docker-flask web application
# Add a base image to build this image off of
FROM
# Add a default port containers from this image should expose
EXPOSE
# Add a default command for this image
CMD
================================================
FILE: labs/building-an-image/app.py
================================================
import socket
from flask import Flask
app = Flask(__name__)
@app.route('/')
def hello_world():
return "Hello! I am a Flask application running on {}".format(socket.gethostname())
if __name__ == '__main__':
# Note the extra host argument. If we didn't have it, our Flask app
# would only respond to requests from inside our container
app.run(host='0.0.0.0')
================================================
FILE: labs/building-an-image/requirements.txt
================================================
Flask==2.3.2
================================================
FILE: labs/docker-compose/.gitignore
================================================
#Prevent leaking any premade solutions
*.yml
web_content
================================================
FILE: labs/docker-compose/00-getting-started.md
================================================
# Getting started
In this section you will install Docker and run your container using compose.
## Installing Docker
Depending on what OS you are running, installation is different, but head over to the [docker compose installation instructions](https://docs.docker.com/compose/install/) website and follow the instructions there.
================================================
FILE: labs/docker-compose/01-hello-world.md
================================================
# hello-world
Try running a command with docker compose:
`docker compose up`
Your terminal output should look like this:
```bash
no configuration file provided: not found
```
Now we need to create docker-compose.yml as the message suggests.
Open editor and create new file with following content:
```yml
services:
hello-world:
image: hello-world:latest
```
Now try to run again `docker compose up` and you should see following message:
```bash
Starting docker-compose_hello-world_1 ... done
Attaching to docker-compose_hello-world_1
hello-world_1 |
hello-world_1 | Hello from Docker!
hello-world_1 | This message shows that your installation appears to be working correctly.
hello-world_1 |
hello-world_1 | To generate this message, Docker took the following steps:
hello-world_1 | 1. The Docker client contacted the Docker daemon.
hello-world_1 | 2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
hello-world_1 | (amd64)
hello-world_1 | 3. The Docker daemon created a new container from that image which runs the
hello-world_1 | executable that produces the output you are currently reading.
hello-world_1 | 4. The Docker daemon streamed that output to the Docker client, which sent it
hello-world_1 | to your terminal.
hello-world_1 |
hello-world_1 | To try something more ambitious, you can run an Ubuntu container with:
hello-world_1 | $ docker run -it ubuntu bash
hello-world_1 |
hello-world_1 | Share images, automate workflows, and more with a free Docker ID:
hello-world_1 | https://hub.docker.com/
hello-world_1 |
hello-world_1 | For more examples and ideas, visit:
hello-world_1 | https://docs.docker.com/get-started/
hello-world_1 |
docker-compose_hello-world_1 exited with code 0
```
As we can see. The hello-world container (tagged with latest) was started using the the compose file and your docker compose installation is working!
Compose file explained:
`services:`: defines the configuration of the container
`hello-world:`: is now the name of the service
`image: hello-world:latest`: defines the container and its version that we're using for `hello-world` service.
Now re-create using hello-world service to use ubuntu container and also add `command:` for the service with value `echo "Hello from me"`
_*Q: So what did this do?*_
================================================
FILE: labs/docker-compose/02-port-forward.md
================================================
# A basic webserver
Running arbitrary Linux commands inside a Docker container with docker compose is quite overhead, but let's do something more useful.
Create docker-compose.yml file with following content:
```yml
services:
nginx:
image: nginx:latest
```
Now run the command `docker compose pull` and you should see:
```bash
Pulling nginx ... done
```
This Docker image uses the [Nginx](http://nginx.org/) webserver to serve a static HTML website.
Configure you're nginx service to expose port 80 as 8080.
With:
```yml
ports:
- "8080:80"
```
Where first one is HOST and second one is CONTAINER.
Open a web browser and go to port 8080 on your host. The exact address will depend on how you're running Docker today:
* **Native Linux** - [http://localhost:8080](http://localhost:8080)
* **Cloud server** - Make sure firewall rules are configured to allow traffic on port 8080. Open browser and use the hostname (or IP) for your server.
Ex: [http://ec2-54-69-126-146.us-west-2.compute.amazonaws.com:8080](http://ec2-54-69-126-146.us-west-2.compute.amazonaws.com:8080) -
Alternatively open a new shell and issue `curl localhost:8080`
If you see a webpage saying "Welcome to nginx!" then you're done!
If you look at the console output from docker, you see nginx producing a line of text for each time a browser hits the webpage:
```bash
$ docker compose up
Creating docker-compose_nginx_1 ... done
Attaching to docker-compose_nginx_1
nginx_1 | 172.18.0.1 - - [07/Jan/2020:22:28:48 +0000] "GET / HTTP/1.1" 200 612 "-" "curl/7.58.0" "-"
```
Press **control + c** in your terminal window to stop your services.
## Working with your docker container
When running a webserver like nginx, it's pretty useful that you do not have to have an open session into the server at all times to run it.
We need to make it run in the background, freeing up our terminal for other things.
Docker enables this with the `-d` parameter for run.
`docker compose up -d`
```bash
$ docker compose up -d
Starting docker-compose_nginx_1 ... done
```
Docker prints out the service name and returns to the terminal.
To see what services you've running from compose file you can use `docker compose ps`
_*Q: What is the status?*_
To stop services you can run `docker compose down`
_*Q: What is the status after stopping?*_
================================================
FILE: labs/docker-compose/03-volumes.md
================================================
# Docker volumes
In some cases you're going to need data outside of docker containers and to do that you can use volumes
**A docker Volume** is where you can use a named or unnamed volume to store the external data. You would normally use a volume driver for this, but you can get a host mounted path using the default local volume driver.
So let's look at the [Nginx](https://hub.docker.com/_/nginx/) service from port-forwarding excercise.
The server itself is of little use, if it cannot access our web content on the host.
We can define volumes with docker compose for the service like this:
```yml
services:
nginx:
image: nginx:latest
volumes:
- "./web_content:/usr/share/nginx/html:ro"
```
Where web_content folder is mapped to nginx html folder. `:ro` defines that it's read only.
Try to do the following:
1. Create folder `web_content`
2. Create index.html file under `web_content`
3. Modify html file
4. Restart the nginx service with compose
_*Q: What do you see now in the browser?*_
Now modify the index.html again and *do* not restart the container.
_*Q: What do you see after you refresh the browser page?*_
================================================
FILE: labs/docker-compose/04-build-image.md
================================================
# Building docker image using docker-compose
In this excercise we're going to setup [bottlepy](https://bottlepy.org/docs/dev/) application running from our compose service.
Checkout the bottle folder and you should be seeing 3 files:
- app.py
- Dockerfile
- requirements.txt
app.py contains our bottle web service that is running on port 8080 and it uses python3.
requirements.txt has the needed requirements for python to be installed before running the service.
Dockerfile is partially empty and now it's your job to fill it in in order to make the application run.
Application can be started with command `python3 /path/to/app.py` and requirements can be installed with `pip3 install -r /path/to/requirements.txt`.
Your `docker-compose.yml` can look something like this:
```yml
services:
bottle:
build: ./bottle/
ports:
```
Where `build:` is refering the folder what docker container we should build.
To make docker compose to build you can use command `docker compose up --build` in order to execute the `build:` configuration.
Try to build your application container and open browser to correct port.
_*Q: What do you see on <domain>:<port>/hello/docker-is-awesome ?*_
================================================
FILE: labs/docker-compose/bottle/Dockerfile
================================================
# Dockerfile for docker-bottle web application
# Add a base image to build this image off of
FROM
COPY
# Add a default port containers from this image should expose
EXPOSE
# Add a default command for this image
CMD
================================================
FILE: labs/docker-compose/bottle/app.py
================================================
import sys
from bottle import route, run, template
@route('/')
def index():
return template("<h1>It works!</h1><p>{{version_info}}", version_info=sys.version_info)
@route('/hello/<name>')
def hello(name):
return template('<b>Hello {{name}}</b>!', name=name)
run(host='0.0.0.0', port=8080)
================================================
FILE: labs/docker-compose/bottle/requirements.txt
================================================
bottle==0.12.20
================================================
FILE: labs/exercise-template.md
================================================
# Template headline
## Learning Goals
- provide a list of goals to learn here
## Introduction
Here you will provide the bare minimum of information people need to solve the exercise.
## Subsections
You can have several subsections if needed.
<details>
<summary>:bulb: If an explanaition becomes too long, the more detailed parts can be encapsulated in a drop down section</summary>
</details>
## Exercise
### Overview
- In bullets, what are you going to solve as a student
### Step by step instructions
<details>
<summary>More Details</summary>
**take the same bullet names as above and put them in to illustrate how far the student have gone**
- all actions that you believe the student should do, should be in a bullet
> :bulb: Help can be illustrated with bulbs in order to make it easy to distinguish.
</details>
### Clean up
If anything needs cleaning up, here is the section to do just that.
================================================
FILE: labs/extra-exercises/image-security.md
================================================
# Docker image security
## 1. Create a new image
Pull the latest alpine docker image as a base:
$ docker pull alpine:latest
You can find out the Repository Digest for the image with this command:
$ docker image ls --digests alpine
REPOSITORY TAG DIGEST IMAGE ID CREATED SIZE
alpine latest sha256:621c2f39f8133acb8e64023a94dbdf0d5ca81896102b9e57c0dc184cadaf5528 196d12cf6ab1 3 weeks ago 4.41MB
Create a simple Dockerfile image to build upon the fixed
mkdir myalpine
cd myalpine
repodigest=$(docker image ls --digests alpine --format "{{.Digest}}")
cat <<EOF > Dockerfile
FROM alpine@${repodigest}
MAINTAINER some maintainer <maintainer@example.com>
EXPOSE 443
EOF
Perform a build on this image:
docker build -t myalpine:1.0 .
### Checking the digests
At this stage you will note that the image does not have a digest:
docker image ls --digests myalpine
REPOSITORY TAG DIGEST IMAGE ID CREATED SIZE
myalpine 1.0 <none> 85aed7cdf75d 38 minutes ago 4.41MB
This is because a digest is a sha of the registry manifest and the layers. This does not exist until the image is pushed to a registry.
Tag your image with a pushable-name (i.e. starting with your dockerhub username) and push it to docker hub. You should be able to then see the image has a digest:
docker image ls --digests meekrosoft/myalpine
REPOSITORY TAG DIGEST IMAGE ID CREATED SIZE
meekrosoft/myalpine 1.0 sha256:b609be091e06834208b9d1d39e7e0fbfd60b550ea5d43a5476241d6218a8ee96 85aed7cdf75d 41 minutes ago 4.41MB
Ask your neighbour to pull your image and check the digest.
Fint!
================================================
FILE: labs/extra-exercises/nginx/docker-compose.yml
================================================
services:
one:
image: nginx:latest
restart: unless-stopped
volumes:
- "./html1:/usr/share/nginx/html"
ports:
- 9001:80
two:
image: nginx:latest
restart: unless-stopped
ports:
- 9002:80
volumes:
- "./html2:/usr/share/nginx/html"
nginx:
image: nginx:latest
restart: unless-stopped
ports:
- 9090:80
volumes:
- "./nginx:/etc/nginx/conf.d"
depends_on:
- one
- two
================================================
FILE: labs/extra-exercises/nginx/html1/index.html
================================================
<!DOCTYPE html>
<html>
<head>
</head>
<body style="background-color:red;">
<h1 style="color:white;">This is a heading</h1>
<p style="color:white;">This is a paragraph.</p>
</body>
</html>
================================================
FILE: labs/extra-exercises/nginx/html2/index.html
================================================
<!DOCTYPE html>
<html>
<head>
</head>
<body style="background-color:green;">
<h1>This is a heading</h1>
<p>This is a paragraph.</p>
</body>
</html>
================================================
FILE: labs/extra-exercises/nginx/network.md
================================================
# Docker networks
When you spin up several containers, they all share the same internal network.
In this exercise, we all use this for creating a `nginx` server that serves content from two other containers. For simplicity, the same `nginx` container wil be used, but each serving a page with a different background color, so it's easier to see that it behaves correctly.
## Creating the containers
There's a `docker-compose.yml` in this folder that contains the necessary configuration to spin up three nginx containers, configured to be a master nginx container. Then do a `docker compose up -d`. When you open a browser window, go to http://localhost:9090/ You'll see the default page from a new `nginx` installation. The configuration file here makes two entry points for the two containers. In the configuration, the host names are used, instead of IP addresses, Docker assigns IP addresses to containers, but the values can vary between runs. If you inspect the docker containers, you'll see a section with `Networks`, where the alias is listed . When docker containers are created, a host name is assigned to them, which is by default the same as the container name. In the `nginx` configuration file, the host names are used without the ports defined in the above example; this is because docker has an internal network, and the `nginx` image by default exposes port 80.
## Running the containers
Start the containers
$ docker compose up -d
The port numbers that are exposed were chosen a bit arbitrarily. You can remove the port configuration from container `one` and `two`, and see if the setup still works.
================================================
FILE: labs/extra-exercises/nginx/nginx/nginx.vh.default.conf
================================================
server {
listen 80 default_server;
listen [::]:80 default_server;
index index.html index.htm;
server_name _;
location /one {
proxy_pass http://one/index.html;
}
location /two {
proxy_pass http://two/index.html;
}
location / {
root /usr/share/nginx/html;
index index.html index.htm;
}
}
================================================
FILE: labs/extra-exercises/secrets.md
================================================
# Docker Secrets
Applications often require access to access tokens, passwords, and other sensitive information. Shipping this configuration in images poses security challenges, not to mention that with containers, applications are now dynamic and portable across multiple environments, making this a poor fit.
Docker secrets provide a means of managing sensitive information required at runtime independently of the build and run process.
> ## Store config in the environment
> An app’s config is everything that is likely to vary between deploys (staging, production, developer environments, etc). This includes:
>
> * Resource handles to the database, Memcached, and other backing services
> * Credentials to external services such as Amazon S3 or Twitter
> * Per-deploy values such as the canonical hostname for the deploy
> [https://12factor.net/config]
## Starting with Docker Secrets
To get started, we need to be running docker in swarm mode. Swarm is the distributed orchestration tool that originally shipped with docker, which now is being replaces with kubernetes.
The first step is to set up swarm mode on your docker host:
docker swarm init
You can see initially that there are no secrets being managed with this command:
$ docker secret ls
ID NAME DRIVER CREATED UPDATED
$
We can create a secret like this:
$ printf "docker1234" | docker secret create db_pwd -
w3yszkcy3ip08cgnfbiggq5e6
$ docker secret ls
ID NAME DRIVER CREATED UPDATED
w3yszkcy3ip08cgnfbiggq5e6 db_pwd 6 seconds ago 6 seconds ago
$
Now we want to grant access to our secret:
$ docker service create --name redis --secret db_pwd redis:alpine
And we can verify that the secret is available like this:
$ docker ps --filter name=redis -q
5cb1c2348a59
$ docker container exec $(docker ps --filter name=redis -q) ls -l /run/secrets
$ docker container exec $(docker ps --filter name=redis -q) cat /run/secrets/db_pwd
Verify it doesn't commit
$ docker commit $(docker ps --filter name=redis -q) committed_redis
$ docker run --rm -it committed_redis cat /run/secrets/my_secret_data
Try removing the secret. The removal fails because the redis service is running and has access to the secret.
$ docker secret ls
ID NAME DRIVER CREATED UPDATED
w3yszkcy3ip08cgnfbiggq5e6 db_pwd 6 seconds ago 6 seconds ago
$ docker secret rm db_pwd
Error response from daemon: rpc error: code = 3 desc = secret
'db_pwd' is in use by the following service: redis
Remove access to the secret from the running redis service by updating the service.
$ docker service update --secret-rm db_pwd redis
Cleanup the service, secret and leave swarm mode:
$ docker service rm redis
$ docker secret rm db_pwd
$ docker swarm leave --force
## Cheatsheet
| Command | Usage |
| --- | --- |
| docker secret create | Create a secret from a file or STDIN as content |
| docker secret inspect |Display detailed information on one or more secrets |
| docker secret ls | List secrets |
| docker secret rm | Remove one or more secrets |
## Slides (later)
High level overview - why secrets, diagram of how
## Additonal exercises
* [Docker lab on secrets](https://github.com/docker/labs/tree/master/security/secrets)
================================================
FILE: labs/image-best-practices.md
================================================
# Best practises
## dockerignore
Before the docker CLI sends the context to the docker daemon, it looks for a file named `.dockerignore` in the root directory of the context. If this file exists, the CLI modifies the context to exclude files and directories that match patterns in it. This helps to avoid unnecessarily sending large or sensitive files and directories to the daemon and potentially adding them to images using ADD or COPY.
> For more info on dockerignore, head over to the [documentation](https://docs.docker.com/engine/reference/builder/#dockerignore-file).
## Lint your Dockerfile
[Hadolint](https://hadolint.github.io/hadolint/) highlights dubious constraints in your `Dockerfile`.
The linter uses the principles described in [Docker's documentation on best practices](https://docs.docker.com/develop/develop-images/dockerfile_best-practices/) as the basis for the suggestions.
## Consider security when building images
[Snyk](https://snyk.io/blog/10-docker-image-security-best-practices/) wrote a blog with 10 things that you should consider when building images. They consider adding a label for the security policy of
the image, using a linter (as described above), and [signing docker images](https://docs.docker.com/notary/getting_started/).
================================================
FILE: labs/multi-container/docker-compose.yaml
================================================
services:
# wordpress_container:
mysql-container:
image: mysql:5.7.36
ports:
- 3306:3306
environment:
MYSQL_ROOT_PASSWORD: wordpress
MYSQL_DATABASE: wordpressdb
================================================
FILE: labs/multi-container/docker-compose_final.yaml
================================================
services:
wordpress-container:
image: wordpress:5.7.2-apache
ports:
- 8080:80
environment:
WORDPRESS_DB_USER: root
WORDPRESS_DB_PASSWORD: wordpress
WORDPRESS_DB_HOST: mysql-container
WORDPRESS_DB_NAME: wordpressdb
depends_on:
- mysql-container
mysql-container:
image: mysql:5.7.36
environment:
MYSQL_ROOT_PASSWORD: wordpress
MYSQL_DATABASE: wordpressdb
================================================
FILE: labs/multi-stage-build/Dockerfile
================================================
FROM golang:1.19 AS builder
WORKDIR /app
COPY . /app
RUN go mod download && go mod verify
RUN cd /app && go build -o goapp
ENTRYPOINT ./goapp
================================================
FILE: labs/multi-stage-build/go.mod
================================================
module example.com/mod
================================================
FILE: labs/multi-stage-build/hello.go
================================================
package main
import "fmt"
func main() {
fmt.Println("Hello world!")
}
================================================
FILE: labs/sharing-images.md
================================================
# Sharing images
Before we can take our dockerized Flask app to another computer, we need to push it up to Docker Hub so that publicly avaliable.
Docker Hub is like GitHub for Docker images. It’s the main place people store their Docker images in the cloud.
> :bulb: In order for you to do this exercise, you need to have an account. If not, create an account on the Docker Hub - it's free:
> [https://hub.docker.com/signup](https://hub.docker.com/signup)
Then, login to that account by running the `docker login` command on your laptop.
> :bulb: If you do not want your password to be stored on the workstation, create an access token instead: https://hub.docker.com/settings/security
We're almost ready to push our Flask image up to the Docker Hub. We just need to rename it to our namespace (which is the same as our docker username) first.
Using the `docker tag` command, tag the image you created in the previous section to your namespace. For example, I would run:
```
docker tag myfirstapp <your-dockerhub-username>/myfirstapp:latest
```
`myfirstapp` is the tag I used in my `docker build` commands in the previous section, and `<your-dockerhub-username>/myfirstapp:` is the full name of the new Docker image I want to push to the Hub.
The `:latest` is a versioning scheme you can append to.
All that's left to do is push up your image:
```
docker push <your-dockerhub-username>/myfirstapp
```
Expected output:
```
The push refers to a repository [docker.io/<your-dockerhub-username>/myfirstapp]
6daf7f1140cb: Pushed
7f74a217d86b: Pushed
09ccfff62b13: Pushed
f83ccb38761b: Pushed
584a7965008a: Pushed
33f1a94ed7fc: Mounted from library/ubuntu
b27287a6dbce: Mounted from library/ubuntu
47c2386f248c: Mounted from library/ubuntu
2be95f0d8a0c: Mounted from library/ubuntu
2df9b8def18a: Mounted from library/ubuntu
latest: digest: sha256:e7016870c297b3c49996ee00972d8abe7f20b4cbe45089dc914193fa894991d3 size: 2407
```
Go to your profile page on the Docker Hub and you should see your new repository listed:
[https://hub.docker.com/repos/u/<username>](https://hub.docker.com/repos/u/<username>)
**Congrats!** You just made your first Docker image and shared it with the world!
Try to pull down the images that your fellows have pushed to see that it's really up there.
For more info on the Docker Hub, and the cli integration,
head over to [https://docs.docker.com/docker-hub/](https://docs.docker.com/docker-hub/) and read the guides there.
================================================
FILE: labs/volumes/index.html
================================================
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Hello, world</title>
</head>
<body>
<h1>Hello, world!</h1>
<img src="https://www.docker.com/wp-content/uploads/2022/03/Moby-logo.png" style="margin-top: 0px;" >
</body>
</html>
================================================
FILE: labs/windows-docker/win1-windows-on-linux.md
================================================
# Dotnet core in Docker
Before starting with .NET in docker, it is important to know the following:
- The ASP framework is supported, but is done differently.
- .NET core runs natively in linux docker (we start with this)
- Microsoft is doing a lot of development on Docker for Windows, and are constantly improving the entire ecosystem. Things that did not work last week might work now.
And since there is always people wondering if it is production ready - .NET core has been production capable since 2.0 according to microsoft, and Kubernetes has support for Windows nodes.
Anyway let's get started !
```
docker container run -it -p 5000:5000 microsoft/dotnet
```
We expose port 5000 preemptively, since that is what our app will run on.
Inside the container, make a new directory (dotnet has issues running in root directory) and then make a new project:
```
mkdir myapp
cd myapp && dotnet new razor
```
The project should automatically restore nuget packages, but in the unlikely case it did not you can run :
```
dotnet restore
```
.NET has a thing with containers, where it needs to expose an environmental variable telling the environment where kestrel (.NET webserver) is allowed to host solutions:
```
export ASPNETCORE_URLS=http://*:5000
```
And then just run the app:
```
dotnet run
```
Go to localhost:5000 on your machine, you should have a fresh web app running (razor pages).
================================================
FILE: labs/windows-docker/win2-windows-on-windows.md
================================================
# ASP framework in containers
Before starting, it is important to be aware of something when working with containers and ASP framework.
Microsoft is pushing for a specific workflow with ASP framework and Docker, and making the best practice workflow (ie everything as code) is out of the scope of this workshop.
The workflow proposed by Microsoft, is to run publish (ie build) from Visual Studio, and then package that output into a container.
To make this correctly, would require us to get our hands real dirty with MSBuild and MSDeploy in powershell - but instead we will run various ready made containers to show off Windows containers as these workshop's focus is on docker.
To start off, remote desktop to the windows machine given and open powershell.
Run the familiar hello-world example:
```
docker run hello-world
```
The interesting thing here, is that command is being executed in powershell. On windows kernel. It is NOT the same hello-world we saw previously.
Let's ramp things up a bit:
```
docker run -it microsoft/nanoserver powershell
```
If you want leave out the "powershell" in the end, it will automatically execute cmd which messes a bit with the powershell of your VM.
Exit the container by typing `exit` to exit the container.
If you run `docker image ls`, you'll note that hello-world is built on nanoserver by looking at the size:
```
REPOSITORY TAG IMAGE ID CREATED SIZE
microsoft/dotnet-framework-samples latest e5cc04acc880 13 hours ago 12.4GB
microsoft/mssql-server-windows-developer latest 9e08a14c562e 3 days ago 11.6GB
hello-world latest b14262c9a790 2 weeks ago 1.1GB
microsoft/windowsservercore latest 1fbef5019583 3 weeks ago 10.4GB
microsoft/nanoserver latest edc711fca788 3 weeks ago 1.1GB
```
It is a bit bigger than the linux equivalent.. but it does the same thing, and loads an entire windows OS while we are at it.
The base image normally run in windows is microsoft/windowsservercore - and is what you should base your windows applications on.
```
docker run -it microsoft/windowsservercore powershell
```
The biggest challenge working with Windows containers in my experience, has been adapting things that are natively provided to run in a container.
Examples include how to build and deploy a project that normally Visual Studio and IIS took care of. This means the real tradeoff to using Windows containers is learning how Microsoft works under the hood. The gain is that a lot of the common problems with Windows go away.
> remember to `exit` your container before moving to the next command-
Containers will allow you to spin up things seamlessly, just like on Linux. For example:
```
docker run -d -p 1433:1433 -e sa_password=YOUR_PWD_HERE -e ACCEPT_EULA=Y microsoft/mssql-server-windows-developer
```
Which spins up a development server for Microsoft SQL. Since the remote desktop machines are not set up with tools, you cannot access it - but a real development machine could just use SQL management tools.
Or how about the entire azure powershell commandline interface?
```
docker run -it azuresdk/azure-powershell powershell
Get-Help Add-AzureRmAccount
```
Microsoft did a pretty good job, making it feel and seem like native docker - because it is. They have an upstream docker fork, that they pull in for releasing docker on windows.
Let's look at some examples to finish:
```
docker run microsoft/dotnet-framework-samples
```
or specify a specific ASP framework version:
```
docker run microsoft/dotnet-framework-samples:4.7.1
```
The docker file for the above example looks like this:
```
FROM microsoft/dotnet-framework-build:4.7.1 AS builder
WORKDIR /app
COPY . ./
RUN ["msbuild.exe", "dotnetapp-4.7.1.csproj", "/p:Configuration=Release"]
FROM microsoft/dotnet-framework:4.7.1
WORKDIR /app
COPY --from=builder /app/bin/Release .
ENTRYPOINT ["dotnetapp-4.7.1.exe"]
```
The above dockerfile is also a way to get started with shipping apps natively on Windows, as you can publish your app and copy it in in a similar fashion. Just make use of the servercore image instead.
These are not supposed to be base images, but serve as an exellent demo that Windows is capable of running native containers.
This concludes the ASP framework exercises.
================================================
FILE: labs/windows-docker/win3-common-commands.md
================================================
# The usual set of Docker commands on Windows
Now, time to show that Docker on Windows really just is Docker as you know it from Linux by now.
## Volumes
Let's start with volume. Make a folder in your C drive, called data and run:
```
mkdir \data
docker container run -it -v C:\data:C:\data microsoft/nanoserver powershell
```
Make some files in the directory by your nomal explorer, and run the following in your container:
```
PS C:\> dir data
Directory: C:\data
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a---- 11/29/2017 12:54 PM 0 Docker.txt
-a---- 11/29/2017 12:54 PM 0 On.txt
-a---- 11/29/2017 12:54 PM 0 Windows.txt
```
## Building
Similarly, let's play with a Dockerfile like the one below:
```
FROM microsoft/windowsservercore
ENV chocolateyUseWindowsCompression false
RUN powershell -Command \
iex ((new-object net.webclient).DownloadString('https://chocolatey.org/install.ps1'));
```
`Build` that image, and run a container based on your image.
You now have chocolatey (a package manager for Windows powershell):
```
chocolatey -?
```
It will print out the avaliable commands on chocolatey. Now, exit the container again.
## Port forwarding
Let's look at an IIS Server:
```
docker run -d -p 80:80 --name iis microsoft/iis
```
Which can be accessed via your windows machine on this ip:
```
docker inspect --format '{{ .NetworkSettings.Networks.nat.IPAddress }}' iis
```
You see the welcome screen of the IIS, but that is not very usefull, so let's start making an application.
## Compiling and serving code
This fresh installation does not have golang installed. We can just use a container to fix that.
Create the following file called `webserver.go`:
```
package main
import (
"fmt"
"net/http"
"os"
)
func main() {
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
fmt.Println("Proudly serving content on port", port)
panic(http.ListenAndServe(fmt.Sprintf(":%s", port), http.FileServer(http.Dir("."))))
}
```
and run:
```
docker run -it -v C:\data:C:\code -w C:\code golang:nanoserver powershell
go build webserver.go
```
Voila. Webserver.exe has been put into the current directory.
Now we need to serve the application in a container.
Make the following dockerfile in the same directory:
```
FROM microsoft/nanoserver
COPY webserver.exe /webserver.exe
EXPOSE 8080
CMD ["/webserver.exe"]
```
And build an image.
IIS needs to be able to find it later, and it does not run on localhost. So we need to name our container:
```
docker run -d --rm --name=mysite -p 8080:8080 <yourtag>
```
> the `--rm` part makes sure that if the container stops, it gets automatically deleted.
You can access it by running:
```
start http://$(docker inspect -f '{{ .NetworkSettings.Networks.nat.IPAddress }}' mysite):8080
```
It will show you a folder view of the containers files, including the webserver.exe that is running.
## Summary
This concludes the Windows bit of the workshop for now, but everything you worked with in regards to Docker works with Windows.
However multi container builds require a newer version of Docker than the Virtual machines have, so this is something you'll have to try at home ;)
================================================
FILE: trainer/README.md
================================================
# trainer
This is the how for additional examples and other trainer resources for the Praqma docker-slides and katas.
# Examples:
## Basic-compose
A simple compose example with only a single container.
## scratch
A simple example building a really empty image to demo that scratch is 0 bytes.
================================================
FILE: trainer/examples/basic-compose/Dockerfile
================================================
FROM python:3
#FROM ubuntu:latest
#RUN apt-get update && apt-get install -y \
# python-pip \
# python-dev \
# build-essential
COPY requirements.txt /usr/src/app/
RUN pip install --no-cache-dir -r /usr/src/app/requirements.txt
WORKDIR /usr/src/app/
ENTRYPOINT ["python"]
================================================
FILE: trainer/examples/basic-compose/README.md
================================================
# Simple single container compose example
## Build image
```
docker build -t basic-compose:latest .
```
## Run image manually
```
docker run --rm -p 8050:5000 -v $(pwd):/usr/src/app -d basic-compose:latest /usr/src/app/app.py
```
and show at [localhost:8050](http://localhost:8050) that it is running.
## Run with compose
Show compose content
Run with:
`docker compose up` and demo that it is running, then stop again with `docker compose down`.
Then show again with `docker compose up -d`
================================================
FILE: trainer/examples/basic-compose/app.py
================================================
from flask import Flask
app = Flask(__name__)
@app.route('/')
def hello_world():
return 'Hello! I am Jans Flask application'
if __name__ == '__main__':
# Note the extra host argument. If we didn't have it, our Flask app
# would only respond to requests from inside our container
app.run(host='0.0.0.0')
================================================
FILE: trainer/examples/basic-compose/docker-compose.yml
================================================
services:
app:
image: basic-compose:latest
ports:
- "8050:5000"
volumes:
- ".:/usr/src/app/"
command: "/usr/src/app/app.py"
================================================
FILE: trainer/examples/basic-compose/requirements.txt
================================================
flask>=1.0.0
================================================
FILE: trainer/examples/building-flask-on-different-os/Dockerfile
================================================
FROM ubuntu:22.04
RUN apt-get update && apt-get install -y \
python3-pip \
python3-dev \
build-essential
COPY requirements.txt /usr/src/app/
RUN pip3 install --no-cache-dir -r /usr/src/app/requirements.txt
COPY app.py /usr/src/app/
EXPOSE 5000
CMD ["python3", "/usr/src/app/app.py"]
================================================
FILE: trainer/examples/building-flask-on-different-os/Dockerfile-python
================================================
FROM python:3
COPY requirements.txt /usr/src/app/
RUN pip install --no-cache-dir -r /usr/src/app/requirements.txt
COPY app.py /usr/src/app/
EXPOSE 5000
CMD ["python", "/usr/src/app/app.py"]
================================================
FILE: trainer/examples/building-flask-on-different-os/Dockerfile-python-alpine
================================================
FROM python:3-alpine
COPY requirements.txt /usr/src/app/
RUN pip install --no-cache-dir -r /usr/src/app/requirements.txt
COPY app.py /usr/src/app/
EXPOSE 5000
CMD ["python", "/usr/src/app/app.py"]
================================================
FILE: trainer/examples/building-flask-on-different-os/README.md
================================================
# Building your flask application with three different from OS's
Building our flask application with Ubuntu might seem like a bad idea, as Ubuntu is a general purpose OS.
Looking into dockerhub we see that python already have an image we can use.
But after building that image, we see that the size of the image is almost doubled.
We go back again and find `python:3-alpine` based on the alpine OS.
This gives us a 75% reduction from Ubuntu, and ~90% reduction from normal python3 image.
```bash
praqmasofus/flaskapp python3-alpine f442f4941c53 9 minutes ago 109MB
praqmasofus/flaskapp python3 fb4d00f89ef3 10 minutes ago 926MB
praqmasofus/flaskapp ubuntu 817bf9d87465 8 days ago 435MB
```
================================================
FILE: trainer/examples/building-flask-on-different-os/app.py
================================================
import socket
from flask import Flask
app = Flask(__name__)
@app.route('/')
def hello_world():
return "Hello! I am a Flask application running on {}".format(socket.gethostname())
if __name__ == '__main__':
# Note the extra host argument. If we didn't have it, our Flask app
# would only respond to requests from inside our container
app.run(host='0.0.0.0')
================================================
FILE: trainer/examples/building-flask-on-different-os/build.sh
================================================
# /bin/bash
docker build -t flask-app .
docker build -f Dockerfile-python -t flask-app:python .
docker build -f Dockerfile-python-alpine -t flask-app:alpine .
================================================
FILE: trainer/examples/building-flask-on-different-os/requirements.txt
================================================
Flask==2.3.2
================================================
FILE: trainer/examples/ci-tools/README.md
================================================
# Docker CI tools
Note, you need to have run the "building flask on different OS" example first.
## Hadolint
https://github.com/hadolint/hadolint
```
docker run --rm -i hadolint/hadolint < Dockerfile
docker run --rm -i hadolint/hadolint < Dockerfile-python
docker run --rm -i hadolint/hadolint < Dockerfile-python-alpine
```
## Trivy
See the different severities you get by the different OS.
```bash
docker run -v /var/run/docker.sock:/var/run/docker.sock -v aquacache:/root/.cache aquasec/trivy image mypy:latest
docker run -v aquacache:/root/.cache aquasec/trivy image mypy:python
docker run -v aquacache:/root/.cache aquasec/trivy image mypy:python-alpine
```
================================================
FILE: trainer/examples/multi-stage-golang/Dockerfile
================================================
FROM golang:1.13.2 AS builder
WORKDIR /go
COPY hello.go /go
RUN go build hello.go
FROM scratch
COPY --from=builder /go/hello /hello
CMD ["/hello"]
================================================
FILE: trainer/examples/multi-stage-golang/README.md
================================================
# Multistage build with go app
Building a go application with the `golang` container,
and copying it into `scratch`.
## Build
```shell
$ docker build -t golang-scratch .
...
Successfully tagged golang-scratch:latest
```
## Run the image
```shell
$ docker run golang-scratch
hello world
```
## Show the image size
```shell
$ docker image ls | grep golang
golang-scracth latest 9330bf30dfe5 2 minutes ago 2.01MB
golang 1.13.2-buster e37698ff7351 2 days ago 803MB
```
================================================
FILE: trainer/examples/multi-stage-golang/hello.go
================================================
package main
import "fmt"
func main() {
fmt.Println("hello world")
}
================================================
FILE: trainer/examples/multistage-java/Dockerfile
================================================
FROM gradle:jdk11
LABEL author="Sofus Albertsen"
COPY --chown=gradle:gradle . /home/gradle/src
WORKDIR /home/gradle/src
RUN gradle shadowjar
RUN cp /home/gradle/src/build/libs/app-*-all.jar app.jar
EXPOSE 8000
CMD java -Dcom.sun.management.jmxremote -noverify ${JAVA_OPTS} -jar app.jar
================================================
FILE: trainer/examples/multistage-java/Dockerfile-multistage
================================================
FROM gradle:jdk11 AS builder
COPY --chown=gradle:gradle . /home/gradle/src
WORKDIR /home/gradle/src
RUN gradle shadowjar
FROM adoptopenjdk/openjdk11-openj9:alpine-slim
LABEL author="Sofus Albertsen"
COPY --from=builder /home/gradle/src/build/libs/app-*-all.jar app.jar
EXPOSE 8000
CMD java -Dcom.sun.management.jmxremote -noverify ${JAVA_OPTS} -jar app.jar
================================================
FILE: trainer/examples/multistage-java/README.md
================================================
# Multistage build with java app
Building a java application with the `gradle` container,
and copying it into `java`.
## Build
```shell
$ docker build -t java-app:single .
...
Successfully tagged java-app:single
```
And build the multi stage dockerfile as well:
```shell
$ docker build -t java-app:multi -f Dockerfile-multistage .
...
Successfully tagged java-app:multi
```
## Show the image size
```shell
$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
java-app multi d2f488502c4d 22 seconds ago 259MB
java-app single 0e8df415465d 3 minutes ago 701MB
```
================================================
FILE: trainer/examples/multistage-java/build.gradle
================================================
plugins {
id "net.ltgt.apt-eclipse" version "0.21"
id "com.github.johnrengelman.shadow" version "5.0.0"
id "application"
}
version "0.1"
group "example.micronaut"
repositories {
mavenCentral()
maven { url "https://jcenter.bintray.com" }
}
configurations {
// for dependencies that are needed for development only
developmentOnly
}
dependencies {
annotationProcessor platform("io.micronaut:micronaut-bom:$micronautVersion")
annotationProcessor "io.micronaut:micronaut-inject-java"
annotationProcessor "io.micronaut:micronaut-validation"
implementation platform("io.micronaut:micronaut-bom:$micronautVersion")
implementation "io.micronaut:micronaut-inject"
implementation "io.micronaut:micronaut-validation"
implementation "io.micronaut:micronaut-runtime"
implementation "javax.annotation:javax.annotation-api"
implementation "io.micronaut:micronaut-http-server-netty"
implementation "io.micronaut:micronaut-http-client"
runtimeOnly "ch.qos.logback:logback-classic:1.2.3"
testAnnotationProcessor platform("io.micronaut:micronaut-bom:$micronautVersion")
testAnnotationProcessor "io.micronaut:micronaut-inject-java"
testImplementation platform("io.micronaut:micronaut-bom:$micronautVersion")
testImplementation "org.junit.jupiter:junit-jupiter-api"
testImplementation "io.micronaut.test:micronaut-test-junit5"
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine"
}
test.classpath += configurations.developmentOnly
mainClassName = "example.micronaut.Application"
// use JUnit 5 platform
test {
useJUnitPlatform()
}
tasks.withType(JavaCompile){
options.encoding = "UTF-8"
options.compilerArgs.add('-parameters')
}
shadowJar {
mergeServiceFiles()
}
run.classpath += configurations.developmentOnly
run.jvmArgs('-noverify', '-XX:TieredStopAtLevel=1', '-Dcom.sun.management.jmxremote')
================================================
FILE: trainer/examples/multistage-java/gradle.properties
================================================
micronautVersion=1.2.7
================================================
FILE: trainer/examples/multistage-java/micronaut-cli.yml
================================================
profile: service
defaultPackage: example.micronaut
---
testFramework: junit
sourceLanguage: java
================================================
FILE: trainer/examples/multistage-java/settings.gradle
================================================
rootProject.name="app"
================================================
FILE: trainer/examples/multistage-java/src/main/java/example/micronaut/Application.java
================================================
package example.micronaut;
import io.micronaut.runtime.Micronaut;
public class Application {
public static void main(String[] args) {
Micronaut.run(Application.class);
}
}
================================================
FILE: trainer/examples/multistage-java/src/main/java/example/micronaut/HelloController.java
================================================
package example.micronaut;
import io.micronaut.http.MediaType;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import io.micronaut.http.annotation.Produces;
@Controller("/hello")
public class HelloController{
@Get("/{name}")
@Produces(MediaType.TEXT_PLAIN)
public String index(String name) {
return combineName(name);
}
//Example of a function that can be tested through normal unit frameworks
public String combineName( String name) {
return "Hello "+name;
}
}
================================================
FILE: trainer/examples/multistage-java/src/main/java/example/micronaut/ReadyController.java
================================================
package example.micronaut;
import io.micronaut.http.MediaType;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import io.micronaut.http.annotation.Produces;
@Controller("/status")
public class ReadyController{
@Get("/")
@Produces(MediaType.TEXT_PLAIN)
public String index() {
return "Up and running";
}
}
================================================
FILE: trainer/examples/multistage-java/src/main/java/example/micronaut/RootController.java
================================================
package example.micronaut;
import io.micronaut.http.MediaType;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import io.micronaut.http.annotation.Produces;
@Controller("/")
public class RootController{
@Get("/")
@Produces(MediaType.TEXT_HTML)
public String index() {
return "<h1> Hello World</h1>";
}
}
================================================
FILE: trainer/examples/multistage-java/src/main/resources/application.yml
================================================
micronaut:
server:
port: 8000
application:
name: app
================================================
FILE: trainer/examples/multistage-java/src/main/resources/logback.xml
================================================
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<withJansi>true</withJansi>
<!-- encoders are assigned the type
ch.qos.logback.classic.encoder.PatternLayoutEncoder by default -->
<encoder>
<pattern>%cyan(%d{HH:mm:ss.SSS}) %gray([%thread]) %highlight(%-5level) %magenta(%logger{36}) - %msg%n</pattern>
</encoder>
</appender>
<root level="info">
<appender-ref ref="STDOUT" />
</root>
</configuration>
================================================
FILE: trainer/examples/multistage-java/src/test/java/example/micronaut/HelloControllerTest.java
================================================
package example.micronaut;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.client.RxHttpClient;
import io.micronaut.http.client.annotation.Client;
import io.micronaut.test.annotation.MicronautTest;
import org.junit.jupiter.api.Test;
import javax.inject.Inject;
@MicronautTest
public class HelloControllerTest {
@Inject
@Client("/")
RxHttpClient client;
//Test spins up an entire server and client to perform the test
@Test
public void testHello() {
HttpRequest<String> request = HttpRequest.GET("/hello/sofus");
String body = client.toBlocking().retrieve(request);
assertNotNull(body);
assertEquals("Hello sofus", body);
}
@Test
public void testCombineName() {
String name = "Sonny";
HelloController sut = new HelloController();
System.out.println("testing");
assertEquals("Hello "+name, sut.combineName(name),"Name and greeting not properly combined");
}
}
================================================
FILE: trainer/examples/nextcloud/README.md
================================================
# Nextcloud showcase
An example of starting a two-container application using docker compose; Nextcloud with a MariaDB backend.
We show that the volumes persist killing the containers,
and a version-upgrade of Nextcloud.
Tags: docker-compose, volumes, upgrading
## Part 1: Persistent storage
1. Start the application with:
```shell
docker compose up
```
1. Visit Nextcloud in a browser on: [localhost](http://localhost),
and create an admin account.
> NB: It takes ~1 min for `mariadb` to become ready.
> (During a demo, you'll usually talk for this minute about the demo..)
> If you get the error:
>
> ```output
> Fejl:
> Error while trying to create admin user: Failed to connect to the database: An exception occured in driver: SQLSTATE[HY000] [2002]Connection refused
> ```
>
> Wait a little while, fill in the password again, and try to finish the setup again.
## Upload a file
1. Drag a picture or other file from your computer into the UI,
to upload it to your Nextcloud.
1. Stop the services afterwards.
> If you ran `docker compose` without `-d`,
> then `ctrl+c` will stop the services.
```shell
docker compose down
```
1. Reload the window in the browser; Nextcloud is down.
1. Show that no containers are running with the command:
```shell
docker ps
```
## Removing the containers (optional)
1. You can even remove the containers:
```shell
docker compose rm
```
1. Show that they're gone with `docker ps -a`.
## Restart Nextcloud
1. Start the application again with,
```shell
docker compose up
```
1. Reload your browser window,
Nextcloud should be available again;
you might even still be logged in.
## Persistent storage
1. Notice how the file you uploaded before is still available.
## Part 2: Upgrading Nextcloud
1. Stop the services again, using `docker compose down` or `ctrl+c`.
1. Change the version of the Nextcloud image from `nextcloud:X`
to `nextcloud:X+1` (swap the commented image-version lines,
in the `docker-compose.yml`.)
## Start the Application
1. Run `docker compose up` to start the application.
You can find the version under the `gear icon -> help`,
[link](http://localhost/settings/help).
## Downgrading Nextcloud
> Downgrading is not supported in Nextcloud.
1. Try stopping the containers again
with `docker compose down` or `ctrl+c`.
1. Changing the version back to `nextcloud:12`.
1. Starting the application again with `docker compose up`.
Nextcloud will fail to start with the message:
```shell
app_1 | Can't start Nextcloud because the version of the data (12.0.13.2) is higher than the docker image version (11.0.8.1) and downgrading is not supported. Are you sure you have pulled the newest image version?
```
> NB: This composefile is missing a network directive, but that is ok.
> Docker will create a network for these containers
> and connect them on that network.
## Possible issues
### Making Nextcloud fail without `depends_on`
In the original example,
the upgrade would fail without adding the `depends_on: db`
to the docker compose file.
Nicolaj has been unable to reproduce this error on `Docker for Windows 19.03`, and thus the "example of using `depends_on`" is left out of the tutorial.
`depends_on` is however kept in the docker compose file,
so as to not cause any unintentional errors.
================================================
FILE: trainer/examples/nextcloud/docker-compose.yml
================================================
volumes:
nextcloud:
db:
services:
app:
image: nextcloud:23
# image: nextcloud:24
depends_on:
- db
environment:
MYSQL_PASSWORD: nextcloud
MYSQL_DATABASE: nextcloud
MYSQL_USER: root
MYSQL_HOST: db
ports:
- 80:80
volumes:
- nextcloud:/var/www/html:rw
db:
image: mariadb:10.4.12-bionic
environment:
MYSQL_ROOT_PASSWORD: nextcloud
volumes:
- db:/var/lib/mysql:rw
================================================
FILE: trainer/examples/scratch/Dockerfile
================================================
FROM scratch
COPY emptyfile .
================================================
FILE: trainer/examples/scratch/README.md
================================================
# Empty image (useles, but really empty)
In this example we show that Scratch is in fact empty.
It is not possible to `docker pull scratch` as the image doesn't actualy exist.
Neither is it possible ot just do a `FROM scratch` Dockerfile.
But we can do the following:
```
FROM scratch
COPY emptyfile .
```
and then `docker build -t empty:latest .`
We can now use `docker image ls` to see that the created image is empty.
================================================
FILE: trainer/examples/scratch/emptyfile
================================================
================================================
FILE: trainer/examples/security-run-as-root/README.md
================================================
# Running as Root in a Container
The purpose of this example is to show,
if you mount the filesystem into a container running as root,
the container will have root privileges on the filesystem.
This example runs well in a Google Cloud Shell.
## As a basic user
1. show the current user
```shell
$ id
uid=1000(ng) gid=1000(ng) groups=1000(ng),4(adm),27(sudo),999(docker)
```
1. create a file and show the owner/permissions
```shell
$ touch file1.txt
$ ls -la
total 16
drwxr-xr-x 2 ng ng 4096 Sep 23 15:50 .
drwxr-xr-x 7 ng ng 4096 Sep 23 15:40 ..
-rw-r--r-- 1 ng ng 0 Sep 23 15:50 file1.txt
```
> `file1.txt` is has
>
> - `user:group` set to your user e.g. `ng:ng`
> - permissions `-rw-r--r--`, where the first
> - `rw-` is `read, write` for `user`, the next
> - `r--` is `read` for `group`, and the last
> - `r--` is `read` for `other`. So,
> - ng (`user`) can `read` and `write` to the file,
> - users in the ng (`group`) can `read`, and
> - everyone on the linux system (`other`) can `read` the file.
## In the same folder
1. start a privileged container that mounts in the current folder
```shell
$ docker run --rm -it -v $(pwd):/mnt/host alpine
/ #
```
1. show the current user (we have `uid=0` and are `root`)
```shell
# id
uid=0(root) gid=0(root) groups=0(root),1(bin),2(daemon),3(sys),4(adm),6(disk),10(wheel),11(floppy),20(dialout),26(tape),27(video)
```
1. show the files and permissions in the `/mnt/host` folder
(we see the file we created before)
```shell
/ # cd /mnt/host/
/mnt/host # ls -la
total 16
drwxr-xr-x 2 1000 1000 4096 Sep 23 13:53 .
drwxr-xr-x 1 root root 4096 Sep 23 13:42 ..
-rw-r--r-- 1 1000 1000 0 Sep 23 13:50 file1.txt
```
1. create a file with some secret content and show the permissions
```shell
/mnt/host # echo "secret" > file2.txt
/mnt/host # ls -la
total 12
drwxr-xr-x 2 1000 1000 4096 Sep 23 14:18 .
drwxr-xr-x 1 root root 4096 Sep 23 13:42 ..
-rw-r--r-- 1 1000 1000 0 Sep 23 13:50 file1.txt
-rw-r--r-- 1 root root 7 Sep 23 14:18 file2.txt
```
1. delete the `read` permission for `other` on the file
```shell
/mnt/host # chmod o-r file2.txt
/mnt/host # ls -la
total 12
drwxr-xr-x 2 1000 1000 4096 Sep 23 14:18 .
drwxr-xr-x 1 root root 4096 Sep 23 13:42 ..
-rw-r--r-- 1 1000 1000 0 Sep 23 13:50 file1.txt
-rw-r----- 1 root root 7 Sep 23 14:18 file2.txt
/mnt/host #
```
1. exit the privileged container, and try viewing the permissions on the files
(`file2.txt` is owned by `root:root`)
```shell
/mnt/host # exit
$ ls -la
total 12
drwxr-xr-x 2 ng ng 4096 Sep 23 16:21 .
drwxr-xr-x 7 ng ng 4096 Sep 23 15:40 ..
-rw-r--r-- 1 ng ng 0 Sep 23 15:50 file1.txt
-rw-r----- 1 root root 7 Sep 23 16:18 file2.txt
```
1. try viewing the contents of the file
(we can't since `other` doesn't have any permissons on the file)
```shell
$ cat file2.txt
cat: file2.txt: Permission denied
```
1. Use the root user to view the contents of the file
```shell
$ sudo cat file2.txt
secret
```
## Conclusion
Use great caution when mounting the filesystem into a container,
and pay attention to the privileges that a container is running with.
Greatly inspired by the
[Keynote: Running with Scissors - Liz Rice, Technology Evangelist, Aqua Security](https://www.youtube.com/watch?v=ltrV-Qmh3oY) at KubeCon 2018
================================================
FILE: trainer/examples/volume-python/README.md
================================================
The idea for this one is to make a small python application that prints "Hello world!" to your terminal, and volume mount in the code from the host.
It is a pre-example to building your own image.
## Exercise
You need a terminal in this folder.
Run:
`docker run -v "$PWD":/usr/src/myapp -w /usr/src/myapp python:3 python main.py`
================================================
FILE: trainer/examples/volume-python/main.py
================================================
import os
import socket
def get_system_info():
# Get the host name
host_name = socket.gethostname()
# Get the OS name
os_name = os.name
if os_name == 'posix':
os_name = os.uname().sysname
elif os_name == 'nt':
os_name = 'Windows'
return host_name, os_name
if __name__ == "__main__":
host_name, os_name = get_system_info()
print(f"Host Name: {host_name}")
print(f"OS Name: {os_name}")
================================================
FILE: trainer/experiments/unicode/README.md
================================================
# Testing unicode support
```
docker build -f Файлдокера -t unicode:latest .
docker run -it unicode:latest
```
Sadly, Docker doesn't support unicode in image names and tags. 😢
================================================
FILE: trainer/experiments/unicode/Файлдокера
================================================
FROM alpine:latest
MAINTAINER "ℑⒶ𝕹 𝕂®𝓪ⓖ 😋"
COPY ఖాళీగా .
RUN touch فارغة
RUN echo "नमस्ते विश्व यो डेमो नमूना हो"
CMD ["echo","😱😅👾❗️"]
================================================
FILE: trainer/experiments/unicode/ఖాళీగా
================================================
gitextract_5i3fxtl4/
├── .gitignore
├── .phlow
├── CHEATSHEET.md
├── LICENSE
├── README.md
├── labs/
│ ├── 00-getting-started.md
│ ├── 01-hello-world.md
│ ├── 02-running-images.md
│ ├── 03-deletion.md
│ ├── 04-port-forward.md
│ ├── 05-executing.md
│ ├── 06-volumes.md
│ ├── 07-building-an-image.md
│ ├── 08-multi-stage-builds.md
│ ├── 09-multi-container.md
│ ├── README.md
│ ├── advanced/
│ │ ├── containers-on-default-bridge.md
│ │ ├── containers-on-docker-compose-network.md
│ │ ├── containers-on-host-network.md
│ │ ├── containers-on-none-network.md
│ │ ├── containers-on-user-defined-bridge.md
│ │ ├── joining-network-and-process-namespace-of-existing-containers.md
│ │ ├── systemd/
│ │ │ ├── Dockerfile
│ │ │ ├── README.md
│ │ │ └── docker-compose.yml
│ │ └── traefik/
│ │ ├── example1/
│ │ │ ├── README.md
│ │ │ ├── docker-compose.yml
│ │ │ └── traefik.toml
│ │ ├── example2/
│ │ │ ├── README.md
│ │ │ ├── proxy/
│ │ │ │ ├── docker-compose.yml
│ │ │ │ └── traefik.toml
│ │ │ └── web/
│ │ │ └── docker-compose.yml
│ │ └── example3/
│ │ ├── acme.json
│ │ ├── docker-compose.yml
│ │ └── traefik.toml
│ ├── building-an-image/
│ │ ├── Dockerfile
│ │ ├── app.py
│ │ └── requirements.txt
│ ├── docker-compose/
│ │ ├── .gitignore
│ │ ├── 00-getting-started.md
│ │ ├── 01-hello-world.md
│ │ ├── 02-port-forward.md
│ │ ├── 03-volumes.md
│ │ ├── 04-build-image.md
│ │ └── bottle/
│ │ ├── Dockerfile
│ │ ├── app.py
│ │ └── requirements.txt
│ ├── exercise-template.md
│ ├── extra-exercises/
│ │ ├── image-security.md
│ │ ├── nginx/
│ │ │ ├── docker-compose.yml
│ │ │ ├── html1/
│ │ │ │ └── index.html
│ │ │ ├── html2/
│ │ │ │ └── index.html
│ │ │ ├── network.md
│ │ │ └── nginx/
│ │ │ └── nginx.vh.default.conf
│ │ └── secrets.md
│ ├── image-best-practices.md
│ ├── multi-container/
│ │ ├── docker-compose.yaml
│ │ └── docker-compose_final.yaml
│ ├── multi-stage-build/
│ │ ├── Dockerfile
│ │ ├── go.mod
│ │ └── hello.go
│ ├── sharing-images.md
│ ├── volumes/
│ │ └── index.html
│ └── windows-docker/
│ ├── win1-windows-on-linux.md
│ ├── win2-windows-on-windows.md
│ └── win3-common-commands.md
└── trainer/
├── README.md
├── examples/
│ ├── basic-compose/
│ │ ├── Dockerfile
│ │ ├── README.md
│ │ ├── app.py
│ │ ├── docker-compose.yml
│ │ └── requirements.txt
│ ├── building-flask-on-different-os/
│ │ ├── Dockerfile
│ │ ├── Dockerfile-python
│ │ ├── Dockerfile-python-alpine
│ │ ├── README.md
│ │ ├── app.py
│ │ ├── build.sh
│ │ └── requirements.txt
│ ├── ci-tools/
│ │ └── README.md
│ ├── multi-stage-golang/
│ │ ├── Dockerfile
│ │ ├── README.md
│ │ └── hello.go
│ ├── multistage-java/
│ │ ├── Dockerfile
│ │ ├── Dockerfile-multistage
│ │ ├── README.md
│ │ ├── build.gradle
│ │ ├── gradle.properties
│ │ ├── micronaut-cli.yml
│ │ ├── settings.gradle
│ │ └── src/
│ │ ├── main/
│ │ │ ├── java/
│ │ │ │ └── example/
│ │ │ │ └── micronaut/
│ │ │ │ ├── Application.java
│ │ │ │ ├── HelloController.java
│ │ │ │ ├── ReadyController.java
│ │ │ │ └── RootController.java
│ │ │ └── resources/
│ │ │ ├── application.yml
│ │ │ └── logback.xml
│ │ └── test/
│ │ └── java/
│ │ └── example/
│ │ └── micronaut/
│ │ └── HelloControllerTest.java
│ ├── nextcloud/
│ │ ├── README.md
│ │ └── docker-compose.yml
│ ├── scratch/
│ │ ├── Dockerfile
│ │ ├── README.md
│ │ └── emptyfile
│ ├── security-run-as-root/
│ │ └── README.md
│ └── volume-python/
│ ├── README.md
│ └── main.py
└── experiments/
└── unicode/
├── README.md
├── Файлдокера
└── ఖాళీగా
SYMBOL INDEX (20 symbols across 12 files)
FILE: labs/building-an-image/app.py
function hello_world (line 6) | def hello_world():
FILE: labs/docker-compose/bottle/app.py
function index (line 5) | def index():
function hello (line 9) | def hello(name):
FILE: labs/multi-stage-build/hello.go
function main (line 5) | func main() {
FILE: trainer/examples/basic-compose/app.py
function hello_world (line 6) | def hello_world():
FILE: trainer/examples/building-flask-on-different-os/app.py
function hello_world (line 6) | def hello_world():
FILE: trainer/examples/multi-stage-golang/hello.go
function main (line 5) | func main() {
FILE: trainer/examples/multistage-java/src/main/java/example/micronaut/Application.java
class Application (line 5) | public class Application {
method main (line 7) | public static void main(String[] args) {
FILE: trainer/examples/multistage-java/src/main/java/example/micronaut/HelloController.java
class HelloController (line 8) | @Controller("/hello")
method index (line 10) | @Get("/{name}")
method combineName (line 17) | public String combineName( String name) {
FILE: trainer/examples/multistage-java/src/main/java/example/micronaut/ReadyController.java
class ReadyController (line 8) | @Controller("/status")
method index (line 10) | @Get("/")
FILE: trainer/examples/multistage-java/src/main/java/example/micronaut/RootController.java
class RootController (line 8) | @Controller("/")
method index (line 10) | @Get("/")
FILE: trainer/examples/multistage-java/src/test/java/example/micronaut/HelloControllerTest.java
class HelloControllerTest (line 14) | @MicronautTest
method testHello (line 22) | @Test
method testCombineName (line 30) | @Test
FILE: trainer/examples/volume-python/main.py
function get_system_info (line 4) | def get_system_info():
Condensed preview — 108 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (162K chars).
[
{
"path": ".gitignore",
"chars": 4999,
"preview": "\n# Created by https://www.gitignore.io/api/csharp\n\n### Csharp ###\n## Ignore Visual Studio temporary files, build results"
},
{
"path": ".phlow",
"chars": 186,
"preview": "[default]\nremote = origin\nservice = github\nintegration_branch = master\nissue_url "
},
{
"path": "CHEATSHEET.md",
"chars": 2222,
"preview": "Source: https://docs.docker.com/get-started/docker_cheatsheet.pdf\n\n## General\n\n- [Docker cli](https://docs.docker.com/en"
},
{
"path": "LICENSE",
"chars": 1063,
"preview": "MIT License\n\nCopyright (c) 2018 Praqma\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof "
},
{
"path": "README.md",
"chars": 1246,
"preview": "# Docker katas\r\n\r\n[](https://conso"
},
{
"path": "labs/00-getting-started.md",
"chars": 1169,
"preview": "# Getting started\n\nIn this section you will install docker.\n\n## Terminology\n\nThroughout these labs, you will see a lot o"
},
{
"path": "labs/01-hello-world.md",
"chars": 1823,
"preview": "# hello-world\n\n## Learning Goals\n\nThe goal of this scenario is to make you run your first Docker container.\n\n## Terminol"
},
{
"path": "labs/02-running-images.md",
"chars": 6098,
"preview": "# Running your first container from image\n\n## Learning Goals\n\n\n- Run an [Alpine Linux](http://www.alpinelinux.org/) cont"
},
{
"path": "labs/03-deletion.md",
"chars": 6185,
"preview": "# Throw your container away\n\nAs containers are just a thin base layer on top of the host kernel, it is really fast to sp"
},
{
"path": "labs/04-port-forward.md",
"chars": 2778,
"preview": "# A basic webserver\n\nRunning arbitrary Linux commands inside a Docker container is fun, but let's do something more usef"
},
{
"path": "labs/05-executing.md",
"chars": 1754,
"preview": "# Executing processes in your container\n\nIt you want to examine a running container, but do not want to disturb the runn"
},
{
"path": "labs/06-volumes.md",
"chars": 8935,
"preview": "# Docker volumes\n\n> _Hint: This lab only covers volumes on Docker for Linux. If you are on windows or mac, things can lo"
},
{
"path": "labs/07-building-an-image.md",
"chars": 16184,
"preview": "# Constructing a docker image\n\nRunning images others made is useful, but if you want to use docker for your own applicat"
},
{
"path": "labs/08-multi-stage-builds.md",
"chars": 3079,
"preview": "# Multi Stage Builds\n\n## Task: create a tiny go-application container\n\nIn [multi-stage-build/hello.go](multi-stage-build"
},
{
"path": "labs/09-multi-container.md",
"chars": 12301,
"preview": "# Multi container setups\n\n## Task: host a Wordpress site\n\nIn this scenario, we are going to deploy the CMS system called"
},
{
"path": "labs/README.md",
"chars": 2667,
"preview": "# Docker labs\n\nIn this folder are a lot of exercises. They are numbered in the way we think makes sence to introduce the"
},
{
"path": "labs/advanced/containers-on-default-bridge.md",
"chars": 1756,
"preview": "# Exploration - Containers on default bridge network\nIn this exercise, you will explore various characterstics of the de"
},
{
"path": "labs/advanced/containers-on-docker-compose-network.md",
"chars": 2403,
"preview": "# Exploration - Containers on docker compose bridge network\nIn this exercise, you will explore various characterstics of"
},
{
"path": "labs/advanced/containers-on-host-network.md",
"chars": 1483,
"preview": "# Exploration - Containers on host network\nIn this exercise, you will explore various characterstics of the **host netwo"
},
{
"path": "labs/advanced/containers-on-none-network.md",
"chars": 1523,
"preview": "# Exploration - Containers on none network\nIn this exercise, you will explore various characterstics of the **none** net"
},
{
"path": "labs/advanced/containers-on-user-defined-bridge.md",
"chars": 2709,
"preview": "# Exploration - Containers on user-defined bridge network\nIn this exercise, you will explore various characterstics of t"
},
{
"path": "labs/advanced/joining-network-and-process-namespace-of-existing-containers.md",
"chars": 4250,
"preview": "# Learn how to join containers to network and process namespace of other containers\nIn this exercise, you will learn how"
},
{
"path": "labs/advanced/systemd/Dockerfile",
"chars": 1204,
"preview": "FROM centos:7\n\nCMD [\"/usr/sbin/init\"]\n\n# Delete everything inside `/lib/systemd/system/sysinit.target.wants/`, except `s"
},
{
"path": "labs/advanced/systemd/README.md",
"chars": 1042,
"preview": "# Build and run instructions for systemd based containers:\n\n## Build:\n```\ndocker build -t local/centos7:mailserver\n```\n\n"
},
{
"path": "labs/advanced/systemd/docker-compose.yml",
"chars": 251,
"preview": "services:\n mailserver:\n build: .\n ports:\n - \"80:80\"\n - \"443:443\"\n - \"25:25\"\n - \"110:110\"\n "
},
{
"path": "labs/advanced/traefik/example1/README.md",
"chars": 592,
"preview": "Make sure you set up some sort of DNS / name resolution from your computer to the IP address where reverse proxy is runn"
},
{
"path": "labs/advanced/traefik/example1/docker-compose.yml",
"chars": 363,
"preview": "services:\n traefik:\n image: traefik:1.7\n volumes:\n - /var/run/docker.sock:/var/run/docker.sock\n - ${PWD"
},
{
"path": "labs/advanced/traefik/example1/traefik.toml",
"chars": 53,
"preview": "[docker]\n endpoint = \"unix:///var/run/docker.sock\"\n\n"
},
{
"path": "labs/advanced/traefik/example2/README.md",
"chars": 2439,
"preview": "# Traefik proxy running as independent docker compose app:\nIn this example, Traefik runs as an independent docker compos"
},
{
"path": "labs/advanced/traefik/example2/proxy/docker-compose.yml",
"chars": 512,
"preview": "services:\n traefik:\n image: traefik:1.7\n volumes:\n - /var/run/docker.sock:/var/run/docker.sock\n - ${PWD"
},
{
"path": "labs/advanced/traefik/example2/proxy/traefik.toml",
"chars": 84,
"preview": "[docker]\n endpoint = \"unix:///var/run/docker.sock\"\n network = \"services-network\"\n\n"
},
{
"path": "labs/advanced/traefik/example2/web/docker-compose.yml",
"chars": 276,
"preview": "services:\n multitool:\n image: praqma/network-multitool\n labels:\n - traefik.enable=true\n - traefik.port="
},
{
"path": "labs/advanced/traefik/example3/acme.json",
"chars": 0,
"preview": ""
},
{
"path": "labs/advanced/traefik/example3/docker-compose.yml",
"chars": 1556,
"preview": "services:\n traefik:\n image: traefik:1.7\n labels:\n - traefik.frontend.rule=Host:traefik.example.com\n - t"
},
{
"path": "labs/advanced/traefik/example3/traefik.toml",
"chars": 1713,
"preview": "defaultEntryPoints = [\"http\", \"https\"]\nlogLevel = \"INFO\"\n\n[docker]\n endpoint = \"unix:///var/run/docker.sock\"\n exposedB"
},
{
"path": "labs/building-an-image/Dockerfile",
"chars": 215,
"preview": "# Dockerfile for docker-flask web application\n\n# Add a base image to build this image off of\nFROM \n\n# Add a default port"
},
{
"path": "labs/building-an-image/app.py",
"chars": 376,
"preview": "import socket\nfrom flask import Flask\napp = Flask(__name__)\n\n@app.route('/')\ndef hello_world():\n return \"Hello! I am "
},
{
"path": "labs/building-an-image/requirements.txt",
"chars": 13,
"preview": "Flask==2.3.2\n"
},
{
"path": "labs/docker-compose/.gitignore",
"chars": 56,
"preview": "#Prevent leaking any premade solutions\n*.yml\nweb_content"
},
{
"path": "labs/docker-compose/00-getting-started.md",
"chars": 334,
"preview": "# Getting started\n\nIn this section you will install Docker and run your container using compose.\n\n## Installing Docker\n\n"
},
{
"path": "labs/docker-compose/01-hello-world.md",
"chars": 2341,
"preview": "# hello-world\n\nTry running a command with docker compose:\n\n`docker compose up`\n\nYour terminal output should look like th"
},
{
"path": "labs/docker-compose/02-port-forward.md",
"chars": 2342,
"preview": "# A basic webserver\n\nRunning arbitrary Linux commands inside a Docker container with docker compose is quite overhead, b"
},
{
"path": "labs/docker-compose/03-volumes.md",
"chars": 1167,
"preview": "# Docker volumes\n\nIn some cases you're going to need data outside of docker containers and to do that you can use volume"
},
{
"path": "labs/docker-compose/04-build-image.md",
"chars": 1200,
"preview": "# Building docker image using docker-compose\n\nIn this excercise we're going to setup [bottlepy](https://bottlepy.org/doc"
},
{
"path": "labs/docker-compose/bottle/Dockerfile",
"chars": 221,
"preview": "# Dockerfile for docker-bottle web application\n\n# Add a base image to build this image off of\nFROM \nCOPY \n# Add a defaul"
},
{
"path": "labs/docker-compose/bottle/app.py",
"chars": 300,
"preview": "import sys\nfrom bottle import route, run, template\n\n@route('/')\ndef index():\n return template(\"<h1>It works!</h1><p>{"
},
{
"path": "labs/docker-compose/bottle/requirements.txt",
"chars": 16,
"preview": "bottle==0.12.20\n"
},
{
"path": "labs/exercise-template.md",
"chars": 916,
"preview": "# Template headline\n\n## Learning Goals\n\n- provide a list of goals to learn here\n\n## Introduction\n\nHere you will provide "
},
{
"path": "labs/extra-exercises/image-security.md",
"chars": 2054,
"preview": "# Docker image security\n\n\n## 1. Create a new image\n\nPull the latest alpine docker image as a base:\n\n $ docker pull al"
},
{
"path": "labs/extra-exercises/nginx/docker-compose.yml",
"chars": 467,
"preview": "services:\n one:\n image: nginx:latest\n restart: unless-stopped\n volumes:\n - \"./html1:/usr/share/nginx/html"
},
{
"path": "labs/extra-exercises/nginx/html1/index.html",
"chars": 190,
"preview": "<!DOCTYPE html>\n<html>\n<head>\n</head>\n<body style=\"background-color:red;\">\n\n<h1 style=\"color:white;\">This is a heading</"
},
{
"path": "labs/extra-exercises/nginx/html2/index.html",
"chars": 150,
"preview": "<!DOCTYPE html>\n<html>\n<head>\n</head>\n<body style=\"background-color:green;\">\n\n<h1>This is a heading</h1>\n<p>This is a pa"
},
{
"path": "labs/extra-exercises/nginx/network.md",
"chars": 1630,
"preview": "# Docker networks\n\nWhen you spin up several containers, they all share the same internal network.\n\nIn this exercise, we "
},
{
"path": "labs/extra-exercises/nginx/nginx/nginx.vh.default.conf",
"chars": 408,
"preview": "server {\n listen 80 default_server;\n listen [::]:80 default_server;\n\n index index.html index.htm;\n\n "
},
{
"path": "labs/extra-exercises/secrets.md",
"chars": 3562,
"preview": "# Docker Secrets\n\nApplications often require access to access tokens, passwords, and other sensitive information. Shipp"
},
{
"path": "labs/image-best-practices.md",
"chars": 1274,
"preview": "# Best practises\n\n## dockerignore\n\nBefore the docker CLI sends the context to the docker daemon, it looks for a file nam"
},
{
"path": "labs/multi-container/docker-compose.yaml",
"chars": 197,
"preview": "services:\n # wordpress_container:\n\n mysql-container:\n image: mysql:5.7.36\n ports:\n - 3306:3306\n environ"
},
{
"path": "labs/multi-container/docker-compose_final.yaml",
"chars": 430,
"preview": "services:\n wordpress-container:\n image: wordpress:5.7.2-apache\n ports:\n - 8080:80\n environment:\n WOR"
},
{
"path": "labs/multi-stage-build/Dockerfile",
"chars": 142,
"preview": "FROM golang:1.19 AS builder\nWORKDIR /app\nCOPY . /app\nRUN go mod download && go mod verify\nRUN cd /app && go build -o goa"
},
{
"path": "labs/multi-stage-build/go.mod",
"chars": 23,
"preview": "module example.com/mod\n"
},
{
"path": "labs/multi-stage-build/hello.go",
"chars": 75,
"preview": "package main\n\nimport \"fmt\"\n\nfunc main() {\n fmt.Println(\"Hello world!\")\n}"
},
{
"path": "labs/sharing-images.md",
"chars": 2461,
"preview": "# Sharing images\n\nBefore we can take our dockerized Flask app to another computer, we need to push it up to Docker Hub s"
},
{
"path": "labs/volumes/index.html",
"chars": 238,
"preview": "<!DOCTYPE html>\n<html>\n<head>\n<meta charset=\"UTF-8\">\n<title>Hello, world</title>\n</head>\n<body>\n<h1>Hello, world!</h1>\n<"
},
{
"path": "labs/windows-docker/win1-windows-on-linux.md",
"chars": 1419,
"preview": "# Dotnet core in Docker\n\nBefore starting with .NET in docker, it is important to know the following: \n- The ASP framewor"
},
{
"path": "labs/windows-docker/win2-windows-on-windows.md",
"chars": 4529,
"preview": "# ASP framework in containers\n\nBefore starting, it is important to be aware of something when working with containers an"
},
{
"path": "labs/windows-docker/win3-common-commands.md",
"chars": 3343,
"preview": "# The usual set of Docker commands on Windows\n\nNow, time to show that Docker on Windows really just is Docker as you kno"
},
{
"path": "trainer/README.md",
"chars": 295,
"preview": "# trainer\nThis is the how for additional examples and other trainer resources for the Praqma docker-slides and katas.\n\n#"
},
{
"path": "trainer/examples/basic-compose/Dockerfile",
"chars": 272,
"preview": "FROM python:3\n#FROM ubuntu:latest\n#RUN apt-get update && apt-get install -y \\\n# python-pip \\\n# python-dev \\\n# build-esse"
},
{
"path": "trainer/examples/basic-compose/README.md",
"chars": 499,
"preview": "# Simple single container compose example\n\n## Build image\n\n```\ndocker build -t basic-compose:latest .\n```\n\n## Run image "
},
{
"path": "trainer/examples/basic-compose/app.py",
"chars": 321,
"preview": "from flask import Flask\n\napp = Flask(__name__)\n\n@app.route('/')\ndef hello_world():\n return 'Hello! I am Jans Flask ap"
},
{
"path": "trainer/examples/basic-compose/docker-compose.yml",
"chars": 155,
"preview": "services:\n app:\n image: basic-compose:latest\n ports:\n - \"8050:5000\"\n volumes:\n - \".:/usr/src/app/\"\n "
},
{
"path": "trainer/examples/basic-compose/requirements.txt",
"chars": 13,
"preview": "flask>=1.0.0\n"
},
{
"path": "trainer/examples/building-flask-on-different-os/Dockerfile",
"chars": 285,
"preview": "FROM ubuntu:22.04\nRUN apt-get update && apt-get install -y \\\n python3-pip \\\n python3-dev \\\n build-essential\nCOPY require"
},
{
"path": "trainer/examples/building-flask-on-different-os/Dockerfile-python",
"chars": 189,
"preview": "FROM python:3\nCOPY requirements.txt /usr/src/app/\nRUN pip install --no-cache-dir -r /usr/src/app/requirements.txt\nCOPY a"
},
{
"path": "trainer/examples/building-flask-on-different-os/Dockerfile-python-alpine",
"chars": 196,
"preview": "FROM python:3-alpine\nCOPY requirements.txt /usr/src/app/\nRUN pip install --no-cache-dir -r /usr/src/app/requirements.txt"
},
{
"path": "trainer/examples/building-flask-on-different-os/README.md",
"chars": 773,
"preview": "# Building your flask application with three different from OS's\n\nBuilding our flask application with Ubuntu might seem "
},
{
"path": "trainer/examples/building-flask-on-different-os/app.py",
"chars": 376,
"preview": "import socket\nfrom flask import Flask\napp = Flask(__name__)\n\n@app.route('/')\ndef hello_world():\n return \"Hello! I am "
},
{
"path": "trainer/examples/building-flask-on-different-os/build.sh",
"chars": 159,
"preview": "# /bin/bash\ndocker build -t flask-app .\ndocker build -f Dockerfile-python -t flask-app:python .\ndocker build -f Dockerfi"
},
{
"path": "trainer/examples/building-flask-on-different-os/requirements.txt",
"chars": 13,
"preview": "Flask==2.3.2\n"
},
{
"path": "trainer/examples/ci-tools/README.md",
"chars": 668,
"preview": "\n# Docker CI tools\nNote, you need to have run the \"building flask on different OS\" example first.\n\n## Hadolint\nhttps://g"
},
{
"path": "trainer/examples/multi-stage-golang/Dockerfile",
"chars": 156,
"preview": "FROM golang:1.13.2 AS builder\r\nWORKDIR /go\r\nCOPY hello.go /go\r\nRUN go build hello.go\r\n\r\nFROM scratch\r\nCOPY --from=builde"
},
{
"path": "trainer/examples/multi-stage-golang/README.md",
"chars": 561,
"preview": "# Multistage build with go app\r\n\r\nBuilding a go application with the `golang` container,\r\nand copying it into `scratch`."
},
{
"path": "trainer/examples/multi-stage-golang/hello.go",
"chars": 82,
"preview": "package main\r\n\r\nimport \"fmt\"\r\n\r\nfunc main() {\r\n fmt.Println(\"hello world\")\r\n}\r\n"
},
{
"path": "trainer/examples/multistage-java/Dockerfile",
"chars": 286,
"preview": "FROM gradle:jdk11\nLABEL author=\"Sofus Albertsen\"\nCOPY --chown=gradle:gradle . /home/gradle/src\nWORKDIR /home/gradle/src\n"
},
{
"path": "trainer/examples/multistage-java/Dockerfile-multistage",
"chars": 358,
"preview": "FROM gradle:jdk11 AS builder\nCOPY --chown=gradle:gradle . /home/gradle/src\nWORKDIR /home/gradle/src\nRUN gradle shadowjar"
},
{
"path": "trainer/examples/multistage-java/README.md",
"chars": 777,
"preview": "# Multistage build with java app\n\nBuilding a java application with the `gradle` container,\nand copying it into `java`.\n\n"
},
{
"path": "trainer/examples/multistage-java/build.gradle",
"chars": 1903,
"preview": "plugins {\n id \"net.ltgt.apt-eclipse\" version \"0.21\"\n id \"com.github.johnrengelman.shadow\" version \"5.0.0\"\n id \""
},
{
"path": "trainer/examples/multistage-java/gradle.properties",
"chars": 22,
"preview": "micronautVersion=1.2.7"
},
{
"path": "trainer/examples/multistage-java/micronaut-cli.yml",
"chars": 96,
"preview": "profile: service\ndefaultPackage: example.micronaut\n---\ntestFramework: junit\nsourceLanguage: java"
},
{
"path": "trainer/examples/multistage-java/settings.gradle",
"chars": 22,
"preview": "rootProject.name=\"app\""
},
{
"path": "trainer/examples/multistage-java/src/main/java/example/micronaut/Application.java",
"chars": 190,
"preview": "package example.micronaut;\n\nimport io.micronaut.runtime.Micronaut;\n\npublic class Application {\n\n public static void m"
},
{
"path": "trainer/examples/multistage-java/src/main/java/example/micronaut/HelloController.java",
"chars": 557,
"preview": "package example.micronaut;\n\nimport io.micronaut.http.MediaType;\nimport io.micronaut.http.annotation.Controller;\nimport i"
},
{
"path": "trainer/examples/multistage-java/src/main/java/example/micronaut/ReadyController.java",
"chars": 375,
"preview": "package example.micronaut;\n\nimport io.micronaut.http.MediaType;\nimport io.micronaut.http.annotation.Controller;\nimport i"
},
{
"path": "trainer/examples/multistage-java/src/main/java/example/micronaut/RootController.java",
"chars": 379,
"preview": "package example.micronaut;\n\nimport io.micronaut.http.MediaType;\nimport io.micronaut.http.annotation.Controller;\nimport i"
},
{
"path": "trainer/examples/multistage-java/src/main/resources/application.yml",
"chars": 65,
"preview": "micronaut:\n server:\n port: 8000\n application:\n name: app\n"
},
{
"path": "trainer/examples/multistage-java/src/main/resources/logback.xml",
"chars": 519,
"preview": "<configuration>\n\n <appender name=\"STDOUT\" class=\"ch.qos.logback.core.ConsoleAppender\">\n <withJansi>true</withJ"
},
{
"path": "trainer/examples/multistage-java/src/test/java/example/micronaut/HelloControllerTest.java",
"chars": 1132,
"preview": "package example.micronaut;\n\nimport static org.junit.jupiter.api.Assertions.assertEquals;\nimport static org.junit.jupiter"
},
{
"path": "trainer/examples/nextcloud/README.md",
"chars": 3362,
"preview": "# Nextcloud showcase\n\nAn example of starting a two-container application using docker compose; Nextcloud with a MariaDB"
},
{
"path": "trainer/examples/nextcloud/docker-compose.yml",
"chars": 451,
"preview": "volumes:\n nextcloud:\n db:\nservices:\n app:\n image: nextcloud:23\n # image: nextcloud:24\n depends_on:\n - d"
},
{
"path": "trainer/examples/scratch/Dockerfile",
"chars": 29,
"preview": "FROM scratch\nCOPY emptyfile ."
},
{
"path": "trainer/examples/scratch/README.md",
"chars": 428,
"preview": "# Empty image (useles, but really empty)\n\nIn this example we show that Scratch is in fact empty.\n\nIt is not possible to "
},
{
"path": "trainer/examples/scratch/emptyfile",
"chars": 0,
"preview": ""
},
{
"path": "trainer/examples/security-run-as-root/README.md",
"chars": 3746,
"preview": "# Running as Root in a Container\n\nThe purpose of this example is to show,\nif you mount the filesystem into a container r"
},
{
"path": "trainer/examples/volume-python/README.md",
"chars": 333,
"preview": "The idea for this one is to make a small python application that prints \"Hello world!\" to your terminal, and volume moun"
},
{
"path": "trainer/examples/volume-python/main.py",
"chars": 451,
"preview": "import os\nimport socket\n\ndef get_system_info():\n # Get the host name\n host_name = socket.gethostname()\n \n # "
},
{
"path": "trainer/experiments/unicode/README.md",
"chars": 177,
"preview": "# Testing unicode support\n\n```\ndocker build -f Файлдокера -t unicode:latest .\ndocker run -it unicode:latest\n```\n\nSadly, "
},
{
"path": "trainer/experiments/unicode/Файлдокера",
"chars": 134,
"preview": "FROM alpine:latest\nMAINTAINER \"ℑⒶ𝕹 𝕂®𝓪ⓖ 😋\"\nCOPY ఖాళీగా .\nRUN touch فارغة\nRUN echo \"नमस्ते विश्व यो डेमो नमूना हो\"\nCMD [\""
},
{
"path": "trainer/experiments/unicode/ఖాళీగా",
"chars": 0,
"preview": ""
}
]
About this extraction
This page contains the full source code of the eficode-academy/docker-katas GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 108 files (146.3 KB), approximately 40.4k tokens, and a symbol index with 20 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.