Deploying your SaaS app using Kubernetes and Digital Ocean

Featured Image
  • Languages: Node, React
  • Tools Used: Kubernetes, Docker
  • Time saved: 2 weeks -> 30 mins

Create Sample Projects

Step 1 - Create a project

mkdir saasbase-project
cd saasbase-project

mkdir saasbase-fe
mkdir saasbase-be

Step 1 - Prepare your Frontend React app

We've written a detailed guide on how to build and dockerize your Frontend React app here. Place the project in the saasbase-fe folder.

docker login

docker build -t sssaini/saasbase-fe .
docker push sssaini/saasbase-fe:0.1

Step 2 - Build a Backend app using Node.js and Express.js

We've written a detailed guide on how to build and dockerize your Backend Node.js app here. Place the project in the saasbase-be folder.

docker login

docker build -t sssaini/saasbase-be .
docker push sssaini/saasbase-be:0.1

Deploy with Kubernetes on Digital Ocean

Step 3 - Create a Kubernetes cluster

  • You can leave the Kubernetes version and Datacenter region as default.
  • For the Cluster Capacity, I decided to choose the $20/month plan with a single node.

Once you have deployed to Production, you should increase the node count to make the deployment more resilient.

  1. Finalize the cluster by giving it a name. I called mine -
saasbase-cluster

.

Congratulations! Your cluster is now created.

Step 4 - Connect to the Cluster locally

Download kubectl

  • Download kubectl CLI from here.
  • Ensure it's set up correctly by running:
kubectl version

Configure the connection to the cluster

  • Download Kube config from the Actions menu in the Cluster UI

Move it to the correct folder by running:

mv saasbase-cluster-kubeconfig.yaml ~/.kube/config

You should be connected to the Digital Ocean cluster. Verify by running:

➜ ~ kubectl get nodes
NAME                   STATUS   ROLES    AGE   VERSION
pool-h5wx2v1ut-cudd5   Ready    <none>   57m   v1.22.7

Step 4 - Deploy images to Kubernetes using deploy.yaml

Create a file called fe.yaml in the saasbase-project folder. This will configure how our frontend deployment.

Notice that we're using the LoadBalancer type in the Service. This lets Digital Ocean know that we want an external IP for this service so we can view the app. In the next step, we will set up a custom domain that can be used to reach the app instead of an IP address.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: fe-deploy
spec:
  replicas: 1
  selector:
    matchLabels:
      app.kubernetes.io/name: saasbase-fe
  template:
    metadata:
      labels:
        app.kubernetes.io/name: saasbase-fe
    spec:
      containers:
        - name: frontend
          image: docker.io/sssaini/saasbase-fe:0.1
---
kind: Service
apiVersion: v1
metadata:
  name: fe-service
spec:
  selector:
    app.kubernetes.io/name: saasbase-fe
  type: LoadBalancer
  ports:
    - protocol: TCP
      port: 80
      targetPort: 3000

Deploy the Kube configuration by running:

➜ ~ kubectl apply -f fe.yaml
deployment.apps/fe-deploy created
service/fe-service created

Once running, we can get the IP address of the service by:

➜  ~ kubectl get pods
NAME                         READY   STATUS    RESTARTS   AGE
fe-deploy-8448fb4b97-6tgfj   1/1     Running   0          38s

Access the service by:

kubectl get services

The External IP takes about 5 mins to provision. Once assigned, you can view your application by opening the IP in your browser. For me it would be: http://143.198.246.142

Brilliant! I can see my React app running.

We can do exactly the same for our backend deployment. Create a file called

be.yaml

at the root level.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: be-deploy
spec:
  replicas: 1
  selector:
    matchLabels:
      app.kubernetes.io/name: saasbase-be
  template:
    metadata:
      labels:
        app.kubernetes.io/name: saasbase-be
    spec:
      containers:
        - name: backend
          image: docker.io/sssaini/saasbase-be:0.1
---
kind: Service
apiVersion: v1
metadata:
  name: be-service
spec:
  selector:
    app.kubernetes.io/name: saasbase-be
  type: LoadBalancer
  ports:
    - protocol: TCP
      port: 80
      targetPort: 7001

Apply the deployment by running:

kubectl apply -f be.yaml
deployment.apps/saasbase-be-deployment created
service/be-service created

Access the service by:

➜  ~ kubectl get pods
NAME                                      READY   STATUS    RESTARTS   AGE
fe-deploy-8448fb4b97-kfzg9                1/1     Running   0          16m
be-deploy-5fcb68649d-vj9sp   1/1     Running   0          7m8s

➜  ~ kubectl get services
NAME         TYPE           CLUSTER-IP       EXTERNAL-IP       PORT(S)        AGE
be-service   LoadBalancer   10.245.148.197   146.190.0.10      80:30791/TCP   6m2s
fe-service   LoadBalancer   10.245.5.13      143.198.246.142   80:31387/TCP   15m
kubernetes   ClusterIP      10.245.0.1       <none>            443/TCP        91m

