Skip to content

Instantly share code, notes, and snippets.

@labianchin
Created May 3, 2016 01:56
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save labianchin/b24282c3359c2b8cd35959b573eb30b1 to your computer and use it in GitHub Desktop.
Save labianchin/b24282c3359c2b8cd35959b573eb30b1 to your computer and use it in GitHub Desktop.

layout: post category : tips tagline: "Some tips about running and building with docker" tags : [docker,tips,tools,devops]

Você já sabe o básico de Docker, mas quando começa a usar com mais frequência, alguns desconfortos surgem. Como qualquer ferramenta, Docker tem seu próprio conjunto de boas práticas e dicas para ser efetivo.

Venho usado Docker a um bom tempo e tenho notado que uso e busco algumas dicas e práticas com frequencia. O objetivo dessse texto é docuemtnar dicas, truques e boas práticas que colhi ao longo de minha jordade de apredizado e uso de Docker. Note que a informação aqui é bem densa, então talvez você queira voltar e se aprofundar em algo quando for necessário.

Esse documento está separado em duas seções: Dicas para rodar (docker run) e boas práticas de construção de imagens (docker build/Dockerfile).

Dicas para Rodar

Lembre-se cada docker run cria um novo container com a imagem especificada e inicia um comando dentro dele (CMD especificado no Dockerfile). É comum que, principalmente em desenvolvimento, criarmos e rodarmos vários containers que são descartados em breve. Nesse caso de usarmos containers verdadeiramente efêmeros, utilize o argumento --rm. Isso faz com que todos dados do container sejam removidos após o termino da execução, evitando consumir disco de maneira desnecessária.

Em geral, pode-se utilizar o comando run conforme o exemplo:

docker run --rm -it debian /bin/bash

Note aqui que -it significa --interactive --tty. Ele é usado para fixar a linha de comando com o container, assim após esse docker run todos comandos serão executados pelo bash de dentro do container. Para sair use exit ou pressione Control-d. Esses parâmetros são muito úteis para executar um container em primeiro plano.

Verifique variáveis de ambiente

Às vezes se faz necessário verificar que metadados estão definidos como variáveis de ambiente em uma imagem. Use o comando env para obter esta informação:

docker run --rm -it debian env

Para verificar as variáveis de ambiente passados de um container já criado:

{% raw %}

docker inspect --format '{{.Config.Env}}' <container>

{% endraw %}

Para outros metadados, use variações do comando docker inspect.

Logs

Docker captura logs da saída padrão (STDOUT) e saída de erros (STDERR). Esses registros podem ser roteados para diferentes sistemas (syslog, fluentd, ...) que podem ser especificado através da configuração de driver --log-driver=VALUE no comando docker run. Quando utilizado o driver padrão json-file (e também journald), pode-se utilizar o seguinte comando para recuperar os logs:

docker logs -f <container_name>

Observe também o argumento -f para acompanhar as próximas mensagens de log. Quando quiser parar, pressione Ctrl-c .

Backup

Dados em containers Docker são expostos e compartilhados através de argumentos de volumes utlizados ao criar e iniciar o container. Outra boa abordagem é armazenar dados dentro de Volume Containers. De qualquer forma, em algums momento se faz necessário mover dados de um servidor (Docker host) para outro. Além de que uma boa prática de administração de sistema é fazer cópias de segurança (backups) periodicamente.

Para fazer backup (extrair dados), use comando a seguir:

docker run --rm -v /tmp:/backup --volumes-from <container-name> busybox tar -cvf /backup/backup.tar <path-to-data>

Para restaurar utlize:

docker run --rm -v /tmp:/backup --volumes-from <container-name> busybox tar -xvf /backup/backup.tar <path-to-data>

Mais informação também pode ser encontrada nessa (minha) resposta StackOverflow answer, onde também é possível encontrar alguns aliases para esses dois comandos. Esse aliases também estão disponíveis abaixo na seção Aliases.

Outras fontes são:

Use docker exec para "entrar num container"

Eventualmente é necessário entrar em um container em execução afim de verificar algum problema, efetuar testes ou simplemente depurar (debug). Nunca instale o daemon SSH em um container docker. Use docker exec para entrar em um container e rodar um comando:

docker exec -it ubuntu_bash bash

Essa funcionalidade é muito útil em desenvolvimento local e experimentações. Mas evite utiliza-lo em produção ou automatizar ferramentas em volta dele.

Verifique a documentação do mesmo: https://docs.docker.com/engine/reference/commandline/exec/

Sem espaço em disco restante

Ao executar containers e construir imagens várias vezes, o espaço em disco pode tornar-se escassos. Quando isso acontece, torna-se necessário limpar alguns containers, imagens e logs.

