Search This Blog

Showing posts with label Dockerfile. Show all posts
Showing posts with label Dockerfile. Show all posts

Tuesday, 29 May 2018

Bootstrapping a database for use with containerized applications (PostgreSQL, MongoDB etc)

PROBLEM

Developing a dockerized microservice that requires access to a dockerized database with a more complex custom setup (a new user with particular permissions, new database with tables etc).

SOLUTION

Docker allows to perform additional initialization during the database bootstrapping. It works through extending the base registry database image with init scripts, placed in the /docker-entrypoint-initdb.d.  Docker first runs the default  docker-entrypoint.sh script, then all scripts under the /docker-entrypoint-initdb.d directory.

Creating custom PostgreSQL image


Dockerfile

FROM postgres:alpine
ADD ./init/* /docker-entrypoint-initdb.d/
.
├── mongodb
│   └── init
├── nats
├── networking.md
├── postgres
│   ├── Dockerfile
│   ├── init
│   │   └── 01-author-setup.sh
│   ├── Makefile
│   └── README.md
└── README.md

Our local ./init dir contains the following bash script creating a database, a custom user with granted privileges to the created database and (optionally) creating a table.

01-author-setup-sh

#!/usr/bin/env bash

# Credits: based on https://medium.com/@beld_pro/quick-tip-creating-a-postgresql-container-with-default-user-and-password-8bb2adb82342

# This script is used to initialize postgres, after it started running,
# to provide the database(s) and table(s) expected by a connecting
# application.

# In this case, postgres is used by a author-service microservice,
# which expects access to:

#   - database called publication_manager
#   - within it a table called author

#   * db user with approriate privileges to the database

set -o errexit

PUBLICATION_MANAGER_DB=${PUBLICATION_MANAGER_DB:-publication_manager}
AUTHOR_DB_TABLE=${AUTHOR_DB_TABLE:-authors}
AUTHOR_DB_USER=${AUTHOR_DB_USER:-author_user}
AUTHOR_DB_PASSWORD=${AUTHOR_DB_PASSWORD:-authorpass}
POSTGRES_USER=${POSTGRES_USER:-postgres}

# By default POSTGRES_PASSWORD is an empty string. For security reasons it is advisable
# to set set it up when we start running the container:
#
#   docker run --rm -e POSTGRES_PASSWORD=mypass -p 5432:5432 -d --name author_postgres author_postgres
#   psql -h localhost -p 5432 -U postgres

#       Note that unlike in MySQL, psql does not provide a flag for providing password.
#       The password is provided interactively.
#       The PostgreSQL image sets up trust authentication locally, so password is not required
#       when connecting from localhost (inside the same container). Ie. psql in this script, 
#       that runs after Postgres starts, does not need the authentication. 

POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-}

# Debug ----------------------------------------------------
echo "==> POSTGRES_USER ... $POSTGRES_USER"
echo "==> POSTGRES_DB ... $POSTGRES_DB"
echo "==> PUBLICATION_MANAGER_DB ... $PUBLICATION_MANAGER_DB"
echo "==> AUTHOR_DB_USER ... $AUTHOR_DB_USER"
echo "==> AUTHOR_DB_PASSWORD ... [$AUTHOR_DB_PASSWORD]"
echo "==> AUTHOR_DB_TABLE ... $AUTHOR_DB_TABLE"
echo "==> POSTGRES_PASSWORD = [$POSTGRES_PASSWORD]"
# ----------------------------------------------------------

# What environment variables need to be set up.
#   Environment variable defaults are set up in this case, 
#   however we want to ensure the defaults are not accidentally
#   removed from this file causing a problem.
readonly REQUIRED_ENV_VARS=(
  "PUBLICATION_MANAGER_DB"
  "AUTHOR_DB_USER"
  "AUTHOR_DB_PASSWORD"
  "AUTHOR_DB_TABLE")

# Main execution:
# - verifies all environment variables are set
# - runs SQL code to create user and database
# - runs SQL code to create table
main() {
  check_env_vars_set
  init_user_and_db

  # Comment out if wanting to use the author-service uses gorm AutoMigrate feature:
  #   the gorm AutoMigrate feature creates extra columns (xxx_unrecognized, xxx_sizecache)
  #   based on the proto message, which are required for proto messages transactions
  #   to work with the table
  # init_db_tables
}

# ----------------------------------------------------------
# HELPER FUNCTIONS

# Check if all of the required environment
# variables are set
check_env_vars_set() {
  for required_env_var in ${REQUIRED_ENV_VARS[@]}; do
    if [[ -z "${!required_env_var}" ]]; then
      echo "Error:
    Environment variable '$required_env_var' not set.
    Make sure you have the following environment variables set:
      ${REQUIRED_ENV_VARS[@]}
Aborting."
      exit 1
    fi
  done
}

# Perform initialization in the already-started PostgreSQL
#   - create the database
#   - set up user for the author-service database:
#         this user needs to be able to create a table,
#         to insert/update and delete records
init_user_and_db() {
  psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" <<-EOSQL
     CREATE DATABASE $PUBLICATION_MANAGER_DB;
     CREATE USER $AUTHOR_DB_USER WITH PASSWORD '$AUTHOR_DB_PASSWORD';
     GRANT ALL PRIVILEGES ON DATABASE $PUBLICATION_MANAGER_DB TO $AUTHOR_DB_USER;
EOSQL
}

#   - create database tables
init_db_tables() {
  psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" "$PUBLICATION_MANAGER_DB" <<-EOSQL
    CREATE TABLE $AUTHOR_DB_TABLE(
    ID             CHAR VARYING(60) PRIMARY KEY NOT NULL,
    FIRST_NAME     CHAR VARYING(40) NOT NULL,
    LAST_NAME      CHAR VARYING(60) NOT NULL,
    ADDRESS        CHAR(100),
    COUNTRY        CHAR(70),
    EMAIL          CHAR(70),
    PASSWORD       CHAR VARYING(50),
    TOKEN          TEXT
);
EOSQL
}

# Executes the main routine with environment variables
# passed through the command line. Added for completeness 
# as not used here.
main "$@"

Makefile

build:
 docker build -t author-postgres .

run:
 docker run --rm  -p 5432:5432 --network=pm-net-bridge --name author-postgres author-postgres

The Docker run command above also attaches the running postgres container to a custom bridge so that a microservice, attached to the same bridge, can connect to the database.

LINKS


https://github.com/tamarakaufler/grpc-publication-manager
https://github.com/tamarakaufler/go-calculate-for-me


Saturday, 12 May 2018

Dockerization of Go web applications using alpine base image on Ubuntu 16.04

Using the attractively small alpine image as a base for dockerizing a Go web application on an Ubuntu host, does not work if the application binary is built using the traditional:

GOOS=linux GOARCH=amd64  go build


FROM alpine:latest

RUN mkdir /app
WORKDIR /app
COPY service /app
EXPOSE 50051
ENTRYPOINT ["./service"]

The image builds without a problem, but running a container:

docker run -p 50051:50051 author-service

results in the following error:

standard_init_linux.go:178: exec user process caused "no such file or directory"

The reason behind this lies in the alpine OS missing dependencies that the Go application by default needs. In particular it is the Go net package, that by default requires cgo to enable dynamic runtime linking to C libraries, which the alpine does not provide.

SOLUTION


a)
On some systems (like Ubuntu) it is possible to use the network without cgo, so we can use the the CGO_ENABLED flag to change the default:


CGO_ENABLED=0 GOOS=linux GOARCH=amd64  go build .

b)
Alternatively it is possible to enforce using Go implementation of net dependencies, netgo: 

GOOS=linux GOARCH=amd64  go build -tags netgo -a -v .

c)
If you for any reason don't want to use either of the above, just use debien instead of alpine.


Wednesday, 10 May 2017

Dockerization of a Golang application

There are several possibilities how to write a Dockerfile for a Golang application. Which one to choose depends on dependencies the application needs.

If the application uses just built in packages, it is possible to have a very minimalistic Dockerfile.


             FROM scratch

             COPY ./goaws /

             CMD ["/goaws"]


Using scratch means we are not basing our image on anything, there is no operating system. This is possible if we do not need to perform any shell operations that require coperation of the OS like mkdir etc.

-------------------------------------------------------------------------------------------------

If there are non built in dependencies, there there are several approaches possible.

a) The first approach bases the new Docker image on an official golang one, downloads all the dependencies, builds the Go binary and starts the application.

There are several gotchas to watch out for:

The application dependencies, whether internal (packages provided within the application) or external (3rd party packages from the Go public repository) need to be in the right place for the build to be able to find them. The paths searched depend on $GOPATH and $GOROOT environment variables. In the golang:1.8 image OS, the $GOPATH is /go. I created a /go/src/github.com/tamarakaufler/goaws directory which allows the Go compiler to find all internal packages during the build. The go get command will install the external packages and we are good to go (excuse the pun).


             FROM golang:1.8

             RUN mkdir -p /go/src/github.com/tamarakaufler/goaws

             COPY . /go/src/github.com/tamarakaufler/goaws/

             RUN go get github.com/ghodss/yaml

             RUN go get github.com/gorilla/mux

             WORKDIR /go/src/github.com/tamarakaufler/goaws

             RUN go build .

             CMD ["./goaws"]


b) Another option is to create an intermediate image based on an official golang one, install various 3rd party packages that your applications need and copy over internal packages. Then:

             FROM my_golang

             COPY ./goaws /

             CMD ["/goaws"]

where my_golang's Dockerfile is:

           FROM golang:1.8

           RUN mkdir -p /go/src/github.com/tamarakaufler/goaws
           COPY ./app/conf/config.go /go/src/github.com/tamarakaufler/goaws/app/conf/
           COPY ./app/router/router.go /go/src/github.com/tamarakaufler/goaws/app/router/

           RUN go get github.com/ghodss/yaml
           RUN go get github.com/gorilla/mux

Saturday, 6 May 2017

Dockerization of an sftp service

Synopsis

As part of building a web service, I wanted to have some of the microservice dependencies dockerized for easy setup and deployment. The three applications my web service depends are:

  • mongodb
  • rabbitmq
  • sftp

This post is about dockerizing sftp. The container will provide sftp-only user accounts and the users will be restricted to their home directory. The former is done through disabling login (in the Dockerfile), the latter by chrooting to the user's home directory (in the sshd_config file).


FROM ubuntu:latest

The latest version of ubuntu is our starting point.


RUN apt-get update && \
    apt-get -y install openssh-server


Sftp (SSH File Transfer Protocol) is a separate protocol packaged with SSH, so we install the ssh server.


RUN mkdir /var/run/sshd

Privilege separation directory, /var/run/sshd, must be present, otherwise the container will exit immediately after starting.


COPY sshd_config /etc/ssh/sshd_config

The default ssh configuration is adjusted for sftp purposes (https://github.com/tamarakaufler/go_loyalty_scheme_service/tree/master/dockerized/sftp).


RUN groupadd sftpusers

All sftp users will be part of this group.

 
 

RUN adduser  --quiet --disabled-password sftp_loyalty

When creating a new sftp user, the
--disabled-password option is provided not to have problems with the following command to change the password.


RUN echo "sftp_loyalty:BIGSeCrEt" | chpasswd sftp_loyalty

RUN usermod -g sftpusers sftp_loyalty && \
    usermod -s /bin/nologin sftp_loyalty && \
    chown root:sftp_loyalty /home/sftp_loyalty && \
    chmod 755 /home/sftp_loyalty


This assigns the sftp user to the correct group and disables normal login.


RUN mkdir /home/sftp_loyalty/uploads && \
    chown sftp_loyalty:sftp_loyalty /home/sftp_loyalty/uploads && \
    chmod 755 /home/sftp_loyalty/uploads

EXPOSE 22

CMD ["/usr/sbin/sshd", "-D"]


Starts the ssh server. I originally tried using:   service sshd start
but that did not work, preventing the container from starting.

Update:  Providing the full path:

             /usr/sbin/service sshs start

works

-----------------------------------------------------------------------------------------------------------------
sshd_config (based on the default /etc/ssh/sshd_config)

  1. Deleted the original line:
    1. Subsystem sftp /usr/lib/openssh/sftp-server
  2. Added at the end of the default sshd_config file:
    1. Subsystem sftp internal-sftp
      Match Group sftpusers
             ChrootDirectory %h #set the home directory
             ForceCommand internal-sftp
             X11Forwarding no
             AllowTCPForwarding no
              PasswordAuthentication yes


https://github.com/tamarakaufler/go_loyalty_scheme_service (when it becomes public)


References


https://www.vultr.com/docs/setup-sftp-only-user-accounts-on-ubuntu-14
https://github.com/atmoz/sftp
https://docs.docker.com/engine/examples/running_ssh_service/

Saturday, 7 May 2016

Why the order of the Docker commands in the Dockerfile matters

The Docker image is built from directives in the Dockerfile. Docker images are actually built in layers and each layer is part of the image's filesystem. A layer either adds to or replaces the previous one. Each Docker directive, when run, results in a new layer and the layer is cached, unless Docker is instructed otherwise. When the container starts, a final read-write layer is added on top to allow to store changes to the container. Docker uses the union filesystem to merge different filesystems and directories into the final one.

When the Docker image is being built, it uses the cached layers. If a layer was invalidated (one of the invalidation criteria is the checksum of files present in the layer, so when a file has changed during the build), Docker reruns all directives from the one, whose cached layer was invalidated, to the current command to recreate/create up-to-date layers. This makes Docker efficient and fast, and it also means the order of commands is important.

Example:


WRONG

1
2
3
4
5
6
WORKDIR /var/www/my_application            container directory where the RUN, CMD will be run

COPY .  /var/www/my_application            puts all our application files, including package,json, to its resting place in the container
RUN ["npm", "install"]                     installs application nodejs dependencies - this invalidates the cached layer for the COPY directive 
RUN ["node_modules/bower/bin/bower.js", "install", "--allow-root"]   installs application frontend dependencies - this invalidates the cached layer for the COPY directive          
RUN ["node_modules/gulp/bin/gulp.js"]      runs task runner that again invalidates the COPY directive layer by creating new (minified etc) files


CORRECT


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
WORKDIR /var/www/my_application            container directory where the RUN, CMD will be run

COPY package.json  /var/www/my_application this layer will only be invalidated if the package,json changes
RUN ["npm", "install"]                     installs application nodejs dependencies

COPY bower.json  /var/www/my_application   this layer will only be invalidated if the bower.json  changes
RUN ["node_modules/bower/bin/bower.js", "install", "--allow-root"]  installs application frontend dependencies    

COPY .  /var/www/my_application            puts all our application files to its resting place in the container

RUN ["node_modules/gulp/bin/gulp.js"]      runs task runner