Leon's Blogging

Coding blogging for hackers.

Rails + Puma + Nginx + MySQL With Docker

| Comments

將 Rails + Puma, Nginx, MySQL 都拆開成各自的 container,並透過 docker-compose 將多個 container 串起來,各司其職,協同服務。

  • Nginx 在最前面解析請求並處理靜態資源
  • Puma 位於 Nginx 於 Rails 程序之間,用於處理動態的請求;最後面還有一個數據存儲的 MySQL

container 分配

  • app - 用來啟動 Rails + Puma
  • web - 存放 nginx,負責解析各種外部請求,處理靜態的資源 (靜態資源就是運行 rake assets:precompile 生成在 public/assets 中的內容)
  • db - MySQL

在現有的 rails project 加上 docker 所需的 file

1
2
3
4
5
6
7
8
9
10
11
rails_project
├── docker
   └── app
       └── Dockerfile
   └── db
       └── grant_user.sql
   └── web
       ├── Dockerfile
       └── nginx.conf
├── docker-compose.yml
└── .env

docker/app/Dockerfile

--path vendor/bundle

Using Docker for Rails Development

We want to install gems in ./vendor/bundle because the gems will persist in ./vendor/bundle regardless of the lifecyle of the container. When we update the Gemfile and do bundle install again, it will only install the newly added gems, not everything again.

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# Base image
FROM ruby:2.5.1

# Install plugin
RUN apt-get update -qq && apt-get install -y build-essential vim

# Install mysql
RUN apt-get install -y default-libmysqlclient-dev

# Install nodejs
RUN curl -sL https://deb.nodesource.com/setup_11.x | bash - &&\
    apt-get install -y nodejs

# Clears out the local repository of retrieved package files
RUN apt-get -q clean

# Set an environment variable where the Rails app is installed to inside of Docker image
ENV APP_PATH /usr/src/app
RUN mkdir -p $APP_PATH

# Set working directory
WORKDIR $APP_PATH

# Setting env up
ENV RAILS_ENV production
ENV RACK_ENV production
# Setting local
ENV LC_ALL C.UTF-8
# Setting timezone
ENV TZ Asia/Taipei
RUN cp /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone

# COPY Gemfile & Gemfil.lock
COPY Gemfile* $APP_PATH/

# Run bundle
RUN bundle install --jobs 20 --retry 5 --without development test --path vendor/bundle

# Adding project files
COPY . $APP_PATH/

# Build Frond-End
RUN RAILS_ENV=$RAILS_ENV bundle exec rake assets:precompile

EXPOSE 3000

CMD ["bundle", "exec", "puma", "-C", "config/puma.rb"]

docker/web/Dockerfile

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
31
32
33
# Base image
FROM nginx:1.15.8

# Install dependencies
RUN apt-get update -qq && apt-get -y install apache2-utils vim

# Establish where Nginx should look for files
ENV RAILS_ROOT /usr/src/app
# Setting local
ENV LC_ALL C.UTF-8
# Setting timezone
ENV TZ Asia/Taipei
RUN cp /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone

# Set our working directory inside the image
WORKDIR $RAILS_ROOT

# create log directory
RUN mkdir log

# copy over static assets
COPY public public/

# Copy Nginx config template
COPY docker/web/nginx.conf /tmp/docker.nginx

# substitute variable references in the Nginx config template for real values from the environment
# put the final config in its place
RUN envsubst '$RAILS_ROOT' < /tmp/docker.nginx > /etc/nginx/conf.d/default.conf
EXPOSE 80

# Use the "exec" form of CMD so Nginx shuts down gracefully on SIGTERM (i.e. `docker stop`)
CMD [ "nginx", "-g", "daemon off;" ]

docker/web/nginx.conf

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
# define our application server

upstream rails_app {
  # The app service 3000 port that points to the docker-compose definition
   server app:3000;
}

server {
   listen 80;
   # define your domain or IP
   server_name localhost;

   # define the public application root
   root   $RAILS_ROOT/public;
   index  index.html;

   # define where Nginx should write its logs
   access_log $RAILS_ROOT/log/nginx.access.log;
   error_log $RAILS_ROOT/log/nginx.error.log;

   # deny requests for files that should never be accessed
   # ~ regular 區分大小寫, .env / .git
   location ~ /\. {
      deny all;
   }

   # ~* regular 不分大小寫, .rb / .log
   location ~* ^.+\.(rb|log)$ {
      deny all;
   }

   # serve static (compiled) assets directly if they exist (for rails production)
   location ~ ^/(assets|images|javascripts|stylesheets|swfs|system)/   {
      # $uri: localhost/404.html,則 $uri 為 `/404.html`
      # @rails: 後面定義的 location @rails
      # 如果 url 匹配進來,則先按 $uri 處理,若沒有找到,則交給 @rails 處理
      try_files $uri @rails;
      # close access log
      access_log off;
      # to serve pre-gzipped version
      # 設定為 `on` ,在處理壓縮之前,先查找已經預壓縮的文件(.gz)
      # 避免每次對同一個文件進行重複的壓縮處理
      gzip_static on;

      expires max;
      # public 對每個用戶有效; private 對當前用戶有效
      add_header Cache-Control public;

      add_header Last-Modified "";
      add_header ETag "";
      break;
   }

   # send non-static file requests to the app server
   location / {
      try_files $uri @rails;
   }

   location @rails {
      internal; # 只能被內部的請求呼叫,外部的呼叫請求會返回 'Not found'
      proxy_set_header  X-Real-IP  $remote_addr;
      proxy_set_header  X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_set_header Host $http_host;
      proxy_redirect off;
      proxy_pass http://rails_app; # 導向到 upstream rails_app
   }
}