Uma maneira rápida de limpar containers e imagens é utilizar os seguintes comandos:

docker ps -aq | xargs docker rm # para remover todos os containers
docker images -aq -f dangling=true | xargs docker rmi # para remover imagens não utilizadas

Dependendo do tipo de apliacção, logs também podem ser volumosos. O seu gerenciamento depende muito de qual driver está sendo utilizado. No driver padrão (json-file) a limpeza pode ser feita através da execução do seguinte comando dentro do Docker host:

{% raw %}

echo "" > $(docker inspect --format='{{.LogPath}}' <container_name_or_id>)

{% endraw %}

Aliases

Use esses aliases no seu .zshrc ou .bashrc para limpar imagens e containers, fazer backup e restauração, etc.

{% raw %}

alias dockercleancontainers="docker ps -aq | xargs docker rm"
alias dockercleanimages="docker images -aq -f dangling=true | xargs docker rmi"
alias dockerclean="dockercleancontainers && dockercleanimages"
alias docker-killall="docker ps -q | xargs docker kill"

# runs docker exec in the latest container
function docker-exec-last {
  docker exec -ti $( docker ps -a -q -l) /bin/bash
}

function docker-get-ip {
  # Usage: docker-get-ip (name or sha)
  [ -n "$1" ] && docker inspect --format "{{ .NetworkSettings.IPAddress }}" $1
}

function docker-get-id {
  # Usage: docker-get-id (friendly-name)
  [ -n "$1" ] && docker inspect --format "{{ .ID }}" "$1"
}

function docker-get-image {
  # Usage: docker-get-image (friendly-name)
  [ -n "$1" ] && docker inspect --format "{{ .Image }}" "$1"
}

function docker-get-state {
  # Usage: docker-get-state (friendly-name)
  [ -n "$1" ] && docker inspect --format "{{ .State.Running }}" "$1"
}

function docker-memory {
  for line in `docker ps | awk '{print $1}' | grep -v CONTAINER`; do docker ps | grep $line | awk '{printf $NF" "}' && echo $(( `cat /sys/fs/cgroup/memory/docker/$line*/memory.usage_in_bytes` / 1024 / 1024 ))MB ; done
}
# keeps the commmand history when running a container
function basher() {
    if [[ $1 = 'run' ]]
    then
        shift
        docker run -e HIST_FILE=/root/.bash_history -v $HOME/.bash_history:/root/.bash_history "$@"
    else
        docker "$@"
    fi
}
# backup files from a docker volume into /tmp/backup.tar.gz
function docker-volume-backup-compressed() {
  docker run --rm -v /tmp:/backup --volumes-from "$1" debian:jessie tar -czvf /backup/backup.tar.gz "${@:2}"
}
# restore files from /tmp/backup.tar.gz into a docker volume
function docker-volume-restore-compressed() {
  docker run --rm -v /tmp:/backup --volumes-from "$1" debian:jessie tar -xzvf /backup/backup.tar.gz "${@:2}"
  echo "Double checking files..."
  docker run --rm -v /tmp:/backup --volumes-from "$1" debian:jessie ls -lh "${@:2}"
}
# backup files from a docker volume into /tmp/backup.tar
function docker-volume-backup() {
  docker run --rm -v /tmp:/backup --volumes-from "$1" busybox tar -cvf /backup/backup.tar "${@:2}"
}
# restore files from /tmp/backup.tar into a docker volume
function docker-volume-restore() {
  docker run --rm -v /tmp:/backup --volumes-from "$1" busybox tar -xvf /backup/backup.tar "${@:2}"
  echo "Double checking files..."
  docker run --rm -v /tmp:/backup --volumes-from "$1" busybox ls -lh "${@:2}"
}

{% endraw %}

Fontes:

Boas práticas para construção de imagens

Em Docker, as imagens são tradicionalmente construídas usando um arquivo Dockerfile. Existem alguns bons guias sobre as melhores práticas para construir imagens docker. Recomendo dar uma olhada neles:

Use um "linter"

Um "linter" é uma ferramenta que fornece dicas e avisos sobre algum código fonte. Para Dockerfile existem algumas opções simples, mas ainda é um espaço muito novo e que muito tem a evoluir.

Muitas opções foram discutidas aqui: https://stackoverflow.com/questions/28182047/is-there-a-way-to-lint-the-dockerfile

Desde Janeiro de 2016, o mais completo parece ser hadolint. Disponível em duas versões: on-line e terminal. O interessante dessa ferramente é que usa o maduro Shell Check para validar os comandos shell.

O básico

O container produzido pela imagem do Dockerfile deve ser o máximo possível efêmero. Isso significa que pode ser parado, destruído e substituído por um novo container construído com o minimo de esforço.