Same as before, I can now access by backend by going to the External IP as such:

http://146.190.0.10

Step 5 - Buy a domain from Namecheap

Using the External IP works but it's not very user-friendly. We can buy a custom domain from Namecheap to access our services. I bought the domain: bearbill.com.

  • After buying the domain, set up DNS to point to Digital Ocean. Add the following custom DNS nameservers:

Step 6 - Add NGINX controller

  • On your Cluster dashboard, under the Add-Ons section, install the NGINX Ingress Controller. It takes a few minutes to provision.

  • Digital Ocean will also provision a Load Balancer for you automatically as part of the NGINX Ingress Controller deployment. Take a look under Networking > Load Balancers. This will be important in the next step.

We can verify that the Controller is successfully running with:

➜  ~ kubectl get pods --all-namespaces -l app.kubernetes.io/name=ingress-nginx
NAMESPACE       NAME                                        READY   STATUS    RESTARTS   AGE
ingress-nginx   ingress-nginx-controller-664d8d6d67-kvpkz   1/1     Running   0          85m
ingress-nginx   ingress-nginx-controller-664d8d6d67-vkmnk   1/1     Running   0          85m

➜  ~ kubectl get svc -n ingress-nginx
NAME                                 TYPE           CLUSTER-IP       EXTERNAL-IP     PORT(S)                      AGE
ingress-nginx-controller             LoadBalancer   10.245.223.54    64.225.91.107   80:32152/TCP,443:31302/TCP   84m
ingress-nginx-controller-admission   ClusterIP      10.245.98.130    <none>          443/TCP                      84m
ingress-nginx-controller-metrics     ClusterIP      10.245.188.111   <none>          10254/TCP                    84m

Add Custom Domain to Digital Ocean

  • Select Networking > Domains > Add a domain.
  • Add an A record with @ on Digital Ocean and point it to Load Balancer instance. This will be for our frontend which will be accessible at bearbill.com

  • Add another A record with **api **and point it to the same Load Balancer instance. This will be for our backend which will be accessible at api.bearbill.com

Since we are going to be using the custom domain to access the services, we can update the deployed frontend and backend services to not provision an external IP.

This can be done by simply commenting out type: LoadBalancer in the service.

Here's what my fe.yaml looks like:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: fe-deploy
spec:
  replicas: 1
  selector:
    matchLabels:
      app.kubernetes.io/name: saasbase-fe
  template:
    metadata:
      labels:
        app.kubernetes.io/name: saasbase-fe
    spec:
      containers:
        - name: frontend
          image: docker.io/sssaini/saasbase-fe:0.1
---
kind: Service
apiVersion: v1
metadata:
  name: fe-service
spec:
  selector:
    app.kubernetes.io/name: saasbase-fe
  # type: LoadBalancer
  ports:
    - protocol: TCP
      port: 80
      targetPort: 3000

Here's what my be.yaml looks like:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: be-deploy
spec:
  replicas: 1
  selector:
    matchLabels:
      app.kubernetes.io/name: saasbase-be
  template:
    metadata:
      labels:
        app.kubernetes.io/name: saasbase-be
    spec:
      containers:
        - name: backend
          image: docker.io/sssaini/saasbase-be:0.1
---
kind: Service
apiVersion: v1
metadata:
  name: be-service
spec:
  selector:
    app.kubernetes.io/name: saasbase-be
  # type: LoadBalancer
  ports:
    - protocol: TCP
      port: 80
      targetPort: 7001

Apply this change by running:

kubectl apply -f fe.yaml
kubectl apply -f be.yaml

Perfect. Now there shouldn't be an External IP when we run:

kubectl get services

Configure NGINX routing

To make the apps accessible with custom domains, we need to set up NGINX so that the traffic can be correctly routed into their respective containers.

Create a deploy.yaml :

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: ingress-echo
  namespace: default
spec:
  tls:
    - hosts:
        - bearbill.com
        - api.bearbill.com
  rules:
    - host: bearbill.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: fe-service
                port:
                  number: 3000
    - host: api.bearbill.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: be-service
                port:
                  number: 7001
  ingressClassName: nginx

Deploy again with:

kubectl apply -f deploy.yaml

We can make sure that the ingress service was created by:

➜  ~ kubectl get ingress
NAME           CLASS   HOSTS                           ADDRESS         PORTS     AGE
ingress-echo   nginx   bearbill.com,api.bearbill.com   64.225.91.107   80, 443   25m

The backend should now be accessible at: http://api.bearbill.com. It should be accessible at: http://bearbill.com.

I'm building a new SaaS to automate content marketing for your SaaS

Check it out →

Tools for SaaS Devs