database.yml

host name 必須對應到 docker-compose 所定義的 service name,並且透過環境變數所設定的 user 來登入

1
2
3
4
5
6
7
8
9
default: &default
  adapter: mysql2
  encoding: utf8
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  host: db
  port: 3306
  username: <%= ENV.fetch('MYSQL_USER') { 'root' } %>
  password: <%= ENV.fetch('MYSQL_PASSWORD') { 'password' } %>
  socket: /tmp/mysql.sock

docker/db/grant_user.sql

因為在 mysql 有另外建立一個 user,並且在 database.yml 也是透過這個 user 來登入,因此必須授權給此 user 權限,才能夠操作

1
2
GRANT ALL PRIVILEGES ON *.* TO 'user_name'@'%';
FLUSH PRIVILEGES;

docker-compose.yml

.env 的變數,可以用 ${MYSQL_USER} 使用在 docker-compose 中

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
31
32
33
version: '3'
services:
  app:
    build:
      context: .
      dockerfile: ./docker/app/Dockerfile
    env_file:
      - .env
    volumes:
      - .:/usr/src/app
    depends_on:
      - db
  db:
    image: mysql:5.7.23
    command: --default-authentication-plugin=mysql_native_password
    restart: always
    env_file:
      - .env
    ports:
      - "3306:3306"
    volumes:
      - db-data:/var/lib/mysql
  web:
    build:
      context: .
      dockerfile: ./docker/web/Dockerfile
    ports:
      - 80:80
    depends_on:
      - app
volumes:
  db-data:
    external: false

.env

docker-compose 所需要用到的環境變數,app & web 都會用到

1
2
3
MYSQL_ROOT_PASSWORD=password
MYSQL_USER=user_name
MYSQL_PASSWORD=user_password

Example project

product_system_production

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
git clone https://github.com/mgleon08/product_system_production
# 建立 image
docker-compose build
# 啟動
docker-compose up -d
# 因為是建立新的 user 來造訪 mysql,因此必須先授權此 user 權限
p# 確認是否授權成功
docker-compose exec db mysql -u user_name -p -e"show grants;"
# 建立資料庫
docker-compose run --rm app bundle exec rails db:create
# 跑 migrate
docker-compose run --rm app bundle exec rails db:migrate
# 建立假資料
docker-compose run --rm app bundle exec rails db:seed
# 查看畫面, 記得是 http
http://localhost

Production

Rails5.2 之後,secret_key_base 的設定改了,在 production 上要在 config 裡面加上 master.key file,並將 local 的亂碼貼上去

最後另外附上 develop 的 docker

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
# docker-compose
version: '3'
services:
  db:
    image: mysql:5.7.23
    command: --default-authentication-plugin=mysql_native_password
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: password
    ports:
      - "3306:3306"
    volumes:
      - ./opt/data:/var/lib/mysql
  backend:
    container_name: product_system
    build:
      context: .
      args:
        UID: ${UID:-1001}
    volumes:
      - .:/usr/src/app
    ports:
      - "3000:3000"
    depends_on:
      - db
    user: deploy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# Dockerfile
FROM ruby:2.5.1

RUN apt-get update -qq &&\
    curl -sL https://deb.nodesource.com/setup_11.x | bash - &&\
    apt-get install -y nodejs cmake &&\
    apt-get clean

ARG UID
RUN adduser deploy --uid $UID --disabled-password --gecos ""

ENV APP /usr/src/app
RUN mkdir $APP
WORKDIR $APP

COPY Gemfile* $APP/
RUN bundle install -j3 --path vendor/bundle

COPY . $APP/

CMD ["bundle", "exec", "rails", "server", "-p", "3000", "-b", "0.0.0.0"]

README

1
2
3
4
5
6
7
8
9
10
11
# build image
docker-compose build

# bundle
docker-compose run -u root backend bundle

# create database
docker-compose run backend bundle exec rails db:create db:migrate db:seed

# start
docker-compose up

New project need to add db (docker-compose.yml mysql name) to database.yml

1
2
3
4
5
6
7
8
9
default: &default
  adapter: mysql2
  encoding: utf8
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  host: db
  port: 3306
  username: root
  password: password
  socket: /tmp/mysql.sock

Sample

參考文件:

Comments