As vezes é comum colocar outros arquivos, como documentação, no diretório junto ao Dockerfile. Para melhorar a performance de contrução, exclua arquivos e diretórios criando um arquivo .dockerignore no mesmo diretório. Esse arquivo funciona de maneira semelhante ao .gitignore. Usa-lo ajuda a minimizar o contexto de contrução que é enviado docker host a cada docker build.

Evite adicionar pacotes e dependências extras e não necessárias a sua aplicação. Isso minimiza a complexidade, tamanho da imagem, tempo de contrução e superficie de ataque.

Minimize o número de camadas, sempre que possível agrupe vários comandos. Porém também leve em conta a volatilidade e manutenção dessas camadas.

Na maioria dos casos, rode apenas um único processo por container. Desacoplando aplicações em vários containers facilida a escalabilidade horizontal, reuso e monitoramento dos containers.

Prefira COPY ao invés de ADD

O comando ADD existe desde o início do Docker. É muito versátil e permite alguns truques além de simplesmente copiar arquivos do contexto de contrução, o que o torna muito mágico e difícil de entender. Ele permite baixar arquivos de urls e automaticamente extrair arquivos de formatos conhecidos (tar, gzip, bzip2, etc.).

Por outro lado, COPY é um comando muito mais simples para inserir arquivos e pastas do caminho de construção para dentro da imagem Docker. Assim, favoreça COPY a menos que tenha certeza absoluta que ADD é necessário.

Mais detalhes aqui: https://labs.ctl.io/dockerfile-add-vs-copy/

Execute um "checksum" depois de baixar e antes de usar o arquivo

Em vez de usar ADD para baixar e adicionar arquivos à imagem, favoreça a utilização de curl e então a verificação através de um checksum após o download. Isso permite garantir que o arquivo é o esperado e não poderá variar ao longo do tempo. Se o arquivo que a URL aponta mudar, o checksum irá mudar e a contrução da imagem irá falhar. Isso é importante pois favorece a reproducibilidade e a segurança na contrução de imagens.

Um bom exemplo para se inspirar é o Dockerfile oficial do Jenkins:

ENV JENKINS_VERSION 1.625.3
ENV JENKINS_SHA 537d910f541c25a23499b222ccd37ca25e074a0c

RUN curl -fL http://mirrors.jenkins-ci.org/war-stable/$JENKINS_VERSION/jenkins.war -o /usr/share/jenkins/jenkins.war \
  && echo "$JENKINS_SHA /usr/share/jenkins/jenkins.war" | sha1sum -c -

Use uma imagem de base mínima

Sempre que possível utilize imagens oficiais como base para sua imagem. Por padrão, recomenda-se usar a imagem debian que é muito bem controlada e mantida mínima (por volta de 150 mb). Lembre-se também usar tags específicas, por exemplo debian:jessie .

Se mais ferramentas e dependências são necessários, olhe por imagens como buildpack-deps.

Porém, caso debian ainda seja muito grande, existem imagens minimalistas como alpine ou mesmo busybox. Evite alpine se DNS é necessário, existem alguns problemas a serem resolvidos. Além disso, evite-o para linguagens que usam o GCC, como Ruby, Node, Python, etc, isso é porque alpine utiliza libc MUSL que pode produzir binários diferentes.

Evite imagens gigantes como phusion/baseimage. Essa uma imagem é muito grande, que foge da filosofia de um processo por container e muitas das coisas que inclui não são essenciais para containers Docker, veja mais aqui .

Outras fontes: http://www.iron.io/microcontainers-tiny-portable-containers/

Use o cache de construção de camadas

Uma funcionalidade muito útil que o uso de Dockerfile proporciona é as reconstruções rápidas usando o cache de camadas. A fim de aproveitar esse recurso, coloque ferramentas e dependências que mudam com menos frequência no topo do Dockerfile.

Por exemplo, considere instalar as dependências de código antes de adicionar o código. No caso de NodeJS:

COPY package.json /app/
RUN npm install
COPY . /app

Mais sobre isso: http://bitjudo.com/blog/2014/03/13/building-efficient-dockerfiles-node-dot-js/

Limpe na mesma camada

Ao usar um gerenciador de pacotes para instalar algum software, é uma boa prática limpar o cache gerado pelo gerenciador de pacotes logo após a instalação das dependências. Por exemplo, ao usar apt-get:

