After describing how to create production-ready Docker images and upload them to Docker Hub in the last article, it is now time to make these images available on a server. The aim is to make the web application accessible to everyone via a dedicated domain. To do this, we use a virtual private server (VPS) from Hetzner and deploy Kubernetes (k3s) with Caddy as a reverse proxy.
You might also be interested in this: Create Docker images and upload them to Dockerhub
Set up a VPS with Hetzner
Hetzner often offers referral links with credit benefits for new customers. Of course, you can also use other providers, but Hetzner is attractively priced and offers solid services.
What is a VPS?
A Virtual Private Server (VPS) is a virtual server that is operated on a physical machine and acts as an independent server. It offers more control than classic shared hosting and is a cost-effective alternative to dedicated servers. Access is usually via SSH (Secure Shell), which allows us to control the server via the command line.
SSH access to the VPS
Once a VPS has been created, it is usually managed via a secure shell (SSH). SSH is a protocol that enables encrypted connections to remote servers. The following command is used to connect to the server:
ssh root@<IP-Server>
If an SSH key has been stored, authentication can be carried out using public key authentication, which is more secure than a password.
Create a server at Hetzner
- After logging into the Hetzner Cloud, we navigate to “Projects” and create a new project.
- Select “Add server” and can configure an instance.
- The cheapest model is often sufficient to start with. However, I recommend activating the option for an IPv4 address, as purely IPv6-based setups often cause compatibility problems.
Setting up a domain
To access the application later under your own domain, you must register a domain and link it to the server.
Apply for a domain at Hetzner
- Register a new domain or add an existing domain in the Hetzner ConsoleH.
- To manage DNS entries, we need to activate DNS access.
Set name servers
The following name servers should be used:
helium.ns.hetzner.de. hydrogen.ns.hetzner.com. oxygen.ns.hetzner.com.
These new name servers offer better performance and flexibility compared to the old Hetzner name servers:
ns1.first-ns.de. robotns2.second-ns.de. robotns3.second-ns.com.
However, both nameserver variants are possible! The DNS changes take some time. However, we can use tools such as MXToolbox to check whether the changes have already taken place.
Connect the domain to the server
Now the IP address of the server must be linked to the domain:
- Switch to DNS zones in the Hetzner Cloud.
- Select the registered domain.
- Create a new A-Record and enter the IPv4 address of the server.
- If available, remove the IPv6 record (AAAA) to avoid compatibility problems.
You can also use MXToolbox to check whether the DNS changes have already been applied.
Set up Kubernetes
Kubernetes is a powerful orchestration tool for containers. I use k3s, a lean Kubernetes variant that is particularly suitable for smaller environments.
Install K3s on the server
Connect to the server via SSH and install k3s with the following command:
curl -sfL https://get.k3s.io | sh -
The script installs k3s and starts the Kubernetes service. After installation, k3s can be checked with the following command:
kubectl get nodes
k3s comes with its own kubectl version, so that no separate installation is necessary.
Create YAML files for FE, BE, MySQL and Redis
To deploy our application, we need YAML files for:
- Frontend (Angular)
- Backend (NestJS)
- Database (MySQL)
- Session-Management (Redis)
A deployment file for the backend could look like this:
apiVersion: apps/v1 kind: Deployment metadata: name: backend spec: replicas: 1 selector: matchLabels: app: backend template: metadata: labels: app: backend spec: containers: - name: backend image: dockerhub-user/backend:latest ports: - containerPort: 3000 --- apiVersion: v1 kind: Service metadata: name: backend-service spec: selector: app: backend ports: - protocol: TCP port: 80 targetPort: 3000 type: ClusterIP
What are deployments and services?
- Deployments manage the provision and scaling of containers.
- Services ensure a stable network connection between containers.
- ClusterIP means that the service is only accessible within the Kubernetes cluster.
Set up Caddy as a reverse proxy
A reverse proxy is required to ensure that incoming traffic is distributed correctly. K3s comes with Traefik by default, but I opted for a simpler solution: Caddy. I was really surprised how little guidance or documentation there is on Caddy in combination with Kubernetes.
Why Kubernetes with Caddy?
- Automatic Let’s Encrypt SSL certificates
- Simple configuration via Caddyfile
- Built-in load balancer
Remove Traefik
kubectl delete helmrelease traefik -n kube-system
Create Caddy Deployment
apiVersion: apps/v1 kind: Deployment metadata: name: caddy spec: replicas: 1 selector: matchLabels: app: caddy template: metadata: labels: app: caddy spec: containers: - name: caddy image: caddy volumeMounts: - name: caddy-config mountPath: /etc/caddy/Caddyfile volumes: - name: caddy-config configMap: name: caddy-config --- apiVersion: v1 kind: ConfigMap metadata: name: caddy-config data: Caddyfile: | example.com { reverse_proxy backend-service:3000 } --- apiVersion: v1 kind: Service metadata: name: caddy-service spec: type: LoadBalancer selector: app: caddy ports: - port: 80 targetPort: 80 - port: 443 targetPort: 443
Important: Since Let’s Encrypt has a rate limit, tests should first be carried out with staging certificates!
Conclusion
After these steps, the application is now running in a Kubernetes cluster on a Hetzner VPS and can be accessed via its own domain. The next step would be to set up an automatic CI/CD pipeline to deploy new versions without manual effort.