Introduction
As a software engineer, have you ever been excited by a fantastic idea for a web project, only to pause when faced with the overwhelming task of handling all the DevOps work needed for deployment? The mere thought of dealing with these complexities can be enough to prevent even the most eager from moving forward with their projects.
In this guide, we’re going to create a fun and simple web service that delivers random dad jokes to lighten up your day. We’ll use the Node Express.js framework to power the backend and a straightforward React application for the frontend, without a database setup. Our journey will begin with the development of the service, followed by packaging our application into a container using Docker, and finally deploying it on a cloud-based VPS through Kubernetes.
By the end of this guide, you will have a web server hosted on a cloud VPS, accessible securely via HTTPS with a fully qualified domain name, ready to spread joy with dad jokes to anyone who visits.
Prerequisites
While this guide’s setup steps are described for Mac OS users, individuals using other operating systems will likely encounter a very similar process.
For this tutorial, you’ll need to ensure you have the following prerequisites:
- Node.js: For building and running the backend server locally.
- Docker: For containerizing the application.
- Domain Name: You’ll need to own a domain name and have access to its DNS settings. Domain names can be purchased from providers like GoDaddy.
- VPS Server: You will need access to a VPS server with the public IPv4 address and minimum specifications of 4GB RAM 2 CPU cores and Linux-based OS — this guide is tailored for Ubuntu 22.04, so sticking to it will ensure the best results. We’ll go over creating a VPS service below if you don’t have one yet.
Step 0. Creating VPS and updating DNS A entry
Given that DNS propagation can take several hours to complete, it is advisable to prioritize this step early on in the tutorial. Before proceeding, ensure you possess a domain name. If you need to acquire one, platforms like GoDaddy offer the option to purchase and also facilitate the modification of DNS settings.
Additionally, securing a VPS server is essential for the next steps. For this guide, I will be utilizing VDSina.com due to its flexible payment system, which allows for daily rather than monthly payments — a convenient feature for this tutorial.
Please note that you are free to choose any VPS provider that suits your needs.
Creating VPS with VDSina
First, let’s generate an SSH key — we’ll be using it to access our server. Run ssh-keygen
in the terminal and click through (you can use all defaults).
Create an account on VDSina.com and add a new SSH key — paste the value from id_rsa.pub
file.
Now, let’s create our server with Ubuntu 22.04. Note that you’ll need to have a couple of bucks on your balance to purchase a server, which should only cost about 0,33$ / day.
After about 2–5 minutes server should be created and we can start using it.
For this step, we only need to copy the IP address of the server and update our DNS entries for our domain to point to that IP.
Updating DNS A record
Now, navigate to the DNS settings of your domain to add a new A record. The entry should be configured with the following values:
Name/Host: dadjokes
Type: A
Value: IP address for your server, e.g. 80.85.245.188
in my case
While I am using the DNS settings page provided by GoDaddy, as my domain was also purchased there, you should access the DNS settings through the provider you’ve used to acquire your domain.
Once you’ve made the changes, the DNS propagation process will start. It’s important to remember that updating DNS records can take anywhere from 20 minutes to 72 hours, so patience is key during this period. To monitor the status of the propagation, you can use the online tool available at https://www.whatsmydns.net/#A, which provides real-time updates on how your DNS changes are spreading across the internet.
Step 1. Building a Web Service
In this chapter, we will develop a simple yet engaging web service that delivers random dad jokes to lighten up your day. The service architecture includes a backend powered by the Node.js Express framework with Typescript, coupled with a frontend developed as a single-page application (SPA) utilizing React.
Let’s get started!
Building a backend server
Create a new project folder named dadjokes
with backend
subfolder. Navigate inside backend
directory and create a new package.json
file with the next content:
{
"name": "dadjokes-backend",
"version": "1.0.0",
"description": "Dadjokes Generator Service",
"main": "server.js",
"scripts": {
}
}
Note that you can also initiate npm project by running npm init
in the terminal.
Now, let’s install the all required dependencies for our typescript backend service:
npm i express
npm i -D typescript nodemon ts-node @types/node @types/express
This will add new entries to the package.json
file, create a package-lock.json
file and node_modules
directory with installed modules.
To configure the TypeScript compiler for your project, we’ll add a tsconfig.json
file to the root directory with the options specified below:
{
"compilerOptions": {
"target": "es6",
"module": "commonjs",
"rootDir": "./",
"outDir": "./dist",
"esModuleInterop": true,
"strict": true
},
"include": [
"src/**/*"
],
"exclude": [
"node_modules",
"dist"
]
}
Note that we’ll not go over the options in this tutorial, you can read about these options on the official typescript site.
Moving forward, let’s add a server with the logic for retrieving a random dad joke. Please note that to keep things simple, we’re not going to use the database, but instead use a fixed list of jokes stored as an array.
Create src
folder and two new files under it: jokes.ts
and server.ts
. The file structure at this point should look like below:
dadjokes
- backend
- node_modules (generated by npm)
- src
- jokes.ts
- server.ts
- package.json
- package-lock.json (generated by npm)
- tsconfig.json
Add the code with logic to retrieve a random joke to the jokes.ts
:
const JOKES = [
{id: 1, setup: 'I hate people who talk about me behind my back...', punchline: 'They discussed me.'},
{id: 2, setup: "Ladies, if he can't appreciate your fruit jokes...", punchline: "...you need to let that mango"},
{id: 3, setup: "Did you hear about the 2 guys that stole a calendar?", punchline: "They both got 6 months."},
{id: 4, setup: "I've got a great joke about construction,", punchline: "but I'm still working on it."},
{id: 5, setup: "I ordered a chicken and an egg online.", punchline: "I’ll let you know."},
{id: 6, setup: "If you see a crime at an Apple Store,", punchline: "does that make you an iWitness?"},
{id: 7, setup: "I'm so good at sleeping,", punchline: "I can do it with my eyes closed!"},
{id: 8, setup: "I was going to tell a time-traveling joke,", punchline: "but you guys didn't like it."},
{id: 9, setup: "How do lawyers say goodbye?", punchline: "We'll be suing ya!"},
{id: 10, setup: "Spring is here!", punchline: "I got so excited I wet my plants."},
{id: 11, setup: "Don't trust atoms.", punchline: "They make up everything!"},
{id: 12, setup: "When does a joke become a dad joke?", punchline: "When it becomes apparent."},
{id: 13, setup: "What did the fish say when he hit the wall?", punchline: "Dam."},
{id: 14, setup: "Is this pool safe for diving?", punchline: "It deep ends."},
{id: 15, setup: "I once got fired from a canned juice factory.", punchline: "Apparently I couldn't concentrate."},
{id: 16, setup: "A cheese burger walks into a bar, the bartender says", punchline: "sorry sir we don't serve food here."},
{id: 17, setup: "When I was a kid, my mother told me I could be anyone I wanted to be.", punchline: "Turns out, identity theft is a crime."},
{id: 18, setup: "Did you hear about the restaurant on the moon?", punchline: "Great food, no atmosphere!"},
{id: 19, setup: "Why did the old man fall in the well?", punchline: "Because he couldn't see that well!"},
{id: 20, setup: "Why did the invisible man turn down the job offer?", punchline: "He couldn't see himself doing it!"},
{id: 21, setup: "Within minutes, the detectives knew what the murder weapon was.", punchline: "It was a brief case."},
{id: 22, setup: "To whoever stole my copy of Microsoft Office, I will find you.", punchline: "You have my Word!"},
{id: 23, setup: "I thought about going on an all-almond diet...", punchline: "But that's just nuts!"},
{id: 24, setup: "I told my girlfriend she drew her eyebrows too high.", punchline: "She seemed surprised!"},
{id: 25, setup: "I know a lot of jokes about retired people", punchline: "but none of them work!"},
]
let seenJokes: number[] = [];
export function getRandomJoke() {
if (seenJokes.length === JOKES.length) {
seenJokes = [];
}
const unseenJokes = JOKES.filter(joke => seenJokes.indexOf(joke.id) === -1);
const randomIndex = Math.floor(Math.random() * unseenJokes.length);
const selectedJoke = unseenJokes[randomIndex];
seenJokes.push(selectedJoke.id);
return selectedJoke;
}
Now, let’s add a server logic to the server.ts
file:
import express, {Request, Response} from "express"
import {getRandomJoke} from "./jokes";
const app = express()
const PORT = process.env.PORT || 8000
app.get('/api/joke', (req: Request, res: Response) => {
res.send(getRandomJoke())
})
app.get('/', (req: Request, res: Response) => {
res.send('Hello there!')
})
app.listen(PORT, () => console.log(`Server Running on Port ${PORT}`))
Update package.json
scripts section with dev script to run a server locally:
"scripts": {
"dev": "nodemon src/server.ts"
}
Next, run npm run dev
from the terminal and open the browser with http://localhost:8000/ — you will see “Hello there!” returned from the server.
Change the URL to http://localhost:8000/api/joke and reload the page a couple of times — you will see a new joke returned on every reload.
We’re done with the backend service for now, let’s switch to the client.
(!) Don’t kill the server process just yet, we’ll need it to test our web service locally, so use the new terminal tab for the frontend.
Building a front-end client
For the front-end single-page application, we’ll be using React. To build it we’ll use vite.js — a modern frontend build tool that significantly improves the development experience for web developers.
Switch to the dadjokes
folder in the terminal and run:
npm create vite@latest
This will bring a dialog with questions about the future project — provide the following answers:
% npm create vite@latest
npx: installed 1 in 1.441s
✔ Project name: … frontend
✔ Select a framework: › React
✔ Select a variant: › TypeScript
Scaffolding project in /Users/liubovpitko/git/dadjokes/frontend...
Done. Now run:
cd frontend
npm install
npm run dev
Run the suggested commands:
cd frontend
npm install
npm run dev
You should see the following in the terminal:
% npm run dev
> frontend@0.0.0 dev
> vite
VITE v5.1.1 ready in 605 ms
➜ Local: http://localhost:5173/
➜ Network: use --host to expose
➜ press h + enter to show help
If you open the browser on http://localhost:5173/ you’ll see the default Vite template project showing up:
Let’s introduce a custom logic to our application. We’ll have a button that fetches the random joke from the server and displays it on the screen with a smooth appearance animation.
Replace the code in the src/App.tsx
to the following:
import {useState} from 'react'
import './App.css'
interface Joke {
id: number;
setup: string;
punchline: string;
}
function App() {
const [joke, setJoke] = useState<Joke>();
const getNewJoke = async () => {
const newJoke: Joke = await fetch('/api/joke').then(response => response.json());
setJoke(newJoke)
}
const animationDelay = `${500 + joke?.setup.length * 30}ms`;
const jokeStyle = {'animationDelay': animationDelay};
return (
<div>
{joke ?
<div key={joke.id}>
<div className="joke">
{joke.setup}
</div>
<div className="joke" style={jokeStyle}>
{joke.punchline}
</div>
<button onClick={getNewJoke}>
Get another one
</button>
</div> :
<button onClick={getNewJoke}>
Get a joke
</button>
}
</div>
)
}
export default App
You can notice that we’re fetching the joke without defining a host and a port but using a relative path to /api/joke
on this line:
const newJoke: Joke = await fetch('/api/joke').then(response => response.json());
This setup is suitable for production, where both the backend and frontend share the same URL. However, for local development, adjustments are needed. Without changes, requests from the frontend development server will be sent to http://localhost:5173
and won't reach the local backend server at http://localhost:8000
. To fix this, we need to set up a proxy for the local development server.
Open vite.config.ts
and replace the code with the following:
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
server: {
proxy: {
'/api': {
target: 'http://localhost:8000',
changeOrigin: true,
secure: false
}
}
}
})
We’re almost ready to test our app, but first, let’s replace definitions in src/index.css
to make it just a little bit prettier:
body {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
color: #213547;
display: flex;
place-items: center;
min-height: 100vh;
}
button {
border-radius: 0.5rem;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 2rem;
font-weight: 500;
font-family: inherit;
cursor: pointer;
transition: border-color 0.25s;
background-color: #f9f9f9;
}
button:hover {
border-color: #213547;
}
button:focus,
button:focus-visible {
outline: 4px auto #1e262f;
}
@keyframes appear {
from {opacity: 0;}
to {opacity: 1;}
}
.joke {
font-size: 2rem;
margin-bottom: 2rem;
opacity: 0;
animation: appear 2s forwards;
}
And update index.html
title to:
<title>Dad Jokes Generator</title>
Open the browser and check what we have:
Great! We’re nearly ready to dive into deployment. But first, we need to modify our backend service to serve static frontend files to wrap our application in a docker image.
Modifying the backend to serve static files
Let’s prepare our backend to serve static files from the frontend app.
Replace next lines:
app.get('/', (req: Request, res: Response) => {
res.send('Hello there!')
})
with the following code:
const cwd = process.cwd()
app.use(express.static(`${cwd}/static`));
app.use((req: Request, res: Response) => {
res.sendFile(`${cwd}/static/index.html`, (err) => {
if (err) {
res.status(500).send(err);
}
});
});
The server.ts
code should look like the following:
import express, {Request, Response} from "express"
import {getRandomJoke} from "./jokes";
const app = express()
const PORT = process.env.PORT || 8000
app.get('/api/joke', (req: Request, res: Response) => {
res.send(getRandomJoke())
})
const cwd = process.cwd()
app.use(express.static(`${cwd}/static`));
app.use((req: Request, res: Response) => {
res.sendFile(`${cwd}/static/index.html`, (err) => {
if (err) {
res.status(500).send(err);
}
});
});
app.listen(PORT, () => console.log(`Server Running on Port ${PORT}`))
This code enables access to static files stored in the backend/static
folder, making them available to the browser. For paths not specified, we will default to serving the index.html
file, which presents the frontend interface and manages the underlying logic.
It's important to note that since we haven't yet placed any static files in the static folder, attempting to visit http://localhost:8000/
at this stage will result in an error.
Now we’re ready to containerize our application and prepare it for deployment.
Step 2. Containerizing application
Containerizing an app means wrapping up everything it needs to run — like its code, necessary software, and settings — into a single package called a container image. This makes the app run the same way, no matter where you use it, on any system that can handle containers, such as Docker.
In this guide, we’re using Docker containers because we’ll be deploying our app with Kubernetes. Think of Kubernetes as a manager for containers — it helps us launch and run our apps smoothly and in a consistent way, no matter where they are.
If you’re new to Docker and Kubernetes and want to dig deeper, I suggest checking out TechWorld with Nana on YouTube. It’s a great resource to learn about the latest in DevOps tools and technologies in an easy-to-understand way.
Creating a Dockerfile
Dockerfile — is a text document that contains instructions to assemble a docker image.
Let’s create a file named Dockerfile in the root of the dadjokes
project and start with the next line:
FROM --platform=linux/x86_64 node:20.11-alpine
This instructs docker to start building our image from an existing node image based on Alpine Linux. Alpine distribution is the smallest Linux distribution which allows building lightweight images.
Now, let's instruct Docker to install typescript globally as we need to build our apps.
RUN npm install -g typescript
Next, we need to copy our project files into the container and build the backend app with typescript.
WORKDIR /backend
COPY ./backend .
RUN tsc
We’re using WORKDIR
instruction to create a new directory and change the dir to it inside the container. We then use COPY
command to copy all files that we have in our local ./backend
directory to /backend
directory inside the container. Lastly, we build our backend application with RUN tsc
command — this will create a /backend/dist
folder with built files.
We need to install production dependencies for our app inside the container. To do that, we’ll copy package.json
and package-lock.json
to the directory with built files and run the npm command to install only production dependencies.
COPY ./backend/package*.json ./dist
RUN cd /backend/dist && npm install --production
Now, we also need to build and copy frontend static files to our backend.
WORKDIR /frontend
COPY ./frontend .
RUN npm i && npm run build && cp -R ./dist /backend/dist/static
In the second stage of our multi-stage Dockerfile, we will selectively copy only the essential files needed to run our application from the first stage into a new container, leaving behind any files that were solely necessary for the development and building of the app. This approach ensures that our image is both leaner and more efficient.
All files we need for our app are now stored in the /backend/dist
folder of the app, therefore we’ll copy it over with COPY --from=0
command where --from=0
instructs to copy from the 0’s stage of the current Dockerfile.
FROM --platform=linux/x86_64 node:20.11-alpine
COPY --from=0 /backend/dist /app
Finally, we’ll instruct our image to run our server:
WORKDIR /app
CMD ["src/server.js"]
The final Dockerifle will look like shown below:
FROM --platform=linux/x86_64 node:20.11-alpine
RUN npm install -g typescript
WORKDIR /backend
COPY ./backend .
RUN tsc
COPY ./backend/package*.json ./dist
RUN cd /backend/dist && npm install --production
WORKDIR /frontend
COPY ./frontend .
RUN npm i && npm run build && cp -R ./dist /backend/dist/static
FROM --platform=linux/x86_64 node:20.11-alpine
COPY --from=0 /backend/dist /app
WORKDIR /app
CMD ["src/server.js"]
Perfect! Now let’s return to the terminal and build the image:
docker build . -t dadjokes
And run it:
docker run -p 8000:8000 dadjokes
Now, if you open the browser on http://localhost:8000/
you should see our app the same way you saw it at the end of the previous step.
Note that if you are still running the development server on port 8000
you will get an error:
docker: Error response from daemon: Ports are not available: listen tcp 0.0.0.0:8000: bind: address already in use.
To fix it either kill the development server or use a different port to run our app on the host by changing this part of the docker run command -p 8001:8000
and accessing it on the new port, e.g. http://localhost:8001/
Awesome job! We are now ready for the most exciting part — deployment of the application to the VPS.
Step 3. Setting up Kubernetes cluster and docker registry
In this section, we will work on installing microk8s on our VPS server, setting up access to it from our local machine, and creating a docker registry to use for deployment.
Setting up Kubernetes cluster
Let’s SSH into your VPS server using an IP address from step 0:
ssh root@80.85.245.188
Upgrade packages for Ubuntu first:
apt-get update
apt-get upgrade
And install microk8s:
sudo snap install microk8s --classic
Check that microk8s installed and running:
microk8s status
microk8s is running
high-availability: no
datastore master nodes: 127.0.0.1:19001
datastore standby nodes: none
...
Enable the following add-ons:
microk8s enable ingress
microk8s enable registry
microk8s enable cert-manager
Now we’ll need to enable pulling images from the local registry. The latest instructions about the private registry can be found on the microk8s site.
Create a new configuration directory and file for the registry. Ensure to replace 80.85.245.188
with your IP.
sudo mkdir -p /var/snap/microk8s/current/args/certs.d/80.85.245.188:32000
Now, create the file hosts.toml
with the next content. Ensure to replace 80.85.245.188
with your IP:
vi /var/snap/microk8s/current/args/certs.d/80.85.245.188:32000/hosts.toml
server = "http://80.85.245.188:32000"
[host."http://80.85.245.188:32000"]
capabilities = ["pull", "resolve"]
And restart microk8s:
microk8s stop
microk8s start
Ensure microk8s is running:
microk8s status
Great. Microk8s is installed and configured and now we’ll work on connecting to it from a local machine. Run:
microk8s config > kubeconfig
Now let's return to your local machine terminal and copy the created kubeconfig
. Replace the IP with your server’s address.
export IP=80.85.245.188
mkdir -p ~/.kube/microk8s/
scp root@$IP:/root/kubeconfig ~/.kube/microk8s/
For the next step we will need kubectl
— a command-line tool to work with Kubernetes, please install it.
We should be able to access the cluster now, let’s run:
export KUBECONFIG=~/.kube/microk8s/kubeconfig
kubectl get namespaces
You should see something like below if everything works correctly:
kubectl get namespaces
NAME STATUS AGE
kube-system Active 5m30s
kube-public Active 5m30s
kube-node-lease Active 5m30s
default Active 5m29s
ingress Active 4m27s
container-registry Active 4m18s
cert-manager Active 4m14s
Setting up the Docker registry
To utilize Docker images we’ve built locally, we must store them in a registry from which Kubernetes can retrieve them. One common choice is the official DockerHub registry, which provides free public repositories. However, for private repositories, which might be more suitable for your production application, DockerHub charges around $5/month. This is a convenient option that requires no extra setup beyond the cost.
Nonetheless, for this guide, we’ll opt for setting up our own private Docker registry. This option is straightforward and free, offering a cost-effective solution for storing images. It’s important to note, though, that this self-hosted registry may not be as fast or efficient as paid services.
Earlier in the guide, we enabled a registry add-on within our microk8s cluster, which we’ll use to store our images. This registry is hosted directly within the Kubernetes cluster and is accessible via port 32000
.
To utilize this registry, we need to construct an image and tag it with the registry’s address as part of the image name. Navigate to the root directory of the dadjokes
project and execute the docker build
command in the format shown below. Remember to substitute IP with the actual IP address of your VPS:
docker build . -t 80.85.245.188:32000/dadjokes:latest
Let’s try pushing it to the registry:
docker push 80.85.245.188:32000/dadjokes:latest
You should see a similar message:
The push refers to repository [80.85.245.188:32000/dadjokes]
Get "https://80.85.245.188:32000/v2/": http: server gave HTTP response to HTTPS client
The reason for this error is that our registry operates over HTTP and lacks encryption, making it unsecured. Nonetheless, since we are the creators of the registry, we can choose to trust it. To configure Docker to recognize and trust our registry, navigate to Docker Desktop, then proceed to Preferences → Docker Engine. Within the provided text field, you should add the following configuration:
"insecure-registries": [
"<IP>:32000"
],
Click on “Apply & Restart” to apply the new docker configuration.
Now we can push our docker image again:
docker push 80.85.245.188:32000/dadjokes
This time everything should work and the image will be pushed to our server — this may take a couple of minutes.
Fantastic! We’re so close to running our app on the internet!
Step 4. Creating Kubernetes Configs and Deploying App
Kubernetes orchestrates deployments and manages resources through yaml configuration files. While Kubernetes supports a wide array of resources and configurations, our aim in this tutorial is to maintain simplicity. For the sake of clarity and ease of understanding, we will use yaml configurations with hardcoded values. This method simplifies the learning process but isn’t ideal for production environments due to the need for manual updates with each new deployment. Although there are methods to streamline and automate this process, such as using Helm charts or bash scripts, we’ll not delve into those techniques to keep the tutorial manageable and avoid fatigue — you might be quite tired by that point!
Let’s create a deployment
folder in the root of the dadjokes
project — we will be creating configurations in it one by one and deploying them to our cluster.
First, let’s create a new namespace in Kubernetes for our application and switch context to it:
export KUBECONFIG=~/.kube/microk8s/kubeconfig
kubectl create namespace dadjokes
kubectl config set-context --current --namespace=dadjokes
Let’s create a service-account.yaml
to define a service account that Kubernetes will be using to deploy our app:
apiVersion: v1
kind: ServiceAccount
metadata:
name: dadjokes-service-account
namespace: dadjokes
Create a deployment.yaml
to define a Deployment resource. A Deployment in Kubernetes is a resource that manages and updates a group of identical pods that run our application. Ensure to substitute IP 80.85.245.188
to your VPS IP.
apiVersion: apps/v1
kind: Deployment
metadata:
name: dadjokes-deployment
namespace: dadjokes
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: dadjokes
template:
metadata:
labels:
app.kubernetes.io/name: dadjokes
spec:
serviceAccountName: dadjokes-service-account
containers:
- name: dadjokes
image: 80.85.245.188:32000/dadjokes:latest
imagePullPolicy: Always
ports:
- name: http
containerPort: 8000
protocol: TCP
Next, create a service.yaml
to define a Service resource that exposes our deployment to the internal network:
apiVersion: v1
kind: Service
metadata:
name: dadjokes-service
namespace: dadjokes
spec:
type: ClusterIP
ports:
- port: 8000
targetPort: http
protocol: TCP
name: http
selector:
app.kubernetes.io/name: dadjokes
Now, create a cluster-issuer.yaml
to define a ClusterIssuer resource that acts like certificate authorities that can generate signed certificates by honoring certificate signing requests. Replace your.email@gmail.com
with your email:
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: dadjokes-letsencrypt
namespace: dadjokes
spec:
acme:
email: your.email@gmail.com
privateKeySecretRef:
name: letsencrypt-private-key
server: https://acme-v02.api.letsencrypt.org/directory
solvers:
- http01:
ingress:
class: public
selector: {}
Create certificate.yaml
to define a Certificate for our site. Replace dadjokes.your-domain.xyz
with your domain:
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: dadjokes-certificate
namespace: dadjokes
spec:
secretName: tls-dadjokes
duration: 24h
renewBefore: 12h
commonName: dadjokes.your-domain.xyz
dnsNames:
- dadjokes.your-domain.xyz
issuerRef:
name: dadjokes-letsencrypt
kind: ClusterIssuer
And lastly, let's create ingress.yaml
to expose our application to the internet and also specify which certificate to use. Replace dadjokes.your-domain.xyz
with your domain:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: dadjokes-ingress
namespace: dadjokes
annotations:
cert-manager.io/cluster-issuer: dadjokes-letsencrypt
spec:
ingressClassName: nginx
rules:
- host: dadjokes.your-domain.xyz
http:
paths:
- backend:
service:
name: dadjokes-service
port:
number: 8000
path: /
pathType: Prefix
tls:
- hosts:
- dadjokes.your-domain.xyz
secretName: tls-dadjokes
We’re ready to apply all our configurations and access our service. Run:
kubectl apply -f service-account.yaml
kubectl apply -f deployment.yaml
kubectl apply -f service.yaml
kubectl apply -f cluster-issuer.yaml
kubectl apply -f certificate.yaml
kubectl apply -f ingress.yaml
After several seconds/minutes, check that pods are running and certificates are ready:
kubectl get pods,certificate
NAME READY STATUS RESTARTS AGE
pod/dadjokes-deployment-6d9ff9d4ff-dvhpq 1/1 Running 0 34s
NAME READY SECRET AGE
certificate.cert-manager.io/tls-dadjokes True tls-dadjokes 30s
certificate.cert-manager.io/dadjokes-certificate True tls-dadjokes 31s
After everything is applied you should be able to access our service at your domain using https.
If you’re unable to access the site, DNS propagation may be still in progress. To monitor the status, visit https://www.whatsmydns.net/#A. Once you observe that the majority of the locations are displaying your IP address, the site should become accessible.
Deploying a new version of an application
To deploy an updated version of your service, please follow the below steps.
Build and push a new docker image from dadjokes
directory with a new version tag, e.g. v2
:
docker build . -t 80.85.245.188:32000/dadjokes:v2
docker push 80.85.245.188:32000/dadjokes:v2
Update deployments.yaml
to use a new image tag:
...
containers:
- name: dadjokes
image: 80.85.245.188:32000/dadjokes:v2
...
Apply new configuration:
kubectl apply -f deployment.yaml
Closing thoughts
The process described in this tutorial unlocks plenty of opportunities for web development. With your own VPS server and Kubernetes, you can create all sorts of different applications and have full control over them. This guide illustrates one of the simple methods for deploying your application using quite advanced technologies like Docker and Kubernetes. Once the initial setup is complete, updating your service with new features and deploying them directly from your local machine becomes straightforward as described above. Kubernetes serves as an excellent resource for managing your application deployments and facilitating the construction of complex applications through code-driven configurations.
I hope this article was useful! Happy coding and deployments!
Top comments (0)