Deploy Rails with helm and serve static files

Kubernetes is a great way to deploy your application independet if your application is for a small web or a apis with 1000 of requests per second. It is used by many big companies like google or IBM.

Every new technology create new chalanges that have to be resolved. One of them is how to deploy a rails application on kubernetes and serve asset files in a eficient way.

Helm

Helm is the kubernetes packet manager. It is posible to deploy things to kubernetes without it, but using it make it much easier. If you have installed kubernetes on a ubuntu server, just do the following on the master node:

snap install helm

Alternatively, you can install helm on you laptop and copy /etc/kubernetes/admin.conf from the server to .kube/config on your pc.

Basic config

Create a new rails project:

rails new project
cd project
mkdir project

 

Then create the following files:

 

# Dockerfile

FROM ruby:2.5.1-alpine3.7
ENV RAILS_ENV production

WORKDIR  /app
RUN apk add --update \
  nodejs nodejs-npm \
  sqlite-dev
RUN gem install bundle
COPY Gemfile Gemfile.lock /app
RUN bundle install
COPY . /app

RUN bundle exec rake asset:precompile

CMD rails s -b 0.0.0.0

 

 

# project/Chart.yaml

name: project
version: 0.1.0

 

 

# project/values.yaml

image:
  repository: your docker image
  tag: latest
secret_key_base: your secret
enviroment: production

 

 

# project/templates/web-deployment.yaml

kind: Deployment
metadata:
  name: {{ .Release.Name }}-web
spec:
  replicas: 1
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 2
      maxUnavailable: 1
  template:
    metadata:
      labels:
        app: {{ .Release.Name }}-web
    spec:
      containers:
        - name: web
          image: {{ .Values.image.repository }}:{{ .Values.image.tag }}
          imagePullPolicy: "Always"
          ports:
            - containerPort: 3000
          env:
            - name: SECRET_KEY_BASE
              value: {{ .Values.secret_key_base }}
            - name: RAILS_ENV
              value: {{ .Values.enviroment }}
            # We will remove this later in this tutorial
            - name: SERVE_STATIC_FILES
              value: true

 

 

# project/templates/web-service.yaml

kind: Service
apiVersion: v1
metadata
  name: {{ .Release.Name }}-service
spec:
  selector:
    app: {{ .Release.Name }}-web
  type: NodePort
  ports:
  - name: http
    protocol: TCP
    port: 80
    :targetPort: 80

Deploy

Execute the following comands:

docker build -t  .
docker push origin 
helm install homepage

Serve static files

Modify the project/templates/web-deployment.yaml to server static files by ngnix, instead to make rails to serve them. The advantage of this is that nginx is much fast on it and that nginx can put cache period on the assets headers.

# project/templates/web-deployment.yaml

apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: {{ .Release.Name }}-web
spec:
  replicas: 1
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 2
      maxUnavailable: 1
  template:
    metadata:
      labels:
        app: {{ .Release.Name }}-web
    spec:
      containers:
        - name: web
          image: {{ .Values.image.repository }}:{{ .Values.image.tag }}
          imagePullPolicy: "Always"
          ports:
            - containerPort: 3000
          env:
            - name: SECRET_KEY_BASE
              value: {{ .Values.secret_key_base }}
            - name: RAILS_ENV
              value: {{ .Values.enviroment }}
          volumeMounts:
            - mountPath: /assets
              name: assets

          lifecycle:
            postStart:
              exec:
                command:
                  - sh
                  - -c
                  - "cp -r /app/public/* /assets"

        - name: nginx
          image: nginx:1.14-alpine
          ports:
            - containerPort: 80
          volumeMounts:
            - mountPath: /assets
              name: assets
              readOnly: true
            - mountPath: /etc/nginx/nginx.conf
              name: nginx-conf
              subPath: nginx.conf
              readOnly: true
      volumes:
        - name: nginx-conf
          configMap:
            name: {{ .Release.Name }}-nginx-conf
            items:
              - key: nginx.conf
                path: nginx.conf
        - name: assets
          emptyDir: {}

You also have to add the nginx config.

# project/templates/nginx-config-map.yml

apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ .Release.Name }}-nginx-conf
data:
  nginx.conf: |
    user nginx;
    worker_processes  1;

    error_log /dev/stdout warn;
    pid        /var/run/nginx.pid;

    events {
      worker_connections  1024;
    }

    http {
      include       /etc/nginx/mime.types;
      default_type  application/octet-stream;

      log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                        '$status $body_bytes_sent "$http_referer" '
                        '"$http_user_agent" "$http_x_forwarded_for"';

      access_log  /var/log/nginx/access.log main;

      sendfile on;
      tcp_nopush on;
      tcp_nodelay on;

      keepalive_timeout  65;

      gzip  on;
      gzip_static  on;
      gzip_http_version 1.0;
      gzip_comp_level 5;
      gzip_proxied any;
      gzip_types application/x-javascript application/xhtml+xml application/xml application/xml+rss text/css text/javascript text/plain text/xml application/octet-stream image/x-icon image/png;
      gzip_vary on;
      gzip_disable "MSIE [1-6].(?!.*SV1)";

      client_max_body_size 10m;

      server_names_hash_bucket_size 64;


      upstream app {
        server localhost:3000 fail_timeout=0;
      }

      server {
        listen 80;

        root /assets;

        keepalive_timeout 5;
        client_max_body_size 20m;

        location / {
          try_files $uri/index.html $uri/index.htm @app;
        }

        location @app {
          proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
          proxy_set_header Host $http_host;
          proxy_http_version 1.1;
          proxy_redirect off;

          proxy_read_timeout 60;
          proxy_send_timeout 60;

          # If you don't find the filename in the static files
          # Then request it from the app server
          if (!-f $request_filename) {
            proxy_pass http://app;
            break;
          }
        }

        location /nginx_status {
          stub_status on;
          access_log off;
          allow 127.0.0.1;
          deny all;
        }

        location ~ ^/assets/ {
          expires 1y;
          add_header Cache-Control public;

          add_header ETag "";
          break;
        }
      }
    }