Docker体验之:在MACOS下安装docker并构建nodejs开发环境

作者:web前端    发布时间:2020-02-04 01:10     浏览次数 :

[返回]

Vidar Hokstad在Docker使用方面非常有经验,尤其在没有数据丢失前提下,使用Docker创建可重复build上经验丰富,在本博客中,他总结了开发Docker容器的8种模式。以下为译文:Docker现在成了我最喜欢的工具,在本文中,我将概述一些在我使用Docker过程中反复出现的模式。我不期待它们能给你带来多少惊喜,但我希望这些能对你有用,我非常愿意与你交流在使用Docker过程中碰到的模式。我所有Docker实验的基础是保持volume状态不变,以便Docker容器在没有数据丢失的前提下任意重构。下面所有的Dockerfiles例子都集中在:创建容器在其本身可以随时更换的地方,而无需考虑其它。1. The Shared Base Container(s)Docker鼓励“继承”,这应用也很自然——这是高效使用Docker的一个基本方式,不仅由于它有助于减少建立新容器的时间,Docker优点多多,它会cache中间步骤,但也容易在不明确的情况下,失去分享机会。很显然,在将我的各种容器迁移到Docker上时,首先要面对的是多个步骤。对于多数想要随处部署的项目来说所,要创建多个容器,尤其是在这个项目需要长进程,或者需要特定包的情况,所以我要运行的容器也变得越来越多。重要的是为了让mybase环境完全自由支配,我正考虑试图在Docker上运行“所有一切”。所以我很快开始提取我的基本设置到base容器。这是我当的“devbase”Dockerfile:

本文由网上资料及自己实践整理。