RUN apt-get update && \
    apt-get install -y curl python-pip && \
    pip install requests && \
    apt-get remove -y python-pip curl && \
    rm -rf /var/lib/apt/lists/*

Em geral deve-se limpar o cache do apt (gerado por apt-get update) através da remoção de /var/lib/apt/lists. Isso ajuda a manter o tamanho da imagem pequeno. Além disso, observe aqui, que pip e curl também são removidos uma vez que não são necessários para a aplicação de produção. Lembre-se que a limpeza precisa ser feito na mesma camada (comando RUN). Caso contrário os dados serão persistidos nessa camada, e removê-lo mais tarde não terá efeito no tamanho da imagem final.

Note que, segundo a documentação, as imagens oficiais de Debian e Ubuntu rodam autoamaticamente apt-get clean, logo a invocação explicita não é necesária.

Evite rodar apt-get upgrade our dist-upgrade, já que várias pacotes da imagem base não vão atualizar dentro de um container desprovido de privilégios. Se há um pacote em específico a ser atualizado, simplesmete use apt-get install -y foo para atualizar automaticamente.

Mais sobre isso:

http://blog.replicated.com/2016/02/05/refactoring-a-dockerfile-for-image-size/ https://docs.docker.com/engine/userguide/eng-image/dockerfile_best-practices/#apt-get

Use um script "wrapper" como ENTRYPOINT, as vezes

Um script wrapper pode ajudar ao tomar a configuração do ambiente e definir a configuração do aplicativo. Ele pode até mesmo definir configurações padrões quando não disponíveis.

Um ótimo exemplo é fornecido no artigo de Kelsey Hightower: 12 Fracturated Apps:

#!/bin/sh
set -e
datadir=${APP_DATADIR:="/var/lib/data"}
host=${APP_HOST:="127.0.0.1"}
port=${APP_PORT:="3306"}
username=${APP_USERNAME:=""}
password=${APP_PASSWORD:=""}
database=${APP_DATABASE:=""}
cat <<EOF > /etc/config.json
{
  "datadir": "${datadir}",
  "host": "${host}",
  "port": "${port}",
  "username": "${username}",
  "password": "${password}",
  "database": "${database}"
}
EOF
mkdir -p ${APP_DATADIR}
exec "/app"

Note, sempre use exec em scripts shell que envolvem a aplicação. Desta forma, a aplicação pode receber sinais Unix.

Considere também usar um sistema de inicialização simples (e.g. dumb init) como a CMD base, desta forma os sinais do Unix podem ser devidamente tratados. Leia mais sobre isso aqui: http://engineeringblog.yelp.com/2016/01/dumb-init-an-init-for-docker.html

Log para stdout

Aplicações dentro de Docker devem emitir logs para stdout. Porém algumas aplicações escrevem esses logs em arquivos. Nestes casos, a solução é criar um symlink do arquivo para stdout.

Um exemplo é o Dockerfile do nginx:

# forward request and error logs to docker log collector
RUN ln -sf /dev/stdout /var/log/nginx/access.log
RUN ln -sf /dev/stderr /var/log/nginx/error.log

Mais sobre isso: https://serverfault.com/questions/599103/make-a-docker-application-write-to-stdout

Cuidado ao adicionar dados num volume no Dockerfile

Lembre de usar a intrução VOLUME para expor dados de bancos de dados, configuração or arquivos e pastas criados pelo container. Use para qualquer dados mutável e partes que são servidas ao usário da sua imagem.

Evite adicionar muitos dados para uma pasta e, em seguida, transformá-lo em um VOLUME. A inicialização de containers com base nessa imagem pode ser lento. Pois, ao criar container, os dados serão copiados da imagem para o volume montado.

Além disso, ainda em tempo de compilação, não adicione dados para caminhos que tenham sido previamente declarados como VOLUME. Isso não irá funcionar, os dados não serão persistidos pois dados em volumes não são comitados em imagens.

Leia mais sobre isso aqui na explicação de Jérôme Petazzoni.

EXPOSE de portas

Docker favorece reproducibilidade e portabilidade. Imagens devem ser capazes de rodar em qualquer servidor e quantas vezes forem necessárias. Dessa forma, nunca exponha portas públicas. Porém exponha, de maneira privada, as portas padrões da sua aplicação.

# mapeamento publico e privado, evite isso
EXPOSE 80:8080

# apenas privado
EXPOSE 80

A adicionar

  • Sort multi-line arguments
  • Version pinning
  • ENTRYPOINT VS CMD
  • Users: RUN groupadd -r postgres && useradd -r -g postgres postgres

Conclusão

Essa foi a minha lista de dicas e truques para Docker. Por favor, sinta-se livre para fornecer feedback e partilhar as suas dicas e truques nos comentários abaixo.

Eu também gostaria de agradecer a Bruno Tavares e Adriano Bonat pela revisão de versões anteriores desse artigo.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment