diff options
| author | mo khan <mo.khan@gmail.com> | 2020-10-23 16:56:34 -0600 |
|---|---|---|
| committer | mo khan <mo.khan@gmail.com> | 2020-10-23 16:56:34 -0600 |
| commit | 214586544d4e51a26bed4fa14ea74393e885099b (patch) | |
| tree | c0f4b9cf521b40e51f53f55b06452b06c82973a2 | |
| parent | 143c894c905ec365c7d6118eb66aa2ab7b948ea9 (diff) | |
Rewrite presentation to tell a story
| -rw-r--r-- | Makefile | 2 | ||||
| -rw-r--r-- | README.md | 641 | ||||
| -rw-r--r-- | examples/001/Dockerfile | 1 | ||||
| -rw-r--r-- | examples/001/docker-exec.sh | 2 | ||||
| -rw-r--r-- | examples/001/hello.rb | 2 |
5 files changed, 474 insertions, 174 deletions
@@ -1,3 +1,3 @@ run: pipenv install - pipenv run lookatme --theme=dark --style=vim --live README.md + pipenv run lookatme --theme=light --live README.md @@ -1,26 +1,138 @@ --- title: Developing with Docker author: gitlab.com/xlgmokha/developing-with-docker -date: 2020-06-10 +date: 2020-10-24 --- -# Developing with Docker - Mo Khan | Software Engineer | Composition Analysis | GitLab +# Developing with Docker +Mo Khan | Software Developer | GitLab ```text - How - to - Docker - better? - ## . - ## ## ## == - ## ## ## ## ## === - /"""""""""""""""""\___/ === - { / ===- - \______ O __/ - \ \ __/ - \____\_______/ + Building + smaller + Docker + images? + ## . + ## ## ## == + ## ## ## ## ## === + /"""""""""""""""""\___/ === + { / ===- + \______ O __/ + \ \ __/ + \____\_______/ ``` +# whoami + +Software developer from Calgary, AB, Canada. + +* GitLab +* Cisco +* ThoughtWorks + +# Why? + +License scanning at GitLab. + +1. Scan target project for lock files (Gemfile.lock, Pipfile.lock etc) +2. Install project tools (Ruby 2.7.2, Python 3.8.4) +3. Install project dependencies (Rails, Django) + +```bash + モ ls ~/development/gitlab | grep lock + Gemfile.lock + Pipfile.lock + yarn.lock +``` + +Ship a Docker image: + +* Multiple versions of Ruby, Python etc +* Package managers for different languages +* Omnibus package of scanner code +* System packages/dependencies (libpq-dev, libsqlite3-dev etc) + +Multiple Languages/Versions: + +* Dotnet Core +* Golang +* Java +* Mono +* NodeJS +* PHP +* Python +* Ruby +* Rust + +Multiple Package Managers: + +* Bundler +* pip +* pipenv +* gradle +* maven + +Large Docker images + +* slow downloads +* more disk space is required +* more bandwidth is consumed + +# Overview + +```text + ------------- + | git | + ------------- ---------------- + | main | --> | gitlab-runner | + | feature-a | ---------------- + | feature-b | | + ------------- | (launch container) + V + --------------- + | | <----- 10GB ------ + | Docker Host | --------- | + | | | | + --------------- | | + | download | + V | | + -------------- V | + | License | ------------ | + | scanner | | registry | -| + -------------- ------------ +``` + +License scanner + +1. Search for lockfiles +2. Install desired version of language tools +3. Install packages via package manager +4. Scan for licenses +5. Export JSON report + +# Zoom in + +How did you shrink the image from 10GB down to 1GB? + +```text + --------------- + | | <----- 10GB ------ + | Docker Host | --------- | + | | | | + --------------- | | + | download | + V | | + -------------- V | + | License | ------------ | + | scanner | | registry | -| + -------------- ------------ +``` + +Also added + +* Support for more languages and versions. +* Support for limited connectivity environments. + # Agenda * Definitions @@ -30,19 +142,19 @@ date: 2020-06-10 * Optimize ```text -< What are we going to talk about? > - ---------------------------------- - \ - \ - \ - ## . - ## ## ## == - ## ## ## ## === - /""""""""""""""""___/ === - ~~~ {~~ ~~~~ ~~~ ~~~~ ~~ ~ / ===- ~~~ - \______ o __/ - \ \ __/ - \____\______/ + < What are we going to talk about? > + ---------------------------------- + \ + \ + \ + ## . + ## ## ## == + ## ## ## ## === + /""""""""""""""""___/ === + ~~~ {~~ ~~~~ ~~~ ~~~~ ~~ ~ / ===- ~~~ + \______ o __/ + \ \ __/ + \____\______/ ``` # Definitions @@ -58,7 +170,14 @@ A class defines the behaviour and data associated with the class. ```ruby class Person - def fist_bump(other_person) + attr_reader :name + + def initialize(name) + @name = name + end + + def hug(other) + puts "#{name} 🤗 #{other.name}" end end ``` @@ -69,8 +188,8 @@ You can't do much with a class until you create an instance of one. ```ruby -mo = Person.new -you = Person.new + mo = Person.new("mo") + me = Person.new("me") ``` # Definitions - Objects @@ -80,7 +199,7 @@ methods on the object. An object can interact with other objects. ```ruby -mo.first_bump(you) + mo.hugs(me) ``` # Definitions - Container @@ -93,28 +212,6 @@ Similar to how an object is an instance of a class. | Class | Image | | Object | Container | -# Identifiers - -Classes can be identified by their name. -Images can be identified by their image Id or `name:tag` - -Objects can be identified by their `object_id` in Ruby. -Containers can be identified by their container Id or a name. - -| Ruby | Docker | -| -- | -- | -| Person | Image ID | -| mo.object_id | Container ID | - -# Image identifier - -`[registry]name:tag` - -If the registry is omitted, then docker.io is assumed. - -* registry.gitlab.com/gitlab-org/security-products/license-management:latest -* alpine:latest - # Definitions - Registry Registry: stores images and makes them available to others @@ -160,46 +257,16 @@ curl -s -i https://registry-1.docker.io/v2/alpine/tags/list https://docs.docker.com/get-started/overview/#docker-architecture -# /var/run/docker.sock - -```bash -$ curl --unix-socket /var/run/docker.sock http://localhost/images/json -``` - -```terminal32 -curl -i --unix-socket /var/run/docker.sock http://localhost/images/json -``` - -# $ docker version - -The docker CLI is an HTTP client that can connect to Unix or TCP sockets. - -```terminal32 -docker version -``` - -# $ docker image ls - -```terminal32 -docker image ls --format "table {{.ID}}\t{{.Repository}}\t{{.Tag}}\t{{.Size}}" -``` - -# $ docker ps +# $ docker run -it alpine:latest cat /etc/os-release -```terminal32 -docker ps --format "table {{.ID}}\t{{.Image}}\t{{.Status}}" +```terminal8 +docker run -it alpine:latest cat /etc/os-release ``` -# $ docker run -it alpine:latest cat /etc/os-release - 1. check if "alpine:latest" is on docker host 1. download "alpine:latest" from registry to docker host 1. start a container using the "alpine:latest" image -```terminal32 -docker run -it alpine:latest cat /etc/os-release -``` - # Dockerfile ```file @@ -222,14 +289,7 @@ lang: docker # COPY "hello.rb" -Copy "hello.rb" from the host to -"/usr/local/bin/hello" within the Docker image. - -```file -path: examples/001/Dockerfile -relative: true -lang: docker -``` +Copy "hello.rb" from the host to "/usr/local/bin/hello" in the Docker image. ```bash $ cat examples/001/hello.rb @@ -270,7 +330,7 @@ lines: # docker build -t developing-with-docker:latest examples/001/ ```terminal32 -time docker build -t developing-with-docker:latest examples/001/ +docker build --network=host -t developing-with-docker:latest examples/001/ ``` # docker run developing-with-docker:latest @@ -279,24 +339,6 @@ time docker build -t developing-with-docker:latest examples/001/ docker run developing-with-docker:latest ``` -# docker run -it developing-with-docker:latest /bin/sh - -```terminal32 -docker run -it developing-with-docker:latest /bin/sh -``` - -# docker ps - -```terminal32 -docker ps --format "table {{.ID}}\t{{.Image}}\t{{.Status}}" -``` - -# docker exec -it <imageid> /bin/sh - -```terminal32 -bash -i examples/001/docker-exec.sh -``` - # dive Useful for identifying bloat. @@ -305,103 +347,360 @@ https://github.com/wagoodman/dive * Displays each layer * Allows investigating files that are added/removed/changed in each layer -```terminal32 -bash examples/001/dive-exec.sh developing-with-docker -``` +# dive original -# docker pull registry.gitlab.com/gitlab-org/security-products/license-management:latest +```bash +│ Layers ├──────────────────────────────────────────────────────────────────────────── ┃ ● Current Layer Contents ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +Cmp Size Command Permission UID:GID Size Filetree + 5.6 MB FROM 5a96ef02e9cab83 drwxr-xr-x 0:0 841 kB ├─⊕ bin + 40 B #(nop) COPY file:253d38e67af26b201caf4e271248576ba6a7da0431f5ddee20c5c6df5 drwxr-xr-x 0:0 0 B ├── dev + 16 MB apk add ruby drwxr-xr-x 0:0 383 kB ├─⊕ etc + 40 B chmod +x /usr/local/bin/hello drwxr-xr-x 0:0 0 B ├── home + drwxr-xr-x 0:0 3.9 MB ├─⊕ lib +│ Layer Details ├───────────────────────────────────────────────────────────────────── drwxr-xr-x 0:0 0 B ├─⊕ media + drwxr-xr-x 0:0 0 B ├── mnt +Tags: (unavailable) drwxr-xr-x 0:0 0 B ├── opt +Id: e19b1e8ac9df6c9165f82c5e278f4d8b4839c8e6727001a12477e3947ffd83ea dr-xr-xr-x 0:0 0 B ├── proc +Digest: sha256:aebe0431f34491da1f2785934e4190cfb9fbba01fc1aeff69f13c139a6ccac65 drwx------ 0:0 0 B ├── root +Command: drwxr-xr-x 0:0 0 B ├── run +apk add ruby drwxr-xr-x 0:0 226 kB ├─⊕ sbin + drwxr-xr-x 0:0 0 B ├── srv +│ Image Details ├───────────────────────────────────────────────────────────────────── drwxr-xr-x 0:0 0 B ├── sys + drwxrwxrwx 0:0 0 B ├── tmp + drwxr-xr-x 0:0 14 MB ├─⊕ usr +Total Image size: 21 MB drwxr-xr-x 0:0 1.8 MB └─⊕ var +Potential wasted space: 532 kB +Image efficiency score: 98 % + +Count Total Space Path + 2 428 kB /etc/ssl/certs/ca-certificates.crt + 2 79 kB /lib/apk/db/installed + 2 25 kB /lib/apk/db/scripts.tar + 2 288 B /lib/apk/db/triggers + 2 123 B /etc/apk/world + 2 80 B /usr/local/bin/hello + 2 0 B /lib/apk/db/lock + 2 0 B /var/cache/misc +``` + +# dive - layers -* More layers == more parallel downloads -* Smaller layers == faster downloads per layer +```bash +│ Layers ├───────────────────────────────────────────────────────── +Cmp Size Command + 5.6 MB FROM 5a96ef02e9cab83 + 40 B #(nop) COPY file:253d38e67af26b201caf4e271248576ba6a7da + 16 MB apk add ruby + 40 B chmod +x /usr/local/bin/hello -```terminal32 -bash -i examples/001/docker-large-download.sh -``` +│ Layer Details ├────────────────────────────────────────────────── -# docker build -t big-image:latest examples/002/ +Tags: (unavailable) +Id: e19b1e8ac9df6c9165f82c5e278f4d8b4839c8e6727001a12477e3947ff +Digest: sha256:aebe0431f34491da1f2785934e4190cfb9fbba01fc1aeff69f13 +Command: +apk add ruby -```file -path: examples/002/Dockerfile -relative: true -lang: docker -``` +│ Image Details ├────────────────────────────────────────────────── -```terminal32 -time docker build -t big-image:latest examples/002/ -``` -# docker image ls +Total Image size: 21 MB +Potential wasted space: 532 kB +Image efficiency score: 98 % -```terminal32 -docker image ls --format "table {{.ID}}\t{{.Repository}}\t{{.Tag}}\t{{.Size}}" +Count Total Space Path + 2 428 kB /etc/ssl/certs/ca-certificates.crt + 2 79 kB /lib/apk/db/installed + 2 25 kB /lib/apk/db/scripts.tar + 2 288 B /lib/apk/db/triggers + 2 123 B /etc/apk/world + 2 80 B /usr/local/bin/hello + 2 0 B /lib/apk/db/lock + 2 0 B /var/cache/misc ``` -# dive big-image:latest +# dive - layer details -```terminal32 -bash examples/001/dive-exec.sh big-image +```bash +┃ ● Current Layer Contents ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +Permission UID:GID Size Filetree +drwxr-xr-x 0:0 841 kB ├─⊕ bin +drwxr-xr-x 0:0 0 B ├── dev +drwxr-xr-x 0:0 383 kB ├─⊕ etc +drwxr-xr-x 0:0 0 B ├── home +drwxr-xr-x 0:0 3.9 MB ├─⊕ lib +drwxr-xr-x 0:0 0 B ├─⊕ media +drwxr-xr-x 0:0 0 B ├── mnt +drwxr-xr-x 0:0 0 B ├── opt +dr-xr-xr-x 0:0 0 B ├── proc +drwx------ 0:0 0 B ├── root +drwxr-xr-x 0:0 0 B ├── run +drwxr-xr-x 0:0 226 kB ├─⊕ sbin +drwxr-xr-x 0:0 0 B ├── srv +drwxr-xr-x 0:0 0 B ├── sys +drwxrwxrwx 0:0 0 B ├── tmp +drwxr-xr-x 0:0 14 MB ├─⊕ usr +drwxr-xr-x 0:0 1.8 MB └─⊕ var +``` + +# docker pull - back then + +```bash +2.8.0: Pulling from gitlab-org/security-products/license-management +0a01a72a686c: Pull complete +cc899a5544da: Pull complete +19197c550755: Pull complete +716d454e56b6: Pull complete +4a00dd5b28fc: Pull complete +e039d25729bf: Pull complete +930b12354a74: Pull complete +5c0b45be82b1: Pull complete +2174b0b17785: Pull complete +79e1fdae2dfc: Extracting [================> ] 322MB/396.1MB +2c46852653ff: Download complete +40e535af8764: Download complete +0c1954047133: Download complete +080be37ae17e: Download complete +4179fc4ef96a: Download complete +87d7bc66884f: Download complete +581b5a43e64d: Download complete +a754f2766934: Download complete +c1e41c3670fe: Download complete +e782eb3070d9: Download complete +50d1fb4b55b2: Download complete +276696690bcf: Download complete +5eec42d5363b: Download complete +2296aa2193e9: Download complete +5fe4c102c0bc: Download complete +97390612da81: Downloading [=====> ] 81.05MB/174MB +311b1e270e29: Downloading [===========> ] 42.12MB/189.4MB +53dfbd975f60: Downloading [=> ] 25.36MB/843.4MB +3dd2acdebe0f: Waiting +d548f098494f: Waiting +da1cc42017ff: Waiting +cfc3cd025ca9: Waiting +69ea647e6c07: Waiting +1e27d5f85aa2: Waiting +94cf5e06627d: Waiting +30e1f788589d: Waiting +d9238ec317d1: Waiting +e17797fa5e82: Waiting +9003b36c1e4e: Waiting +``` + +# Start with a minimal base image + +```Dockerfile +FROM licensefinder/license_finder:5.6.2 ``` -# docker build -t small-image:latest examples/003/ +```bash +REPOSITORY TAG SIZE +debian stable-slim 69.2MB +licensefinder/license_finder 5.6.2 3.63GB +``` + +```Dockerfile +FROM debian:stable-slim +``` + +# Be picky + +*Warning: The content below may be considered offensive.* + +```Dockerfile +RUN apt-get update && apt-get install -y \ + build-essential \ + curl \ + git-core \ + sudo \ + unzip \ + wget +RUN curl -sL https://deb.nodesource.com/setup_8.x | bash - && \ + apt-get -y install nodejs +RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add - && \ + echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list && \ + apt-get update && \ + apt-get install yarn +RUN apt-get install -y python rebar +RUN apt-get install -y python-pip && \ + pip install --upgrade pip==$PIP_INSTALL_VERSION +RUN apt-get install -y locales +RUN wget https://packages.erlang-solutions.com/erlang-solutions_${MIX_VERSION}_all.deb && \ + sudo dpkg -i erlang-solutions_${MIX_VERSION}_all.deb && \ + sudo rm -f erlang-solutions_${MIX_VERSION}_all.deb && \ + sudo apt-get update && \ + sudo apt-get install -y esl-erlang && \ + sudo apt-get install -y elixir +RUN apt-get install -y python-dev && \ + pip install --ignore-installed six --ignore-installed colorama --ignore-installed requests --ignore-installed chardet --ignore-installed urllib3 --upgrade setuptools && \ + pip install conan +RUN apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 3FA7E0328081BFF6A14DA29AA6A19B38D3D831EF &&\ + echo "deb https://download.mono-project.com/repo/ubuntu stable-xenial main" | sudo tee /etc/apt/sources.list.d/mono-official-stable.list &&\ + apt-get update &&\ + apt-get install -y mono-complete &&\ + curl -o /usr/local/bin/nuget.exe https://dist.nuget.org/win-x86-commandline/latest/nuget.exe &&\ + echo "alias nuget=\"mono /usr/local/bin/nuget.exe\"" >> ~/.bash_aliases +RUN wget -q https://packages.microsoft.com/config/ubuntu/16.04/packages-microsoft-prod.deb &&\ + sudo dpkg -i packages-microsoft-prod.deb &&\ + sudo apt-get update &&\ + sudo apt-get install -y dotnet-runtime-2.1 +``` + +# Group 'apt-get install' together + +```Dockerfile +COPY config/install.sh /opt/install.sh +RUN bash /opt/install.sh +``` -* Collapse layers -* Cleanup unnecessary artifacts -* Deflate files within layers -* Inflate files when container is launched +```bash +#!/bin/bash +apt-get install -y --no-install-recommends \ + apt-transport-https \ + autoconf \ + automake \ + bsdmainutils \ + bzip2 \ + ca-certificates \ + cmake \ + curl \ + gnupg2 \ + make \ + pkg-config \ + re2c \ + rebar \ + zstd +``` + +*note: avoid installing build tools if you can* + +# Deflate + +Compress directories that are large during build time. -```file -path: examples/003/Dockerfile -relative: true -lang: docker -``` +```bash +#!/bin/bash -```file -path: examples/003/run.sh -relative: true -lang: sh -``` +function deflate() { + local file=$1 + local dir=$2 + local zstd_command="/usr/bin/zstd -19 -T0" + tar --use-compress-program "$zstd_command" -cf "$file" "$dir" +} -```terminal32 -time docker build -t small-image:latest examples/003/ +cd /opt +deflate /opt/asdf.tar.zst asdf + +cd /usr/lib +deflate /usr/lib/gcc.tar.zst gcc +deflate /usr/lib/mono.tar.zst mono +deflate /usr/lib/rustlib.tar.zst rustlib + +cd /usr/share +deflate /usr/share/dotnet.tar.zst dotnet ``` -# docker image ls +# Inflate -```terminal32 -docker image ls --format "table {{.ID}}\t{{.Repository}}\t{{.Tag}}\t{{.Size}}" +Decompress directories when the container is launched +by hooking into the ENTRYPOINT. + +```Dockerfile +ENTRYPOINT ["/run.sh"] ``` -# dive small-image:latest +```bash +#!/bin/bash + +function inflate() { + local file=$1 + local to_dir=$2 + if [ -f "$file" ]; then + tar --use-compress-program zstd -xf "$file" -C "$to_dir" + rm "$file" + fi +} -```terminal32 -bash examples/001/dive-exec.sh small-image +inflate /opt/asdf.tar.zst /opt +inflate /usr/lib/gcc.tar.zst /usr/lib +inflate /usr/lib/mono.tar.zst /usr/lib +inflate /usr/lib/rustlib.tar.zst /usr/lib +inflate /usr/share/dotnet.tar.zst /usr/share + +sh "$@" ``` -# docker run -it small-image:latest +# Build specialized packages -```terminal32 -docker run -it small-image:latest +```ruby +build do + env = with_standard_compiler_flags(with_embedded_path) + configure_command = [ + "--disable-debug-env", + "--disable-dtrace", + "--disable-install-capi", + "--disable-install-doc", + "--disable-install-rdoc", + "--disable-jit-support", + "--enable-shared", + "--prefix=#{install_dir}", + "--with-out-ext=coverage,dbm,readline,rdoc,win32,win32ole,sdbm", + "--without-gdbm", + "--without-gmp", + "--without-jemalloc", + "--without-tk", + "--without-valgrind" + ] + configure(*configure_command, env: env) + make "-j #{workers}", env: env + make "-j #{workers} install", env: env +end ``` -# docker image ls +```bash +# ls -lh /opt/toolcache/ruby-* | awk '{ print $5 " " $9 }' +5.3M /opt/toolcache/ruby-2.4.10_2.4.10-1_amd64.deb +5.3M /opt/toolcache/ruby-2.4.5_2.4.5-1_amd64.deb +5.3M /opt/toolcache/ruby-2.4.9_2.4.9-1_amd64.deb +5.4M /opt/toolcache/ruby-2.5.8_2.5.8-1_amd64.deb +5.6M /opt/toolcache/ruby-2.6.0_2.6.0-1_amd64.deb +5.6M /opt/toolcache/ruby-2.6.1_2.6.1-1_amd64.deb +5.6M /opt/toolcache/ruby-2.6.2_2.6.2-1_amd64.deb +5.6M /opt/toolcache/ruby-2.6.3_2.6.3-1_amd64.deb +5.6M /opt/toolcache/ruby-2.6.4_2.6.4-1_amd64.deb +5.6M /opt/toolcache/ruby-2.6.5_2.6.5-1_amd64.deb +5.6M /opt/toolcache/ruby-2.6.6_2.6.6-1_amd64.deb +5.7M /opt/toolcache/ruby-2.7.0_2.7.0-1_amd64.deb +5.7M /opt/toolcache/ruby-2.7.1_2.7.1-1_amd64.deb +5.7M /opt/toolcache/ruby-2.7.2_2.7.2-1_amd64.deb +``` + +# Results -```terminal32 -docker image ls --format "table {{.ID}}\t{{.Repository}}\t{{.Tag}}\t{{.Size}}" +```bash +REPOSITORY TAG SIZE +analyzers/license-finder 3.28.1 1.4GB +analyzers/license-finder 2.8.0 9.83GB +analyzers/license-finder 1.5.0 4.06GB ``` # Summary * Keep each layer small * More layers provides opportunity for more parallel downloads +* Download will block on the largest layer. +* Too many layers can cause too many parallel downloads +* Try to collapse layers by grouping logical things together +* Cleanup unnecessary artifacts in each layer +* Deflate files when building layers +* Inflate files when container is launched -# Fin - -Thank you for your time +Thank you [gitlab.com/xlgmokha/developing-with-docker](https://gitlab.com/xlgmokha/developing-with-docker) - -## Feedback +# Feedback * people will know what Docker is * watch the screen from different sizes diff --git a/examples/001/Dockerfile b/examples/001/Dockerfile index f5f827d..3657293 100644 --- a/examples/001/Dockerfile +++ b/examples/001/Dockerfile @@ -1,4 +1,5 @@ FROM alpine:latest COPY hello.rb /usr/local/bin/hello +RUN apk add ruby RUN chmod +x /usr/local/bin/hello CMD ["hello"] diff --git a/examples/001/docker-exec.sh b/examples/001/docker-exec.sh index 39478b2..25c1085 100644 --- a/examples/001/docker-exec.sh +++ b/examples/001/docker-exec.sh @@ -3,4 +3,4 @@ set -e image_id="$(docker ps | grep developing | awk '{ print $1 }' | tail -n1)" -docker exec -it "$image_id" /bin/sh +docker exec -it --network=host "$image_id" /bin/sh diff --git a/examples/001/hello.rb b/examples/001/hello.rb index 0a6e058..e772410 100644 --- a/examples/001/hello.rb +++ b/examples/001/hello.rb @@ -1,3 +1,3 @@ #!/usr/bin/env ruby -puts "Hello World" +puts "Hello, World!" |