p.p1 {margin: 0.0px 0.0px 0.0px 0.0px; font: 13.0px 'Helvetica Neue'; color: #555555}

FROM debian:wheezy RUN apt-get update RUN apt-get -y install ruby ruby-dev build-essential git RUN apt-get install -y libopenssl-ruby libxslt-dev libxml2-dev # For debugging RUN apt-get install -y gdb strace # Set up my user RUN useradd vidarh -u 1000 -s /bin/bash --no-create-home RUN gem install -n /usr/bin bundler RUN gem install -n /usr/bin rake WORKDIR /home/vidarh/ ENV HOME /home/vidarh VOLUME ["/home"] USER vidarh EXPOSE 8080

 

p.p2 {margin: 0.0px 0.0px 0.0px 0.0px; font: 20.0px 'Helvetica Neue'; color: #555555}

这里没有什么需要特别说明的地方——它安装一些需要随时可用的特定工具。这些可能会对大多数人来说是不同的。值得注意的是如果/当你重建一个容器的时候,你需要指定一个特定的标签来避免意外。使用默认端口8080,因为这是我发布web app的端口,这也是我用这些容器的目的。它为我添加了一个用户,并且不会创建一个/ home目录。我从宿主机绑定挂载了一个共享文件夹/ home,这就引出了下一个模式。2. The Shared Volume Dev Container我所有的dev容器与宿主机分享至少一个volume: / home,这是为了便于开发。对于许多app,在开发模式中,使用基于file-system-change的code-reloader运行,这样一来容器内封装了OS / distro-level的依赖,并在初始环境中帮助验证app-as-bundled工作,而不需要让我每次在代码改变时重启/重建VM。至于其他,我只需要重启容器来应对代码的更改。对于test/staging和production容器,大多数情况下不通过volume共享代码,转而使用“ADD”来增添代码到Docker容器中。这是我的“homepage”的dev容器的Dockerfile,例如,包含我的个人wiki,存在于“devbase”容器中的 /home下,以下展示了如何使用共享的base容器和/home卷:

什么是Docker?

docker的英文本意是码头工人,也就是搬运工,这种搬运工搬运的是集装箱(Container),集装箱里面装的可不是商品货物,而是任意类型的App,Docker把App(叫Payload)装在Container内,通过Linux Container技术的包装将App变成一种标准化的、可移植的、自管理的组件,这种组件可以在你的latop上开发、调试、运行,最终非常方便和一致地运行在production环境下。

Docker的核心底层技术是LXC(Linux Container),Docker在其上面加了薄薄的一层,添加了许多有用的功能。这篇stackoverflow上的问题和答案很好地诠释了Docker和LXC的区别,能够让你更好的了解什么是Docker, 简单翻译下就是以下几点:

  • Docker提供了一种可移植的配置标准化机制,允许你一致性地在不同的机器上运行同一个Container;而LXC本身可能因为不同机器的不同配置而无法方便地移植运行;
  • Docker以App为中心,为应用的部署做了很多优化,而LXC的帮助脚本主要是聚焦于如何机器启动地更快和耗更少的内存;
  • Docker为App提供了一种自动化构建机制(Dockerfile),包括打包,基础设施依赖管理和安装等等;
  • Docker提供了一种类似git的Container版本化的机制,允许你对你创建过的容器进行版本管理,依靠这种机制,你还可以下载别人创建的Container,甚至像git那样进行合并;
  • Docker Container是可重用的,依赖于版本化机制,你很容易重用别人的Container(叫Image),作为基础版本进行扩展;
  • Docker Container是可共享的,有点类似github一样,Docker有自己的INDEX,你可以创建自己的Docker用户并上传和下载Docker Image;
  • Docker提供了很多的工具链,形成了一个生态系统;这些工具的目标是自动化、个性化和集成化,包括对PAAS平台的支持等;

那么Docker有什么用呢?对于运维来说,Docker提供了一种可移植的标准化部署过程,使得规模化、自动化、异构化的部署成为可能甚至是轻松简单的事情;而对于开发者来说,Docker提供了一种开发环境的管理方法,包括映像、构建、共享等功能,而后者是本文的主题。

 

p.p3 {margin: 0.0px 0.0px 0.0px 0.0px; font: 13.0px 'Helvetica Neue'; color: #555555; min-height: 15.0px}

FROM vidarh/devbase WORKDIR /home/vidarh/src/repos/homepage ENTRYPOINT bin/homepage web

Docker的安装和构成

Docker官方本身提供了非常具体的安装教程,这里不说具体的安装过程,请参考Docker安装(Mac系统),重要的是描述下原理和安装完成后的结构,好对Docker更好的了解。 由于LXC本身不支持Mac内核,因此需要跑一个VirtualBox虚拟机(TinyCoreLinux)来安装,幸好Docker社区提供了一个非常方便的工具boot2docker(其实就是一个VBoxManage的包装shell脚本),用于安装Mac下的整个Docker环境。具体的结构如下:

图片 1

如图所示,安装完成后,具体情况如下:

  • 在Mac的home目录~/.boot2docker下创建了虚拟机所需要的文件,其中boot2docker.iso是虚拟机映像,这是一个由CD-ROM引导的TinyCoreLinux系统;而boot2docker-vm.vmdk文件则是你的虚拟机磁盘,你所有的持久化数据都存放在这里,包括docker创建的lxc容器等文件。
  • 在Mac下,docker被分为客户端docker-client和服务端docker-daemon两部分,如果是在linux(比如ubuntu),实际上则是同一个可执行文件同时充当客户端和服务端。docker-daemon可以监听unix scoket,也可以在tcp socket(默认端口为4234),docker-client会通过一个叫DOCKER_HOST的环境变量读取服务地址和端口,因此你应该在你的bash_profile文件里面添加这么一行:

     

     

    1
    2
    export DOCKER_HOST=tcp://127.0.0.1:4243
     

     

docker-daemon跑在虚拟机上,这个程序实际上就是接收docker-client发送过来的消息命令,创建、启动和销毁lxc容器,以及docker本身的版本管理、映像存储等等 运行你的第一个docker容器 安装完成后,就差不多可以开始创建和运行docker容器了,在这之前,你首先得下载一个Image,什么是Image?我们先来了解docker的2个基础概念:ImageContainer

Container和Image 在Docker的世界里,Image是指一个只读的层(Layer),这里的层是AUFS里的概念,最直观的方式就是看一下docker官方给出的图:

图片 2

Docker使用了一种叫AUFS的文件系统,这种文件系统可以让你一层一层地叠加修改你的文件,最底下的文件系统是只读的,如果需要修改文件,AUFS会增加一个可写的层(Layer),这样有很多好处,例如不同的Container可以共享底层的只读文件系统(同一个Kernel),使得你可以跑N多个Container而不至于你的硬盘被挤爆了!这个只读的层就是Image!而如你所看到的,一个可写的层就是Container。

那Image和Container的区别是什么?很简单,他们的区别仅仅是一个是只读的层,一个是可写的层,你可以使用docker commit 命令,将你的Container变成一个Image,也就是提交你所运行的Container的修改内容,变成一个新的只读的Image,这非常类似于git commit命令,感觉真棒!

实际上这就是Docker对Container映像的版本管理基石,AUFS文件系统实在是太美妙了,更多细节可以参考DotCloud的这篇文章。

 

p.p4 {margin: 0.0px 0.0px 0.0px 0.0px; font: 13.0px 'Helvetica Neue'; color: #101010; min-height: 15.0px}

以下是dev-version的博客:

运行和退出

在了解了Image和Container的概念后,我们可以开始下载一个Image,Docker的好处就是提供了一个类似github的Image仓库管理,你可以非常方便pull别人的Image下来运行,例如,我们可以下载一个ubuntu Image:

 

 

1
2
docker pull ubuntu:13.10
 

 

这里的13.10是一个Tag,类似于git的tag,这里的tag可以为你制定一个ubuntu的版本。下载完成后,执行docker images命令可以列出你已经下载或者自己构建的image:(请允许我使用可爱的马赛克 :) )

图片 3

你可以看到ubuntu:13.10的大小为178MB,以及它的IMAGE ID。 现在我们开始运行一个Container,命令很简单,例如我们想运行一个执行Shell终端的Container:

图片 4

如你看到的,你已经进入到一个Shell里面,可以执行你想执行的任何命令,就和在ubuntu里面一样,进去后默认是在根目录/下,可以看到经典的unix/linux目录结构,以及你所运行的bash版本等信息。你可以给你的Container定一个名字,通过–name选项,例如这里命名了shell,日后你就可以直接用这个名字引用Contanier。

退出一个Container也很简单,你直接exit就好了。 其他更多的命令这里不做赘述,因为官方的文档已经非常全面,这里只是给一个直观的初步印象。下面进入主题。

 

p.p6 {margin: 0.0px 0.0px 0.0px 0.0px; font: 12.0px Monaco; color: #aaaaaa; background-color: #f8f8ff}

FROM vidarh/devbase WORKDIR / USER root # For Graphivz integration RUN apt-get update RUN apt-get -y install graphviz xsltproc imagemagick USER vidarh WORKDIR /home/vidarh/src/repos/hokstad-com ENTRYPOINT bundle exec rackup -p 8080

利用Docker搭建开发环境

我们先看看程序员在搭建开发环境时遇到的一些问题:

  • 软件安装麻烦,比如很多公司都使用redhat,一般开发人员又不给root,安装一个nginx或者是mysql都得自己下载编译安装 权限问题,没有root,一些软件无法运行,例如dnsmasq;
  • 没有root,无法修改hosts,无法netstat -nptl,无法tcpdump,无法iptable
  • 隔离性差,例如不同的开发人员如果在同一台主机环境下共享开发,虽然是用户隔离,但端口如果不规范可能会冲突;同一个Mysql如果权限管理不好很有可能误删别人的数据
  • 可移植性差,例如和生产环境不一致,开发人员之间也无法共享;更严重的情况是当有新人入职时,通常需要又折腾一遍开发环境,无法快速搭建

这些问题可以通过在本地搭建虚拟机来解决,但虚拟机是一个很笨重的解决方案,Docker是一个非常轻量级的方案,而且还拥有虚拟机没有的一些功能,例如标准化Image,Image共享等,更重要的是,利用Docker,你可以运行非常多的容器,在你的Mac下搭建一个分布式的开发环境根本不是什么大的问题,而且对内存、磁盘和cpu的消耗相比传统的虚拟机要低许多,这些都要归功于AUFS和LXC这两大神奇的技术。

 

p.p8 {margin: 0.0px 0.0px 0.0px 0.0px; font: 19.0px 'Helvetica Neue'; color: #555555}

因为他们从一个共享的库中取代码,并且基于一个共享的base容器,这些容器通常当我添加/修改/删除依赖项时会极其迅速重建。即便如此也有一些地方是我非常愿意改善,尽管上面的base是轻量级的,他们大多数在这些容器仍未使用。由于Docker使用copy-on-write覆盖,这不会导致一个巨大的开销,但它仍然意味着我没有做到最小的资源消耗,或者说最小化attack或error的几率。3. The Dev Tools Container这可能对我们这些喜欢依靠ssh写代码的人很有吸引力,但是对IDE人群则小一点。对我来说,关于以上设置更大的 一个好处,是它让我在开发应用程序中,能够将编辑和测试执行代码的工作分离开来。过去dev-systems对我来说一件烦人的事,是dev和production依赖项以及开发工具依赖项容易混淆,很容易产生非法的依赖项。虽然有很多方法解决这个,比如通过定期的测试部署,但我更偏爱下面的解决方案,因为可以在第一时间防止问题的发生:我有一个单独的容器包含Emacs的installation以及其他各种我喜欢的工具,我仍然试图保持sparse,但关键是我的screen session可以运行在这个容器中,再加上我笔记本电脑上的“autossh”,这个连接几乎一直保持,在那里我可以编辑代码,并且和我的其他dev容器实时共享。如下:

构建基础Image

 

想要搭建一个节省磁盘空间和扩展性良好的开发环境,最重要的第一步就是构建一个基础性的Image,比如你的主要开发语言是Nodejs,那么你肯定需要一个已经安装好以下工具的基础Image:

  • node
  • npm

然后在此基础上,你可以扩展这个基础的Image(下面叫base)为不同的开发环境,例如nodejs,或者是nats。当然,你的这个base也可以从别人的Image扩展而来,还记得我们刚刚pull下来的ubuntu:13.10这个Image吗?你可以从这个Image扩展开始构建你的base,如何做呢?Docker提供了一种标准化的DSL方式,你只需要编写一个Dockerfile,运行docker build指令,就可以构建你自己的Image,这有点像Makefile和make命令一样,只是大家要构建的内容和构建语言不同。

 

Dockerfile的语法请参考Dockerfile Reference,这里给出nodejs的Dockerfile开发环境实例:


2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
FROM dockerfile/ubuntu
 
MAINTAINER Abhinav Ajgaonkar <abhinav316@gmail.com>
 
# update apt-get
RUN
apt-get update &&
apt-get upgrade 
 
# Install Redis
RUN   
  apt-get -y -qq install python redis-server
 
# Install Node
RUN   
  cd /opt && 
  wget http://nodejs.org/dist/v0.10.28/node-v0.10.28-linux-x64.tar.gz && 
  tar -xzf node-v0.10.28-linux-x64.tar.gz && 
  mv node-v0.10.28-linux-x64 node && 
  cd /usr/local/bin && 
  ln -s /opt/node/bin/* . && 
  rm -f /opt/node-v0.10.28-linux-x64.tar.gz
 
# Set the working directory
WORKDIR   /src
 
CMD ["/bin/bash"]

 

我们一行一行的来理解,

?

1
FROM dockerfile/ubuntu

这回告诉docker要使用Docker Inc. 提供的 dockerfile/ubuntu 镜像. 作为构建的基准镜像.

 

 

# update apt-get
RUN 
apt-get update && 
apt-get upgrade 

这里要多apt-get更新一下,要不在安装python的时候这个会出现“E: Package <python> has no installation candidate”错误,当然如果images封装得好的话,这里的更新可以忽略的。

 

?

1
2
RUN  
  apt-get -y -qq install python redis-server

基准镜像完全没有包含任何东西——因此我们需要使用apt-get来获取应用运行起来所需的所有东西. 这一句会安装python 和 redis-server. Redis 服务器是必须的,因为我们将会把会话信息存储到它之中,而python的必要性则是通过npm可以构建为Redis node模块所需的C扩展.

?

1
2
3
4
5
6
7
8
RUN  
  cd /opt && 
  wget http://nodejs.org/dist/v0.10.28/node-v0.10.28-linux-x64.tar.gz && 
  tar -xzf node-v0.10.28-linux-x64.tar.gz && 
  mv node-v0.10.28-linux-x64 node && 
  cd /usr/local/bin && 
  ln -s /opt/node/bin/* . && 
  rm -f /opt/node-v0.10.28-linux-x64.tar.gz

这会下载并提取64位的NodeJS二进制文件.

?

1
WORKDIR  /src

这句会告诉docker一旦容器已经启动,在执行CMD属性指定的东西之前,要做一次 cd /src.

?

1
CMD ["/bin/bash"]

作为最后一步,运行 /bin/bash.

 

写好Dockerfile后,运行以下指令就可以创建你的base image了:

1
2
docker build --rm -t myname/nodejs-dev:base . //注意:docker build命令最后一定要以.结束才能执行下去
 

-t 选项是你要构建的base image的tag,就好比ubuntu:13.10一样 –rm 选项是告诉Docker在构建完成后删除临时的Container,Dockerfile的每一行指令都会创建一个临时的Container,一般你是不需要这些临时生成的Container的 如你所想,我们可以像运行ubuntu:13.10那样运行我们的base了:

1
2
docker run -i -t --name ruby myname/nodejs-dev:base node
 

 这里我们使用myname/nodejs-dev:base这个Image运行了一个node解释器。

 

p.p9 {margin: 0.0px 0.0px 0.0px 0.0px; font: 12.0px Monaco}

FROM vidarh/devbaseRUN apt-get updateRUN apt-get -y install openssh-server emacs23-nox htop screen# For debuggingRUN apt-get -y install sudo wget curl telnet tcpdump# For 32-bit experimentsRUN apt-get -y install gcc-multilib # Man pages and "most" viewer:RUN apt-get install -y man mostRUN mkdir /var/run/sshdENTRYPOINT /usr/sbin/sshd -DVOLUME ["/home"]EXPOSE 22EXPOSE 8080

指定Image入口

当你构建完你的base Image和其他应用的Image之后,你就可以启动这些Image了,还记得前面我们给出的运行命令吗?

1
2
docker run -i -t --name shell myname/nodejs-dev:base /bin/bash
 

这里我们运行了一个bash,这样你就可以在shell里面执行你所想要执行的任何命令了,但是我们有时候并不想每次都启动一个shell,接着再在shell里面启动我们的程序,比如一个mysql,而是想一启动一个容器,mysql服务就自动运行了,这很简单,Dockerfile提供了CMD和ENTRYPOINT这2个指令,允许你指定一个Image启动时的默认命令。CMD和ENTRYPOINT的区别是CMD的参数可以由docker run指令指定的参数覆盖,而ENTRYPOINT则不可以。例如我们想运行一个memcached服务,可以这么写Dockerfile:

 

 

1
2
3
FROM dockerfile/ubuntu
RUN apt-get install -y memcached CMD memcached -u root -p 40000
 

 

或者可以这么写:

 

 

1
2
3
FROM dockerfile/ubuntu
RUN apt-get install -y memcached ENTRYPOINT ["memcached", "-u", "root", "-p", "40000"]
 

 

注意不要把memcached启动为后台进程,即加上-d选项,否则docker启动的container会马上stop掉,这点我也觉得比较意外。 接着我们build这个Image:

 

 

1
2
docker build -t dev:memcache .
 

 

这样,当你build完你的Image后,你可以直接将该Image运行为一个容器,它会自动启动mysql服务:

 

 

1
2
docker run --name memcache_service -d dev:memcache
 

 

注意使用-d (detach) 选项,这样这个container就会作为后台进程运行了,接着你可以使用docker ps命令查看是否有在运行。

 

p.p10 {margin: 0.0px 0.0px 0.0px 0.0px; font: 12.0px Monaco; color: #008080; background-color: #f8f8ff}

结合共享“/ home“,已经足够让ssh的接入了,并且被证明能满足我的需要。4. The Test In A Different Environment containers我喜欢Docker的一个原因,是它可以让我在不同的环境中测试我的代码。例如,当我升级Ruby编译器到1.9时,我可以生成一个Dockerfile,派生一个1.8的环境。

磁盘映射

大部分时候你会需要把你host主机(宿主)上的目录映射到Container里面,这样你就非常方便地在host主机上编辑代码,然后直接就可以在Container里面运行它们,而不用手动copy到Container里面再重启Container。按理将host的目录映射到guest(指Container)上应该是一件很容易的事情,就好像VMWare那样,但可惜的是,由于Mac上的Docker多了一层虚拟机,因此多了一层周折,你必须先VM上的目录通过sshfs mount到host(指Mac)上,然后再将你的目录或文件copy到这个mount的目录,再将VM上的这个目录映射到Container里,听起来比较拗口,画个图会清晰很多。

图片 5

如上图所示,VM里面的/mnt/sda1/dev/目录(你需要自己创建)通过sshfs命令mount到了host主机(Mac)的~/workspace/dev/目录 ,而VM里的/mnt/sda1/dev/目录又被映射到了Container的/src/目录下,这样你就可以在Container里面的/src/目录下访问你的host文件了。具体如何做呢?首先你需要安装sshfs命令,然后将VM的password写到一个文件中,例如~/.boot2docker/b2d-passwd,在用sshfs命令mount起VM的/mnt/sda1/dev目录:

首先在Mac主机上安装osxfuse和sshfs,下载地址:

然后在boot2docker里执行以下命令:

sudo mkdir /mnt/sda1/dev

sudo chown -R docker:docker /mnt/sda1/dev

接下来在mac主机里执行:

cat tcuser > ~/.boot2docker/b2d-passwd

这下就可以进行挂载了,在mac主机执行:

sshfs docker@localhost:/mnt/sda1/dev ~/workspace/node -oping_diskarb,volname=node-dev -p 2022 
 -o reconnect -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o password_stdin < ~/.boot2docker/b2d-passwd

如要去除挂载命令如下:

umount -f  ~/workspace/node

接着你在run一个Container的时候需要通过-v选项来将/mnt/sda1/dev/映射到/src目录:

docker run -i -t myname/nodejs-dev:base -v /mnt/sda1/dev:/src /bin/bash

 

这样你就可以在你的Container的/src目录下看到你host里的文件了。 磁盘映射还有2个地方需要注意:

  • 你的文件实际上是存储在VM里面的,也就是说你需要将你的目录或者文件copy到VM里面,你sshfs之后,就是copy到~/workspace/node目录下
  • 千万不要sshfs mount非/mnt/sda1下的目录,因为VM里面跑的是TinyCoreLinux,这个OS的rootfs是临时性的(放在内存的,实际上就是boot2docker.iso文件里面的一个rootfs),因此其根目录/下的东西(包括/home)根本不会持久化,只有/mnt/sda1这个目录下的才能持久化。如果你放在/home目录下,只要VM一重启,就会丢失的,/mnt/sda1则不会,实际上就是那个~/.boot2docker-vm.vmdk文件挂载到了/mnt/sda1目录下

p.p11 {margin: 0.0px 0.0px 0.0px 0.0px; font: 12.0px Monaco; color: #555555; background-color: #f8f8ff}

FROM vidarh/devbaseRUN apt-get updateRUN apt-get -y install ruby1.8 git ruby1.8-dev

端口映射

和磁盘映射一样,你有时候会需要将Container的端口映射到host主机上,同样蛋疼的是,由于多了一层VM,端口映射也显得比较麻烦。首先你需要设置VirtualBox的端口映射,然后再将Container的端口映射到你的VM里面:

图片 6

具体是这么做的,通过2条命令:

boot2docker ssh -L 8000:localhost:8000 

docker run -i -t -p 8000:8000

 

也就是说在docker run的时候通过-p选项指定要映射的端口到VM,而boot2docker ssh命令则是将VM的8000端口映射到了host(Mac)的8000端口,这样你就可以通过Mac的localhost:8000访问Container的8000端口了。 其实,有另一种解决方案就是你不用映射到host(Mac),而是直接登录到VM里面进行访问就好了,boot2docker ssh就可以登录到VM,这样就类似于你的host是ubuntu,但这种解决方案的问题是这个ubuntu太弱了(TinyCoreLinux),如果你在这个ubuntu里面开发代码,或者是运行浏览器,是非常蛋疼的事情,关键还是这个ubuntu是每次重启都会复原的!所以我建议还是做多一层映射好了。 最后,实际上在VM里面,你是可以直接访问所有的Container的端口的,因为VM到Container的网络都是桥接的。

 

ok,万事具备,我们现在在刚才挂载的~/workspace/node目录下构建nodejs程式应用,此应用包含一个 package.json, server.js ,如下:

package.json

?

1
2
3
4
5
6
7
8
9
10
11
{
    "name""docker-dev",
    "version""0.1.0",
    "description""Docker Dev",
    "dependencies": {
        "connect-redis""~1.4.5",
        "express""~3.3.3",
        "hiredis""~0.1.15",
        "redis""~0.8.4"
    }
}

server.js

?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
var express = require('express'),
    app = express(),
    redis = require('redis'),
    RedisStore = require('connect-redis')(express),
    server = require('http').createServer(app);
 
app.configure(function() {
  app.use(express.cookieParser('keyboard-cat'));
  app.use(express.session({
        store: new RedisStore({
            host: process.env.REDIS_HOST || 'localhost',
            port: process.env.REDIS_PORT || 6379,
            db: process.env.REDIS_DB || 0
        }),
        cookie: {
            expires: false,
            maxAge: 30 * 24 * 60 * 60 * 1000
        }
    }));
});
 
app.get('/'function(req, res) {
  res.json({
    status: "ok"
  });
});
 
var port = process.env.HTTP_PORT || 3000;
server.listen(port);
console.log('Listening on port ' + port);

server.js 会拉取所有的依赖并启动一个特定的应用. 这个特定的应用被设定成将会话信息存储到Redis中,并暴露出一个请求端点,其会响应返回一个JSON的状态消息. 这都是非常标准的东西.

需要注意的一件事情就是针对Redis的连接信息可以使用环境变量重写——这将会在稍后从开发环境dev迁移到生产环境prod时起到作用.

最后来启动容器:

docker run -i -t --``rm 

``-p 3000:3000 

``-``v /mnt/sda1/dev:``/src 

myname/nodejs-dev:base

这里把主机上的端口 3000 转发到容器上的端口3000,把~/workspace/node目录,也就是VM里的/mnt/sda1/dev目录挂载到容器中的 /src 里面。

由于Dockerfile指定了CMD ["/bin/bash"], 容器一启动,我们就会登录到一个bash shell环境中. 如果docker运行命令执行成功了,就会像下面这样:

bash-3.2$ docker run -i -t --rm -p 3000:3000

> -v /mnt/sda1/dev:/src

> myname/nodejs-dev:base

 

[ root@a987b4f6f3f9:/src ]$ 

 

p.p12 {margin: 0.0px 0.0px 0.0px 0.0px; font: 12.0px Monaco; color: #002d7a; background-color: #f8f8ff}

当然你可以用rbenv等达到类似的效果。但我总是觉得这些工具很讨厌,因为我喜欢尽可能多地用distro-packages部署,不仅仅是因为如果这项工作的顺利进行,能使其他人更容易地使用我的代码。当拥有一个Docker容器,我需要一个不同的环境时,我仅需要“docker run”一下,几分钟便能很好的解决这个问题。当然,我也可以使用虚拟机来达到目的,但使用Docker更省时间。5. The Build Container这些天我写的代码大部分都是解释性语言,但还是有一些代价高昂的“build”步骤,我并不愿意每次都去执行它们。一个例子是为Ruby应用程序运行“bundler”。Bundler 为Rubygems更新被缓存的依赖,并且需要时间来运行一个更大的app。经常需要在应用程序运行时不必要的依赖项。例如安装依赖本地扩展gems的通常还需要很多包——通常没有记录——通过添加所有build-essential和它的依赖项就轻松启动。同时,你可以预先让bundler做所有的工作,我真的不想在主机环境中运行它,因为这可能与我部署的容器不兼容。一个解决方案是创建一个build容器。如果依赖项不同的话,你可以创建分别的Dockerfile,或者你可以重用主app Dockerfile以及重写命令运行你所需的build commands。Dockerfile如下:

开始开发

现在容器是运行起来了,在开始写代码之前,我们将需要整理出一些标准的,非docker相关的东西. 首先,要使用下面的语句启动容器里面的redis服务器:

?

1
service redis-server start

然后,要安装项目依赖和nodemon. Nodemon 会观察项目文件中的变更,并适时重启服务器.

?

1
2
npm install
npm install -g nodemon

最后,使用如下命令启动服务器:

?

1
nodemon server.js

现在,如果你在浏览器中导航到 , 你应该会看到像下面这样的东西:

图片 7

让我们来像Server.js中加入另外一个端点,以模拟开发流程:

?

1
2
3
4
5
app.get('/hello/:name'function(req, res) {
  res.json({
    hello: req.params.name
  });
});

你会看到nodemon已经侦测到了你所做的修改,并重启了服务器:

图片 8

而现在,如果你将浏览器导航到, 你会看到如下的响应:

 

图片 9

 

 

p.p13 {margin: 0.0px 0.0px 0.0px 0.0px; font: 12.0px Monaco; color: #dd2244; background-color: #f8f8ff}

FROM myappRUN apt-get updateRUN apt-get install -y build-essential [assorted dev packages for libraries]VOLUME ["/build"]WORKDIR /buildCMD ["bundler", "install","--path","vendor","--standalone"]

其他的一些坑

在使用的过程中,还遇到一些不少的坑:

  1. /etc/hosts文件无法修改,这样你就不能自己做域名解析
  2. VM的系统时间是UTC +0000的,而且貌似无法修改
  3. Container的IP无法指定为静态IP,因此每次重启Container时,IP可能会变化

 

p.p14 {margin: 0.0px 0.0px 0.0px 0.0px; font: 12.0px Monaco; color: #016fe0; background-color: #f8f8ff}

然后每当有依赖更新时,都可以运行上面的代码,同时将build/source目录挂载在容器的"/build"路径下。6. The Installation Container这不是我擅长的,但是真的值得提及。优秀的nsenter和docker-enter工具在安装时有一个选项,对于现在流行的curl | bash模式是一个很大的进步,它通过提供一个Docker容器实现“Build Container”模式。这是Dockerfile的最后部分,下载并构建一个nsenter的合适版本:

docker的限制以及后续的一些想法

docker其实还是有一些限制的:

  • 要求你的环境是Linux的,而且内核必须很新(>= 2.6.27 (29)),这其实是LXC本身的限制,和docker无关
  • docker的Container目前host是不能修改的,当然有解决方案(dnsmasq)
  • docker的Container也暂时无法指定静态IP

用docker作为开发环境甚至是生产环境其实还有很多地方值得尝试:

  • 在团队内部构建本地的仓库,标准化所有的开发环境,使得团队的新人可以快速上手
  • 在生产环境部署docker,这其实是PAAS的虚拟化和自动化的一种方式,利用LXC和Docker能够更便捷地实施PAAS
  • 尝试用docker做分布式集群模拟和测试,成本会更加低廉,更加容器维护

 

 

参考文章:

利用Docker构建开发环境

基于Docker开发nodejs应用

 

 

 

 

p.p15 {margin: 0.0px 0.0px 0.0px 0.0px; font: 12.0px Monaco; min-height: 16.0px}

ADD installer /installerCMD /installer

p.p16 {margin: 0.0px 0.0px 0.0px 0.0px; font: 12.0px Monaco; color: #b85c00; background-color: #f8f8ff}

“installer”如下:

p.p17 {margin: 0.0px 0.0px 0.0px 0.0px; font: 12.0px Monaco; color: #009999; background-color: #f8f8ff}

#!/bin/shif mountpoint -q /target; then echo "Installing nsenter to /target" cp /nsenter /target echo "Installing docker-enter to /target" cp /docker-enter /targetelse echo "/target is not a mountpoint." echo "You can either:" echo "- re-run this container with -v /usr/local/bin:/target" echo "- extract the nsenter binary (located at /nsenter)"fi

p.p19 {margin: 0.0px 0.0px 0.0px 0.0px; font: 17.0px 'Helvetica Neue'; color: #555555}

虽然可能还有恶意攻击者试图利用容器潜在的特权升级问题,但是attack surface至少显著变小。这种模式能吸引大多数人,是因为这种模式能避免开发人员在安装脚本时偶尔犯的非常危险的错误。7. The Default-Service-In-A-Box Containers当我认真对待一个app,并且相对快速的准备一个合适的容器来处理数据库等,对于这些项目,我觉得难能可贵的是已经有一系列的“基本的”基础设施容器,只做适当的调整就可以满足我的需求。当然你也可以通过“docker run”得到“主要的”部分,在Docker索引里有诸多替代品,但我喜欢首先检查它们,找出如何处理数据,然后我将修改版本添加到自己的“library”。例如Beanstalkd:

li.li1 {margin: 0.0px 0.0px 0.0px 0.0px; font: 13.0px 'Helvetica Neue'; color: #555555}

FROM debian:wheezyENV DEBIAN_FRONTEND noninteractiveRUN apt-get -q updateRUN apt-get -y install build-essentialADD a href="" /tmp/RUN cd /tmp  tar zxvf v1.9.tar.gzRUN cd /tmp/beanstalkd-1.9/  makeRUN cp /tmp/beanstalkd-1.9/beanstalkd /usr/local/bin/EXPOSE 11300CMD ["/usr/local/bin/beanstalkd","-n"]

li.li5 {margin: 0.0px 0.0px 0.0px 0.0px; font: 12.0px Monaco; color: #555555}

8. The Infrastructure / Glue Containers许多这些模式专注于开发环境,但有一个大类别缺失:容器其目的是将你的环境组合起来成为一个整体,这是目前为止对我来说有待进一步研究的领域,但我将提到一个特殊的例子:为了轻松地访问我的容器,我有一个小的haproxy容器。我有一个通配符DNS条目指向我的主服务器,和一个iptable入口开放访问我的haproxy容器。Dockerfile没什么特别的:

li.li6 {margin: 0.0px 0.0px 0.0px 0.0px; font: 12.0px Monaco; color: #aaaaaa; background-color: #f8f8ff}

FROM debian:wheezyADD wheezy-backports.list /etc/apt/sources.list.d/RUN apt-get updateRUN apt-get -y install haproxyADD haproxy.cfg /etc/haproxy/haproxy.cfgCMD ["haproxy", "-db", "-f", "/etc/haproxy/haproxy.cfg"]EXPOSE 80EXPOSE 443

li.li7 {margin: 0.0px 0.0px 0.0px 0.0px; font: 12.0px Monaco; color: #999999; background-color: #f8f8ff}

这里有趣的是haproxy.cfg

li.li18 {margin: 0.0px 0.0px 0.0px 0.0px; font: 13.0px 'Helvetica Neue'; color: #101010}

backend test acl authok http_auth(adminusers)  auth realm Hokstad if !authok server s1 192.168.0.44:8084

span.s1 {letter-spacing: 0.0px}

如果我想要特别一点,我会部署类似 AirBnB's Synapse ,但这已经超出了我的需求。在工作时扩大容器的规模,目的是让部署应用程序简单便捷,就像我正在过渡到一个完整的面向Docker的私有云系统。

span.s2 {text-decoration: underline ; letter-spacing: 0.0px; color: #101010}

span.s3 {font: 12.0px Monaco; letter-spacing: 0.0px; color: #000000}

span.s4 {letter-spacing: 0.0px; background-color: transparent}

span.s5 {color: #008080}

span.s6 {letter-spacing: 0.0px; color: #008080}

span.s7 {letter-spacing: 0.0px; color: #002d7a}

span.s8 {letter-spacing: 0.0px; color: #016fe0}

span.s9 {letter-spacing: 0.0px; color: #000000; background-color: transparent}

span.s10 {letter-spacing: 0.0px; color: #555555}

span.s11 {font: 12.0px Monaco}

span.s12 {font: 12.0px Monaco; letter-spacing: 0.0px}

span.s13 {font: 12.0px Monaco; letter-spacing: 0.0px; background-color: #f8f8ff}

span.s14 {letter-spacing: 0.0px; color: #009999}

span.s15 {letter-spacing: 0.0px; color: #323333}

span.s16 {letter-spacing: 0.0px; color: #dd2244}

span.s17 {text-decoration: underline ; letter-spacing: 0.0px}

table.t1 {border-collapse: collapse}

td.td1 {width: 26.0px; background-color: #eeeeee}

td.td2 {width: 558.0px}

td.td3 {border-style: solid; border-width: 1.0px 1.0px 1.0px 1.0px; border-color: #cbcbcb #cbcbcb #cbcbcb #cbcbcb; padding: 0.0px 5.0px 0.0px 5.0px}

td.td4 {width: 606.0px}

td.td5 {width: 637.0px}

td.td6 {width: 875.0px}

td.td7 {width: 695.0px}

td.td8 {width: 1244.0px}

ol.ol1 {list-style-type: decimal}

ul.ul1 {list-style-type: square}

最近接触PAAS相关的知识,在研发过程中开始使用Docker搭建了自己完整的开发环境,感觉生活在PAAS时代的程序员真是幸福,本文会简要介绍下Docker是什么,如何利用Docker来搭建自己的开发环境(本文主要是面向Mac OS X),以及期间所遇到的一些坑和解决方案。(本文会要求你对PAAS、LXC、CGroup、AUFS有一定的了解基础,请自行Google )

大背景–虚拟化技术历史

计算机虚拟化技术由来已久,从硬件仿真到全虚拟化,再到准虚拟化和操作系统虚拟化,各种技术粉墨登场,种类繁多,说实在的有点眼花缭乱和复杂;但用户的核心诉求一直是比较简单的,降低信息技术(IT)的运营成本,提高资源利用率,提高安全性和可靠性等等;虽说用户的核心诉求比较简单,但每个时代的需求场景却是不同的。在大型机时代,虚拟化技术被用来支持多个用户能够同时使用大型机,在x86架构时代,随着企业服务的大规模部署,虚拟化技术主要是用来提高企业资源的利用率,而现如今,随着云计算时代的到来,人们对应用的安全性、隔离性越来越高,对于部署的标准化以及虚拟机的性能要求越来越高。现如今,一种叫Linux容器的虚拟化技术逐渐得到广泛的应用,它的优点有许多,本文不一一赘述,有太多的文章可以参考。

什么是Docker?

docker的英文本意是码头工人,也就是搬运工,这种搬运工搬运的是集装箱(Container),集装箱里面装的可不是商品货物,而是任意类型的App,Docker把App(叫Payload)装在Container内,通过Linux Container技术的包装将App变成一种标准化的、可移植的、自管理的组件,这种组件可以在你的latop上开发、调试、运行,最终非常方便和一致地运行在production环境下。

Docker的核心底层技术是LXC(Linux Container),Docker在其上面加了薄薄的一层,添加了许多有用的功能。这篇stackoverflow上的问题和答案很好地诠释了Docker和LXC的区别,能够让你更好的了解什么是Docker, 简单翻译下就是以下几点:

Docker提供了一种可移植的配置标准化机制,允许你一致性地在不同的机器上运行同一个Container;而LXC本身可能因为不同机器的不同配置而无法方便地移植运行;

Docker以App为中心,为应用的部署做了很多优化,而LXC的帮助脚本主要是聚焦于如何机器启动地更快和耗更少的内存;

Docker为App提供了一种自动化构建机制(Dockerfile),包括打包,基础设施依赖管理和安装等等;

Docker提供了一种类似git的Container版本化的机制,允许你对你创建过的容器进行版本管理,依靠这种机制,你还可以下载别人创建的Container,甚至像git那样进行合并;

Docker Container是可重用的,依赖于版本化机制,你很容易重用别人的Container(叫Image),作为基础版本进行扩展;

Docker Container是可共享的,有点类似github一样,Docker有自己的INDEX,你可以创建自己的Docker用户并上传和下载Docker Image;

Docker提供了很多的工具链,形成了一个生态系统;这些工具的目标是自动化、个性化和集成化,包括对PAAS平台的支持等;

那么Docker有什么用呢?对于运维来说,Docker提供了一种可移植的标准化部署过程,使得规模化、自动化、异构化的部署成为可能甚至是轻松简单的事情;而对于开发者来说,Docker提供了一种开发环境的管理方法,包括映像、构建、共享等功能,而后者是本文的主题。

Docker的安装和构成

Docker官方本身提供了非常具体的安装教程,这里不说具体的安装过程,请参考Docker安装(Mac系统),重要的是描述下原理和安装完成后的结构,好对Docker更好的了解。 由于LXC本身不支持Mac内核,因此需要跑一个VirtualBox虚拟机(TinyCoreLinux)来安装,幸好Docker社区提供了一个非常方便的工具boot2docker(其实就是一个VBoxManage的包装shell脚本),用于安装Mac下的整个Docker环境。具体的结构如下:

如图所示,安装完成后,具体情况如下:

在Mac的home目录~/.boot2docker下创建了虚拟机所需要的文件,其中boot2docker.iso是虚拟机映像,这是一个由CD-ROM引导的TinyCoreLinux系统;而boot2docker-vm.vmdk文件则是你的虚拟机磁盘,你所有的持久化数据都存放在这里,包括docker创建的lxc容器等文件。

在Mac下,docker被分为客户端docker-client和服务端docker-daemon两部分,如果是在linux(比如ubuntu),实际上则是同一个可执行文件同时充当客户端和服务端。docker-daemon可以监听unix scoket,也可以在tcp socket(默认端口为4234),docker-client会通过一个叫DOCKER_HOST的环境变量读取服务地址和端口,因此你应该在你的bash_profile文件里面添加这么一行:

Crayon Syntax Highlighter v2.6.7 export DOCKER_HOST=tcp://127.0.0.1:4243

1

2

export DOCKER_HOST=tcp://127.0.0.1:4243

 

[Format Time: 0.0033 seconds] 

docker-daemon跑在虚拟机上,这个程序实际上就是接收docker-client发送过来的消息命令,创建、启动和销毁lxc容器,以及docker本身的版本管理、映像存储等等 运行你的第一个docker容器 安装完成后,就差不多可以开始创建和运行docker容器了,在这之前,你首先得下载一个Image,什么是Image?我们先来了解docker的2个基础概念:Image和Container。

Container和Image 在Docker的世界里,Image是指一个只读的层(Layer),这里的层是AUFS里的概念,最直观的方式就是看一下docker官方给出的图:

Docker使用了一种叫AUFS的文件系统,这种文件系统可以让你一层一层地叠加修改你的文件,最底下的文件系统是只读的,如果需要修改文件,AUFS会增加一个可写的层(Layer),这样有很多好处,例如不同的Container可以共享底层的只读文件系统(同一个Kernel),使得你可以跑N多个Container而不至于你的硬盘被挤爆了!这个只读的层就是Image!而如你所看到的,一个可写的层就是Container。

那Image和Container的区别是什么?很简单,他们的区别仅仅是一个是只读的层,一个是可写的层,你可以使用docker commit 命令,将你的Container变成一个Image,也就是提交你所运行的Container的修改内容,变成一个新的只读的Image,这非常类似于git commit命令,感觉真棒!

实际上这就是Docker对Container映像的版本管理基石,AUFS文件系统实在是太美妙了,更多细节可以参考DotCloud的这篇文章。

运行和退出

在了解了Image和Container的概念后,我们可以开始下载一个Image,Docker的好处就是提供了一个类似github的Image仓库管理,你可以非常方便pull别人的Image下来运行,例如,我们可以下载一个ubuntu Image:

Crayon Syntax Highlighter v2.6.7

docker pull ubuntu:13.10 

1

2

docker pull ubuntu:13.10 

 

[Format Time: 0.0008 seconds]

这里的13.10是一个Tag,类似于git的tag,这里的tag可以为你制定一个ubuntu的版本。下载完成后,执行docker images命令可以列出你已经下载或者自己构建的image:(请允许我使用可爱的马赛克 :) )

你可以看到ubuntu:13.10的大小为178MB,以及它的IMAGE ID。 现在我们开始运行一个Container,命令很简单,例如我们想运行一个执行Shell终端的Container:

如你看到的,你已经进入到一个Shell里面,可以执行你想执行的任何命令,就和在ubuntu里面一样,进去后默认是在根目录/下,可以看到经典的unix/linux目录结构,以及你所运行的bash版本等信息。你可以给你的Container定一个名字,通过–name选项,例如这里命名了shell,日后你就可以直接用这个名字引用Contanier。

退出一个Container也很简单,你直接exit就好了。 其他更多的命令这里不做赘述,因为官方的文档已经非常全面,这里只是给一个直观的初步印象。下面进入主题。

利用Docker搭建开发环境

我们先看看程序员在搭建开发环境时遇到的一些问题:

软件安装麻烦,比如很多公司都使用redhat,一般开发人员又不给root,安装一个nginx或者是mysql都得自己下载编译安装 权限问题,没有root,一些软件无法运行,例如dnsmasq;

没有root,无法修改hosts,无法netstat -nptl,无法tcpdump,无法iptable

隔离性差,例如不同的开发人员如果在同一台主机环境下共享开发,虽然是用户隔离,但端口如果不规范可能会冲突;同一个Mysql如果权限管理不好很有可能误删别人的数据

可移植性差,例如和生产环境不一致,开发人员之间也无法共享;更严重的情况是当有新人入职时,通常需要又折腾一遍开发环境,无法快速搭建

这些问题可以通过在本地搭建虚拟机来解决,但虚拟机是一个很笨重的解决方案,Docker是一个非常轻量级的方案,而且还拥有虚拟机没有的一些功能,例如标准化Image,Image共享等,更重要的是,利用Docker,你可以运行非常多的容器,在你的Mac下搭建一个分布式的开发环境根本不是什么大的问题,而且对内存、磁盘和cpu的消耗相比传统的虚拟机要低许多,这些都要归功于AUFS和LXC这两大神奇的技术。

构建基础Image

想要搭建一个节省磁盘空间和扩展性良好的开发环境,最重要的第一步就是构建一个基础性的Image,比如你的主要开发语言是Ruby,那么你肯定需要一个已经安装好以下工具的基础Image:

ruby

bundler

gem

然后在此基础上,你可以扩展这个基础的Image(下面叫base)为不同的开发环境,例如rails,或者是nats。当然,你的这个base也可以从别人的Image扩展而来,还记得我们刚刚pull下来的ubuntu:13.10这个Image吗?你可以从这个Image扩展开始构建你的base,如何做呢?Docker提供了一种标准化的DSL方式,你只需要编写一个Dockerfile,运行docker build指令,就可以构建你自己的Image,这有点像Makefile和make命令一样,只是大家要构建的内容和构建语言不同。

Dockerfile的语法请参考Dockerfile Reference,这里给出上面提到的Ruby开发的base Dockerfile示例:

Crayon Syntax Highlighter v2.6.7

FROM ubuntu:13.10 

RUN apt-get update 

RUN apt-get install -y ruby ruby-dev gem 

RUN gem install bundler 

1

2

3

4

5

FROM ubuntu:13.10 

RUN apt-get update 

RUN apt-get install -y ruby ruby-dev gem 

RUN gem install bundler 

 

[Format Time: 0.0020 seconds]

这里只用到了很简单的2个指令:FROM和RUN,FROM指定了我们要扩展的Image,RUN指定我们要运行的命令,这里是安装ruby,gem、bundler等软件。写好Dockerfile后,运行以下指令就可以创建你的base image了:

Crayon Syntax Highlighter v2.6.7

docker build --rm -t dev:base . 

1

2

docker build --rm -t dev:base . 

 

[Format Time: 0.0010 seconds]

-t 选项是你要构建的base image的tag,就好比ubuntu:13.10一样 –rm 选项是告诉Docker在构建完成后删除临时的Container,Dockerfile的每一行指令都会创建一个临时的Container,一般你是不需要这些临时生成的Container的 如你所想,我们可以像运行ubuntu:13.10那样运行我们的base了:

Crayon Syntax Highlighter v2.6.7

docker run -i -t --name ruby dev:base irb 

1

2

docker run -i -t --name ruby dev:base irb 

 

[Format Time: 0.0011 seconds]

这里我们使用dev:base这个Image运行了一个irb解释器(Ruby的交互式解释器)。 在构建完base之后,你可以依样画葫芦构建你的rails环境,很简单,只需要FROM dev:base,然后RUN安装你的rails组件就可以了,不再赘述。最终你可能构建的开发环境是这样的:

如上图所示,base和service都是从ubutnu:13.10继承而来,他们作为不同的基础开发环境,base是ruby开发环境(也许命名为dev:ruby更为合适?),而service是一些基础数据服务,例如mysql,memcache,我建议将这些第三方组件集中在一个Container中,因为他们的环境不经常修改,可以作为一种底层服务Container运行,除非你需要构建分布式的服务,例如memcache集群,那可以继续拆分。

指定Image入口

当你构建完你的base Image和其他应用的Image之后,你就可以启动这些Image了,还记得前面我们给出的运行命令吗?

Crayon Syntax Highlighter v2.6.7

docker run -i -t --name shell dev:base /bin/bash 

下一篇:没有了