Streamline your dev env with Docker 
Giacomo Bagnoli, #RUBYDAY2014
WHOAMI 
Giacomo Bagnoli 
Currently Backend Engineer at Gild 
Previously at Amazon and Asidev 
Twitter: @gbagnoli 
Github: gbagnoli 
about.me/gbagnoli
WHAT'S WRONG WITH MY DEV ENVIRONMENT? 
Nothing in particular. It's not* broken, so let's fix it. 
* conditions apply
WHAT'S WRONG WITH MY DEV ENVIRONMENT? 
Your development 
environment is probably 
a beautiful, unique 
snowflake 
Photo credits: https://www.flickr.com/photos/amagill/4223790595/
WHAT'S DOCKER, ANYWAY?!? 
Open source platform 
Docker Engine 
Container management runtime 
Packaging tools for images 
Docker Hub
CONTAINERS, UH? 
Operating system-level virtualization* 
Runs multiple isolated linux systems 
On a single host, with a single kernel 
No /sbin/init, no device emulation 
Think them as chroot on steroids 
Isolation provided by linux cgroups and namespaces 
Resource limiting and prioritization via cgroups 
Resource usage accounting via cgroups 
* not a virtualization method, containers != VM
DOCKER IMAGES (0) 
Portable, read-only layers. 
Images are composed at run-time to 
form the container root FS using an 
union filesystem. 
Processes tricked to see the filesystem 
as R/W. 
The writable layer is discarded if the 
container is not committed. 
The read-only and portable properties 
are important as they enable sharing 
(via the docker hub).
DOCKER IMAGES (1) 
Docker images form a Direct Acyclic Graph. 
Each layer is cached (possibly) and reused by other images. 
This means that if multiple images derive from debian:wheezy, 
that particular image is shared by all of them (thus downloaded 
once). 
Images are pushed/pull to/from the docker hub.
DOCKER IMAGES (2) 
$ docker pull ubuntu:14.04 
$ docker pull ubuntu:12.04 
$ docker pull redis:2.8.13 
$ docker pull debian:wheezy 
$ docker pull mongo:2.6.4 
$ docker images 
REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE 
redis 2.8.13 dd52dc9c8f76 9 minutes ago 98.44 MB 
mongo 2.6.4 dd1f260c0731 12 minutes ago 391.2 MB 
debian wheezy 9cdcc6025135 18 hours ago 85.19 MB 
ubuntu 14.04 96864a7d2df3 2 days ago 204.4 MB 
ubuntu 12.04 ec966722cde4 2 days ago 103.8 MB
DOCKER IMAGES (3) 
$ docker images ‐‐tree 
└─511136ea3c5a Virtual Size: 0 B 
└─b37448882294 Virtual Size: 85.19 MB 
└─9cdcc6025135 Virtual Size: 85.19 MB Tags: debian:wheezy 
├─e365f7cdb352 Virtual Size: 85.52 MB 
│ └─b15940870e43 Virtual Size: 85.52 MB 
│ └─22ad4fc6b16f Virtual Size: 98.44 MB 
│ └─bd1e22dd175d Virtual Size: 98.44 MB 
│ └─3b1ce200fdad Virtual Size: 98.44 MB 
│ └─dd52dc9c8f76 Virtual Size: 98.44 MB Tags: redis:2.8.13 
└─49fd1ae472a8 Virtual Size: 85.52 MB 
└─6c203838fd07 Virtual Size: 99.62 MB 
└─b1cd74f30329 Virtual Size: 391.2 MB 
└─9d0a3438646f Virtual Size: 391.2 MB 
└─dd1f260c0731 Virtual Size: 391.2 MB Tags: mongo:2.6.4
LET'S BUILD A RUBY2 IMAGE 
Ubuntu 14.04 image has no ruby at all. Repos have ruby 1.9. 
Let's create an image with 2.1 as default. 
Dockerfile: 
FROM ubuntu:14.04 
MAINTAINER Giacomo Bagnoli <gbagnoli@gmail.com> 
RUN echo "deb http://ppa.launchpad.net/brightbox/ruby‐ng/ubuntu trusty main" >  
/etc/apt/sources.list.d/ruby‐ng.list 
RUN apt‐key adv ‐‐keyserver hkp://keyserver.ubuntu.com:80 ‐‐recv‐keys C3173AA6 
RUN apt‐get update 
RUN apt‐get install ‐y ruby2.1 
Build! 
$ docker build ‐‐rm ‐t rubyday/ruby:2.1 .
RUBY2 IMAGE 
Each directive in the Dockerfile adds a layer 
$ docker images ‐‐tree 
└─96864a7d2df3 Virtual Size: 204.4 MB Tags: ubuntu:14.04 
└─8f1b6341c5be Virtual Size: 204.4 MB # MAINTAINER 
└─d323cc59da91 Virtual Size: 204.4 MB # RUN 
└─724a6664d97a Virtual Size: 204.4 MB # RUN 
└─8614dab05fbe Virtual Size: 224.8 MB # RUN 
└─d7ae4a198781 Virtual Size: 257.2 MB Tags: rubyday/ruby:2.1 # RUN 
$ docker run ‐t rubyday/ruby:2.1 ruby ‐v 
ruby 2.1.2p95 (2014‐05‐08 revision 45877) [x86_64‐linux‐gnu] 
Woah, 53Mb. apt‐get update adds 20Mb to the image.
I TRIED... 
Let's remove apt-get files by adding another RUN statement 
diff ‐‐git a/ruby2/Dockerfile b/ruby2/Dockerfile 
index dd37dcb..2b9c105 100644 
‐‐‐ a/ruby2/Dockerfile 
+++ b/ruby2/Dockerfile 
@@ ‐8,3 +8,4 @@ 
RUN echo "deb http://ppa.launchpad.net/brightbox/ruby‐ng/ubuntu trusty main" > / 
RUN apt‐key adv ‐‐keyserver hkp://keyserver.ubuntu.com:80 ‐‐recv‐keys C3173AA6 
RUN apt‐get update 
RUN apt‐get install ‐y ruby2.1 
+RUN rm ‐rf /var/lib/apt/lists/* /var/cache/apt/archives/*.deb
... GRUMPY CAT SAYS
LAYERS.. 
$ docker images ‐‐tree 
└─96864a7d2df3 Virtual Size: 204.4 MB Tags: ubuntu:14.04 
└─8f1b6341c5be Virtual Size: 204.4 MB # MAINTAINER 
└─d323cc59da91 Virtual Size: 204.4 MB # RUN 
└─724a6664d97a Virtual Size: 204.4 MB # RUN 
└─8614dab05fbe Virtual Size: 224.8 MB # RUN 
└─d7ae4a198781 Virtual Size: 257.2 MB # RUN 
└─b8bb3ce3008e Virtual Size: 257.2 MB Tags: rubyday/ruby:2.1 # RUN 
Remember that every directive adds a layer. Layers are read only.
LET'S TRY ONCE MORE 
Let's rewrite the Dockerfile 
FROM ubuntu:14.04 
MAINTAINER Giacomo Bagnoli <gbagnoli@gmail.com> 
RUN  
echo "deb http://ppa.launchpad.net/brightbox/ruby‐ng/ubuntu trusty main"  
> /etc/apt/sources.list.d/ruby‐ng.list &&  
apt‐key adv ‐‐keyserver hkp://keyserver.ubuntu.com:80 ‐‐recv‐keys C3173AA6 &&  
apt‐get update &&  
apt‐get install ‐y ruby2.1 &&  
rm ‐rf /var/lib/apt/lists/* /var/cache/apt/archives/*.deb
REBUILD 
Build: 
$ docker build ‐‐rm ‐t rubyday/ruby:2.1 . 
$ docker images 
REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE 
rubyday/ruby 2.1 b337a5c538f3 About a minute ago 236.9 MB 
└─96864a7d2df3 Virtual Size: 204.4 MB Tags: ubuntu:14.04 
└─86ae939e2da3 Virtual Size: 204.4 MB # MAINTAINER 
└─b337a5c538f3 Virtual Size: 236.9 MB Tags: gild/ruby:2.1 # RUN 
yay!
PAT ME ON THE BACK
... ONE MORE THING 
We probably want ‐dev packages and bundle 
Let's update the Dockerfile 
FROM ubuntu:14.04 
MAINTAINER Giacomo Bagnoli 
RUN  
echo "deb http://ppa.launchpad.net/brightbox/ruby‐ng/ubuntu trusty main"  
> /etc/apt/sources.list.d/ruby‐ng.list 
RUN apt‐key adv ‐‐keyserver hkp://keyserver.ubuntu.com:80 ‐‐recv‐keys C3173AA6 
RUN apt‐get update &&  
apt‐get install ‐y build‐essential &&  
apt‐get install ‐y ruby2.1 ruby2.1‐dev &&  
update‐alternatives ‐‐set ruby /usr/bin/ruby2.1 &&  
update‐alternatives ‐‐set gem /usr/bin/gem2.1 &&  
rm ‐rf /var/lib/apt/lists/* /var/cache/apt/archives/*.deb 
RUN gem install bundle
TIME FOR RAILS 
Let's try creating a Dockerfile for a rails app. 
The app is a random simple TODO list 
application found on Github. 
It's a rails4 application that uses SQL, nothing 
fancy. 
Let's assume we are developing this app 
targeting postgresql. 
Github url: 
https://github.com/gbagnoli/todo‐rails4‐angularjs
A WORD ON LINKING 
In docker, we can access service(s) running in other container(s) via linking. 
Linking a container to another will setup some environment variables in it, allowing the container to 
discover and connect to the service. 
We will use this feature to access postgres from our app container.
THE DOCKERFILE 
FROM rubyday/ruby:2.1 
MAINTAINER Giacomo Bagnoli <gbagnoli@gmail.com> 
RUN adduser todo ‐‐home /opt/todo ‐‐shell /bin/bash ‐‐disabled‐password ‐‐gecos "" 
RUN apt‐get update &&  
apt‐get install ‐y libpq‐dev nodejs &&  
rm ‐rf /var/lib/apt/lists/* /var/cache/apt/archives/*.deb 
ADD Gemfile /opt/todo/ 
ADD Gemfile.lock /opt/todo/ 
RUN chown ‐R todo:todo /opt/todo 
RUN su ‐c "D=/opt/todo/bundle; mkdir $D && bundle install ‐‐deployment ‐‐path $D" 
‐s /bin/bash ‐l todo 
WORKDIR /opt/todo 
EXPOSE 3000 
ADD . /opt/todo 
RUN chown ‐R todo:todo /opt/todo 
USER todo 
ENTRYPOINT ["/bin/bash", "/opt/todo/bin/docker_entrypoint.sh"] 
CMD ["bundle", "exec", "rails", "server"]
DOCKERFILE EXPLAINED (0) 
FROM rubyday/ruby:2.1 
MAINTAINER Giacomo Bagnoli <gbagnoli@gmail.com>
DOCKERFILE EXPLAINED (1) 
RUN adduser todo ‐‐home /opt/todo ‐‐shell /bin/bash ‐‐disabled‐password ‐‐gecos "" 
RUN apt‐get update &&  
apt‐get install ‐y libpq‐dev nodejs &&  
rm ‐rf /var/lib/apt/lists/* /var/cache/apt/archives/*.deb
DOCKERFILE EXPLAINED (2) 
ADD Gemfile /opt/todo/ 
ADD Gemfile.lock /opt/todo/ 
RUN chown ‐R todo:todo /opt/todo 
RUN su ‐c "D=/opt/todo/bundle; mkdir $D && bundle install ‐‐deployment ‐‐path $D" 
‐s /bin/bash ‐l todo
DOCKERFILE EXPLAINED (3) 
WORKDIR /opt/todo 
EXPOSE 3000 
ADD . /opt/todo 
RUN chown ‐R todo:todo /opt/todo
DOCKERFILE EXPLAINED (4) 
USER todo 
ENTRYPOINT ["/bin/bash", "/opt/todo/bin/docker_entrypoint.sh"] 
CMD ["bundle", "exec", "rails", "server"]
POSTGRES CONTAINER (0) 
$ docker pull postgres:9.3 
$ docker run ‐d ‐‐name postgres ‐t postgres:9.3 
a5723351c46ce015d585dd49f230ecb376557d0b955f233dbff3bf92f3a6721d 
$ docker ps 
CONTAINER ID IMAGE [...] PORTS NAMES 
a5723351c46c postgres:9 [...] 5432/tcp postgres 
This container EXPOSEs port 5432. 
Question is, how do we connect to it?
We can't just hardcode its ip address, as it defeats the purpose... 
$ docker inspect postgres | grep NetworkSettings ‐A 9 
"NetworkSettings": { 
"Bridge": "docker0", 
"Gateway": "172.17.42.1", 
"IPAddress": "172.17.0.4", 
"IPPrefixLen": 16, 
"PortMapping": null, 
"Ports": { 
"5432/tcp": null 
} 
}, 
POSTGRES CONTAINER (1)
THE WRAPPER SCRIPT (0) 
In the Dockerfile, an ENTRYPOINT was specified. 
#!/bin/bash 
# exit with error if a variable is unbound (not set) 
set ‐u 
# exit with error if a command returns a non‐zero status 
set ‐e 
PGADDR=$DB_PORT_5432_TCP_ADDR 
PGPORT=$DB_PORT_5432_TCP_PORT 
PGDBNAME="${DATABASE_NAME:‐todo}" 
PGUSER="${DATABASE_USER:‐postgres}" 
# export database configuration for rails. 
export DATABASE_URL="postgresql://${PGUSER}@${PGADDR}:${PGPORT}/${PGDBNAME}" 
# exec what the user wants 
exec "$@"
THE WRAPPER SCRIPT (1) 
Trying to execute the container will throw an error (it's a feature!) 
$ docker run ‐‐rm ‐‐name todoapp ‐t rubyday/todo 
/opt/todo/bin/docker_entrypoint.sh: line 6: DB_PORT_5432_TCP_ADDR: unbound variable
LINKING! 
$ docker run ‐‐rm ‐‐link postgres:db ‐‐name todoapp  
‐t rubyday/todo /bin/bash ‐c 'env' 
DB_ENV_PGDATA=/var/lib/postgresql/data 
DB_NAME=/todoapp/db 
DB_PORT_5432_TCP_ADDR=172.17.0.4 
DB_PORT=tcp://172.17.0.4:5432 
DB_ENV_LANG=en_US.utf8 
DB_PORT_5432_TCP=tcp://172.17.0.4:5432 
DB_ENV_PG_MAJOR=9.3 
DB_PORT_5432_TCP_PORT=5432 
DB_PORT_5432_TCP_PROTO=tcp 
DB_ENV_PG_VERSION=9.3.5‐1.pgdg70+1 
DATABASE_URL=postgresql://postgres@172.17.0.4:5432/todo 
‐‐link postgres:db link container named postgres with alias db 
alias db tells docker to prefix all variables with DB
START! 
Pretty much standard business 
$ docker run ‐‐rm ‐‐link postgres:db ‐t rubyday/todo bundle exec rake db:create 
$ docker run ‐‐rm ‐‐link postgres:db ‐t rubyday/todo bundle exec rake db:schema:load 
$ docker run ‐‐link postgres:db ‐‐name todoapp ‐p 3000:3000 ‐d ‐t rubyday/todo 
7540f7647309110c53d2349cf7c68d1388e0f43de3d5904396fa2bb4041b6b28 
$ docker ps 
CONTAINER ID IMAGE [..] PORTS NAMES 
7540f7647309 rubyday/todo:latest [..] 0.0.0.0:3000‐>3000/tcp todoapp 
a5723351c46c postgres:9 [..] 5432/tcp postgres,todoapp/db 
‐p 3000:3000 creates a port forward from the host to the container
DOES IT WORK? 
$ netstat -lnp | grep 3000 
tcp6 0 0 :::3000 :::* LISTEN 3645/docker-proxy 
$ curl -v http://localhost:3000 
* Connected to localhost (127.0.0.1) port 3000 (#0) 
> GET / HTTP/1.1 
> User-Agent: curl/7.35.0 
> Host: localhost:3000 
> Accept: */* 
>< 
HTTP/1.1 200 OK 
[...] 
Good.
TOO MUCH WORK. LET'S AUTOMATE 
Enters FIG. 
fig.yml: 
web: 
build: . 
links: 
- db 
ports: 
- "3000:3000" 
db: 
image: postgres:9.3 
ports: 
- "5432"
..SMALL TWEAK FOR FIG 
diff --git a/bin/docker_entrypoint.sh b/bin/docker_entrypoint.sh 
index 0775ece..b69980c 100644 
--- a/bin/docker_entrypoint.sh 
+++ b/bin/docker_entrypoint.sh 
@@ -3,8 +3,8 @@ 
set -u 
set -e 
-PGADDR=$DB_PORT_5432_TCP_ADDR 
-PGPORT=$DB_PORT_5432_TCP_PORT 
+PGADDR=$DB_1_PORT_5432_TCP_ADDR 
+PGPORT=$DB_1_PORT_5432_TCP_PORT 
PGDBNAME="${DATABASE_NAME:-todo}" 
PGUSER="${DATABASE_USER:-postgres}"
PROFIT! 
$ fig up ‐d # familiar, huh? 
$ fig run web bundle exec rake db:create 
$ fig run web bundle exec rake db:schema:load 
$ netstat ‐lnp | grep 3000 
tcp6 0 0 :::3000 :::* LISTEN 24727/docker‐proxy 
$ curl ‐v http://localhost:3000 
* Connected to localhost (127.0.0.1) port 3000 (#0) 
> GET / HTTP/1.1 
> User‐Agent: curl/7.35.0 
> Host: localhost:3000 
> Accept: */* 
>< 
HTTP/1.1 200 OK 
[...]
MORE FIG COMMANDS 
$ fig ps 
Name Command State Ports 
‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐ 
todo_db_1 postgres Up 49160‐>5432/tcp 
todo_web_1 bundle exec rails server Up 3000‐>3000/tcp 
# if we remove the :3000 for the web port in the fig.yml 
$ fig scale web=2 
Starting todo_web_2... 
$ fig ps 
Name Command State Ports 
‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐ 
todo_db_1 postgres Up 49172‐>5432/tcp 
todo_web_2 bundle exec rails server Up 49174‐>3000/tcp 
todo_web_1 bundle exec rails server Up 49173‐>3000/tcp
VERY OPINIONATED TIPS 
Leverage the cache. 
$ time docker build ‐t rubyday/todo . 
# ==> 0m1.384s 
$ touch app/models/user.rb && time docker build ‐t rubyday/todo . 
# ==> 0m4.835s 
# Move the ADD . statement above bundle, then rebuild from scratch 
$ touch app/model/user.rb && time docker build ‐t rubyday/todo . 
# ==> 1m54.277s
VERY OPINIONATED TIPS 
Choose your storage driver wisely. 
devicemapper is slower. AUFS works ok. 
BTRFS is ... well... btrfs the future.
VERY OPINIONATED TIPS 
Always tag your image(s). 
Always pull supplying a tag. 
Always use a tag for FROM. 
Don't rely on :latest tag.
VERY OPINIONATED TIPS 
If possible, avoid run+commit. 
Prefer Dockerfiles. 
Waaaaaay more reproducible.
VERY OPINIONATED TIPS 
Installing ssh into the container is not clever. 
NSINIT is your friend (gist) 
https://gist.github.com/ubergarm/ed42ebbea293350c30a6
VERY OPINIONATED TIPS 
One process per container. Don't fork. 
Don't doublefork either. Stay in foreground.
VERY OPINIONATED TIPS 
Use a process manager. 
Both upstart or systemd are good at it. 
Run containers without the ‐d.
THANKS! 
That's all. For now.
QUESTIONS? 
QUESTIONS?
SHAMELESS PLUG 
These slides were made with applause 
https://github.com/Granze/applause 
Go check it out, it's AWESOME!

Streamline your development environment with docker

  • 1.
    Streamline your devenv with Docker Giacomo Bagnoli, #RUBYDAY2014
  • 2.
    WHOAMI Giacomo Bagnoli Currently Backend Engineer at Gild Previously at Amazon and Asidev Twitter: @gbagnoli Github: gbagnoli about.me/gbagnoli
  • 3.
    WHAT'S WRONG WITHMY DEV ENVIRONMENT? Nothing in particular. It's not* broken, so let's fix it. * conditions apply
  • 4.
    WHAT'S WRONG WITHMY DEV ENVIRONMENT? Your development environment is probably a beautiful, unique snowflake Photo credits: https://www.flickr.com/photos/amagill/4223790595/
  • 5.
    WHAT'S DOCKER, ANYWAY?!? Open source platform Docker Engine Container management runtime Packaging tools for images Docker Hub
  • 6.
    CONTAINERS, UH? Operatingsystem-level virtualization* Runs multiple isolated linux systems On a single host, with a single kernel No /sbin/init, no device emulation Think them as chroot on steroids Isolation provided by linux cgroups and namespaces Resource limiting and prioritization via cgroups Resource usage accounting via cgroups * not a virtualization method, containers != VM
  • 7.
    DOCKER IMAGES (0) Portable, read-only layers. Images are composed at run-time to form the container root FS using an union filesystem. Processes tricked to see the filesystem as R/W. The writable layer is discarded if the container is not committed. The read-only and portable properties are important as they enable sharing (via the docker hub).
  • 8.
    DOCKER IMAGES (1) Docker images form a Direct Acyclic Graph. Each layer is cached (possibly) and reused by other images. This means that if multiple images derive from debian:wheezy, that particular image is shared by all of them (thus downloaded once). Images are pushed/pull to/from the docker hub.
  • 9.
    DOCKER IMAGES (2) $ docker pull ubuntu:14.04 $ docker pull ubuntu:12.04 $ docker pull redis:2.8.13 $ docker pull debian:wheezy $ docker pull mongo:2.6.4 $ docker images REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE redis 2.8.13 dd52dc9c8f76 9 minutes ago 98.44 MB mongo 2.6.4 dd1f260c0731 12 minutes ago 391.2 MB debian wheezy 9cdcc6025135 18 hours ago 85.19 MB ubuntu 14.04 96864a7d2df3 2 days ago 204.4 MB ubuntu 12.04 ec966722cde4 2 days ago 103.8 MB
  • 10.
    DOCKER IMAGES (3) $ docker images ‐‐tree └─511136ea3c5a Virtual Size: 0 B └─b37448882294 Virtual Size: 85.19 MB └─9cdcc6025135 Virtual Size: 85.19 MB Tags: debian:wheezy ├─e365f7cdb352 Virtual Size: 85.52 MB │ └─b15940870e43 Virtual Size: 85.52 MB │ └─22ad4fc6b16f Virtual Size: 98.44 MB │ └─bd1e22dd175d Virtual Size: 98.44 MB │ └─3b1ce200fdad Virtual Size: 98.44 MB │ └─dd52dc9c8f76 Virtual Size: 98.44 MB Tags: redis:2.8.13 └─49fd1ae472a8 Virtual Size: 85.52 MB └─6c203838fd07 Virtual Size: 99.62 MB └─b1cd74f30329 Virtual Size: 391.2 MB └─9d0a3438646f Virtual Size: 391.2 MB └─dd1f260c0731 Virtual Size: 391.2 MB Tags: mongo:2.6.4
  • 11.
    LET'S BUILD ARUBY2 IMAGE Ubuntu 14.04 image has no ruby at all. Repos have ruby 1.9. Let's create an image with 2.1 as default. Dockerfile: FROM ubuntu:14.04 MAINTAINER Giacomo Bagnoli <gbagnoli@gmail.com> RUN echo "deb http://ppa.launchpad.net/brightbox/ruby‐ng/ubuntu trusty main" > /etc/apt/sources.list.d/ruby‐ng.list RUN apt‐key adv ‐‐keyserver hkp://keyserver.ubuntu.com:80 ‐‐recv‐keys C3173AA6 RUN apt‐get update RUN apt‐get install ‐y ruby2.1 Build! $ docker build ‐‐rm ‐t rubyday/ruby:2.1 .
  • 12.
    RUBY2 IMAGE Eachdirective in the Dockerfile adds a layer $ docker images ‐‐tree └─96864a7d2df3 Virtual Size: 204.4 MB Tags: ubuntu:14.04 └─8f1b6341c5be Virtual Size: 204.4 MB # MAINTAINER └─d323cc59da91 Virtual Size: 204.4 MB # RUN └─724a6664d97a Virtual Size: 204.4 MB # RUN └─8614dab05fbe Virtual Size: 224.8 MB # RUN └─d7ae4a198781 Virtual Size: 257.2 MB Tags: rubyday/ruby:2.1 # RUN $ docker run ‐t rubyday/ruby:2.1 ruby ‐v ruby 2.1.2p95 (2014‐05‐08 revision 45877) [x86_64‐linux‐gnu] Woah, 53Mb. apt‐get update adds 20Mb to the image.
  • 13.
    I TRIED... Let'sremove apt-get files by adding another RUN statement diff ‐‐git a/ruby2/Dockerfile b/ruby2/Dockerfile index dd37dcb..2b9c105 100644 ‐‐‐ a/ruby2/Dockerfile +++ b/ruby2/Dockerfile @@ ‐8,3 +8,4 @@ RUN echo "deb http://ppa.launchpad.net/brightbox/ruby‐ng/ubuntu trusty main" > / RUN apt‐key adv ‐‐keyserver hkp://keyserver.ubuntu.com:80 ‐‐recv‐keys C3173AA6 RUN apt‐get update RUN apt‐get install ‐y ruby2.1 +RUN rm ‐rf /var/lib/apt/lists/* /var/cache/apt/archives/*.deb
  • 14.
  • 15.
    LAYERS.. $ dockerimages ‐‐tree └─96864a7d2df3 Virtual Size: 204.4 MB Tags: ubuntu:14.04 └─8f1b6341c5be Virtual Size: 204.4 MB # MAINTAINER └─d323cc59da91 Virtual Size: 204.4 MB # RUN └─724a6664d97a Virtual Size: 204.4 MB # RUN └─8614dab05fbe Virtual Size: 224.8 MB # RUN └─d7ae4a198781 Virtual Size: 257.2 MB # RUN └─b8bb3ce3008e Virtual Size: 257.2 MB Tags: rubyday/ruby:2.1 # RUN Remember that every directive adds a layer. Layers are read only.
  • 16.
    LET'S TRY ONCEMORE Let's rewrite the Dockerfile FROM ubuntu:14.04 MAINTAINER Giacomo Bagnoli <gbagnoli@gmail.com> RUN echo "deb http://ppa.launchpad.net/brightbox/ruby‐ng/ubuntu trusty main" > /etc/apt/sources.list.d/ruby‐ng.list && apt‐key adv ‐‐keyserver hkp://keyserver.ubuntu.com:80 ‐‐recv‐keys C3173AA6 && apt‐get update && apt‐get install ‐y ruby2.1 && rm ‐rf /var/lib/apt/lists/* /var/cache/apt/archives/*.deb
  • 17.
    REBUILD Build: $docker build ‐‐rm ‐t rubyday/ruby:2.1 . $ docker images REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE rubyday/ruby 2.1 b337a5c538f3 About a minute ago 236.9 MB └─96864a7d2df3 Virtual Size: 204.4 MB Tags: ubuntu:14.04 └─86ae939e2da3 Virtual Size: 204.4 MB # MAINTAINER └─b337a5c538f3 Virtual Size: 236.9 MB Tags: gild/ruby:2.1 # RUN yay!
  • 18.
    PAT ME ONTHE BACK
  • 19.
    ... ONE MORETHING We probably want ‐dev packages and bundle Let's update the Dockerfile FROM ubuntu:14.04 MAINTAINER Giacomo Bagnoli RUN echo "deb http://ppa.launchpad.net/brightbox/ruby‐ng/ubuntu trusty main" > /etc/apt/sources.list.d/ruby‐ng.list RUN apt‐key adv ‐‐keyserver hkp://keyserver.ubuntu.com:80 ‐‐recv‐keys C3173AA6 RUN apt‐get update && apt‐get install ‐y build‐essential && apt‐get install ‐y ruby2.1 ruby2.1‐dev && update‐alternatives ‐‐set ruby /usr/bin/ruby2.1 && update‐alternatives ‐‐set gem /usr/bin/gem2.1 && rm ‐rf /var/lib/apt/lists/* /var/cache/apt/archives/*.deb RUN gem install bundle
  • 20.
    TIME FOR RAILS Let's try creating a Dockerfile for a rails app. The app is a random simple TODO list application found on Github. It's a rails4 application that uses SQL, nothing fancy. Let's assume we are developing this app targeting postgresql. Github url: https://github.com/gbagnoli/todo‐rails4‐angularjs
  • 21.
    A WORD ONLINKING In docker, we can access service(s) running in other container(s) via linking. Linking a container to another will setup some environment variables in it, allowing the container to discover and connect to the service. We will use this feature to access postgres from our app container.
  • 22.
    THE DOCKERFILE FROMrubyday/ruby:2.1 MAINTAINER Giacomo Bagnoli <gbagnoli@gmail.com> RUN adduser todo ‐‐home /opt/todo ‐‐shell /bin/bash ‐‐disabled‐password ‐‐gecos "" RUN apt‐get update && apt‐get install ‐y libpq‐dev nodejs && rm ‐rf /var/lib/apt/lists/* /var/cache/apt/archives/*.deb ADD Gemfile /opt/todo/ ADD Gemfile.lock /opt/todo/ RUN chown ‐R todo:todo /opt/todo RUN su ‐c "D=/opt/todo/bundle; mkdir $D && bundle install ‐‐deployment ‐‐path $D" ‐s /bin/bash ‐l todo WORKDIR /opt/todo EXPOSE 3000 ADD . /opt/todo RUN chown ‐R todo:todo /opt/todo USER todo ENTRYPOINT ["/bin/bash", "/opt/todo/bin/docker_entrypoint.sh"] CMD ["bundle", "exec", "rails", "server"]
  • 23.
    DOCKERFILE EXPLAINED (0) FROM rubyday/ruby:2.1 MAINTAINER Giacomo Bagnoli <gbagnoli@gmail.com>
  • 24.
    DOCKERFILE EXPLAINED (1) RUN adduser todo ‐‐home /opt/todo ‐‐shell /bin/bash ‐‐disabled‐password ‐‐gecos "" RUN apt‐get update && apt‐get install ‐y libpq‐dev nodejs && rm ‐rf /var/lib/apt/lists/* /var/cache/apt/archives/*.deb
  • 25.
    DOCKERFILE EXPLAINED (2) ADD Gemfile /opt/todo/ ADD Gemfile.lock /opt/todo/ RUN chown ‐R todo:todo /opt/todo RUN su ‐c "D=/opt/todo/bundle; mkdir $D && bundle install ‐‐deployment ‐‐path $D" ‐s /bin/bash ‐l todo
  • 26.
    DOCKERFILE EXPLAINED (3) WORKDIR /opt/todo EXPOSE 3000 ADD . /opt/todo RUN chown ‐R todo:todo /opt/todo
  • 27.
    DOCKERFILE EXPLAINED (4) USER todo ENTRYPOINT ["/bin/bash", "/opt/todo/bin/docker_entrypoint.sh"] CMD ["bundle", "exec", "rails", "server"]
  • 28.
    POSTGRES CONTAINER (0) $ docker pull postgres:9.3 $ docker run ‐d ‐‐name postgres ‐t postgres:9.3 a5723351c46ce015d585dd49f230ecb376557d0b955f233dbff3bf92f3a6721d $ docker ps CONTAINER ID IMAGE [...] PORTS NAMES a5723351c46c postgres:9 [...] 5432/tcp postgres This container EXPOSEs port 5432. Question is, how do we connect to it?
  • 29.
    We can't justhardcode its ip address, as it defeats the purpose... $ docker inspect postgres | grep NetworkSettings ‐A 9 "NetworkSettings": { "Bridge": "docker0", "Gateway": "172.17.42.1", "IPAddress": "172.17.0.4", "IPPrefixLen": 16, "PortMapping": null, "Ports": { "5432/tcp": null } }, POSTGRES CONTAINER (1)
  • 30.
    THE WRAPPER SCRIPT(0) In the Dockerfile, an ENTRYPOINT was specified. #!/bin/bash # exit with error if a variable is unbound (not set) set ‐u # exit with error if a command returns a non‐zero status set ‐e PGADDR=$DB_PORT_5432_TCP_ADDR PGPORT=$DB_PORT_5432_TCP_PORT PGDBNAME="${DATABASE_NAME:‐todo}" PGUSER="${DATABASE_USER:‐postgres}" # export database configuration for rails. export DATABASE_URL="postgresql://${PGUSER}@${PGADDR}:${PGPORT}/${PGDBNAME}" # exec what the user wants exec "$@"
  • 31.
    THE WRAPPER SCRIPT(1) Trying to execute the container will throw an error (it's a feature!) $ docker run ‐‐rm ‐‐name todoapp ‐t rubyday/todo /opt/todo/bin/docker_entrypoint.sh: line 6: DB_PORT_5432_TCP_ADDR: unbound variable
  • 32.
    LINKING! $ dockerrun ‐‐rm ‐‐link postgres:db ‐‐name todoapp ‐t rubyday/todo /bin/bash ‐c 'env' DB_ENV_PGDATA=/var/lib/postgresql/data DB_NAME=/todoapp/db DB_PORT_5432_TCP_ADDR=172.17.0.4 DB_PORT=tcp://172.17.0.4:5432 DB_ENV_LANG=en_US.utf8 DB_PORT_5432_TCP=tcp://172.17.0.4:5432 DB_ENV_PG_MAJOR=9.3 DB_PORT_5432_TCP_PORT=5432 DB_PORT_5432_TCP_PROTO=tcp DB_ENV_PG_VERSION=9.3.5‐1.pgdg70+1 DATABASE_URL=postgresql://postgres@172.17.0.4:5432/todo ‐‐link postgres:db link container named postgres with alias db alias db tells docker to prefix all variables with DB
  • 33.
    START! Pretty muchstandard business $ docker run ‐‐rm ‐‐link postgres:db ‐t rubyday/todo bundle exec rake db:create $ docker run ‐‐rm ‐‐link postgres:db ‐t rubyday/todo bundle exec rake db:schema:load $ docker run ‐‐link postgres:db ‐‐name todoapp ‐p 3000:3000 ‐d ‐t rubyday/todo 7540f7647309110c53d2349cf7c68d1388e0f43de3d5904396fa2bb4041b6b28 $ docker ps CONTAINER ID IMAGE [..] PORTS NAMES 7540f7647309 rubyday/todo:latest [..] 0.0.0.0:3000‐>3000/tcp todoapp a5723351c46c postgres:9 [..] 5432/tcp postgres,todoapp/db ‐p 3000:3000 creates a port forward from the host to the container
  • 34.
    DOES IT WORK? $ netstat -lnp | grep 3000 tcp6 0 0 :::3000 :::* LISTEN 3645/docker-proxy $ curl -v http://localhost:3000 * Connected to localhost (127.0.0.1) port 3000 (#0) > GET / HTTP/1.1 > User-Agent: curl/7.35.0 > Host: localhost:3000 > Accept: */* >< HTTP/1.1 200 OK [...] Good.
  • 35.
    TOO MUCH WORK.LET'S AUTOMATE Enters FIG. fig.yml: web: build: . links: - db ports: - "3000:3000" db: image: postgres:9.3 ports: - "5432"
  • 36.
    ..SMALL TWEAK FORFIG diff --git a/bin/docker_entrypoint.sh b/bin/docker_entrypoint.sh index 0775ece..b69980c 100644 --- a/bin/docker_entrypoint.sh +++ b/bin/docker_entrypoint.sh @@ -3,8 +3,8 @@ set -u set -e -PGADDR=$DB_PORT_5432_TCP_ADDR -PGPORT=$DB_PORT_5432_TCP_PORT +PGADDR=$DB_1_PORT_5432_TCP_ADDR +PGPORT=$DB_1_PORT_5432_TCP_PORT PGDBNAME="${DATABASE_NAME:-todo}" PGUSER="${DATABASE_USER:-postgres}"
  • 37.
    PROFIT! $ figup ‐d # familiar, huh? $ fig run web bundle exec rake db:create $ fig run web bundle exec rake db:schema:load $ netstat ‐lnp | grep 3000 tcp6 0 0 :::3000 :::* LISTEN 24727/docker‐proxy $ curl ‐v http://localhost:3000 * Connected to localhost (127.0.0.1) port 3000 (#0) > GET / HTTP/1.1 > User‐Agent: curl/7.35.0 > Host: localhost:3000 > Accept: */* >< HTTP/1.1 200 OK [...]
  • 38.
    MORE FIG COMMANDS $ fig ps Name Command State Ports ‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐ todo_db_1 postgres Up 49160‐>5432/tcp todo_web_1 bundle exec rails server Up 3000‐>3000/tcp # if we remove the :3000 for the web port in the fig.yml $ fig scale web=2 Starting todo_web_2... $ fig ps Name Command State Ports ‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐ todo_db_1 postgres Up 49172‐>5432/tcp todo_web_2 bundle exec rails server Up 49174‐>3000/tcp todo_web_1 bundle exec rails server Up 49173‐>3000/tcp
  • 39.
    VERY OPINIONATED TIPS Leverage the cache. $ time docker build ‐t rubyday/todo . # ==> 0m1.384s $ touch app/models/user.rb && time docker build ‐t rubyday/todo . # ==> 0m4.835s # Move the ADD . statement above bundle, then rebuild from scratch $ touch app/model/user.rb && time docker build ‐t rubyday/todo . # ==> 1m54.277s
  • 40.
    VERY OPINIONATED TIPS Choose your storage driver wisely. devicemapper is slower. AUFS works ok. BTRFS is ... well... btrfs the future.
  • 41.
    VERY OPINIONATED TIPS Always tag your image(s). Always pull supplying a tag. Always use a tag for FROM. Don't rely on :latest tag.
  • 42.
    VERY OPINIONATED TIPS If possible, avoid run+commit. Prefer Dockerfiles. Waaaaaay more reproducible.
  • 43.
    VERY OPINIONATED TIPS Installing ssh into the container is not clever. NSINIT is your friend (gist) https://gist.github.com/ubergarm/ed42ebbea293350c30a6
  • 44.
    VERY OPINIONATED TIPS One process per container. Don't fork. Don't doublefork either. Stay in foreground.
  • 45.
    VERY OPINIONATED TIPS Use a process manager. Both upstart or systemd are good at it. Run containers without the ‐d.
  • 46.
  • 47.
  • 48.
    SHAMELESS PLUG Theseslides were made with applause https://github.com/Granze/applause Go check it out, it's AWESOME!