Once your user (authentication) service runs in a Docker container, Kubernetes can run several copies of it, restart them when they fail, and roll out new versions without downtime. This guide takes the image from Containerizing the User Service with Docker and deploys it to a Kubernetes cluster you run yourself: a registry push, a Secret, a Deployment with health checks and a non-root security context, a Service, an Ingress, and how to update and roll back.
Push the image to a registry, store MONGODB_URI and JWT_SECRET in a Kubernetes Secret, and describe the service in a Deployment with 2-3 replicas, CPU and memory requests, readiness and liveness probes on /health, and a security context that runs as the non-root node user. Expose it inside the cluster with a Service and to the internet with an Ingress, then apply everything with kubectl apply -f and watch it with kubectl rollout status. Kubernetes needs a server or cluster you control, such as k3s on a VPS; it can't run on shared hosting.
1. What you need
- A Kubernetes cluster you control. For one server, k3s on a VPS is the simplest option; a managed Kubernetes service from a cloud provider works the same way.
kubectlon your computer or the server, configured for that cluster.kubectl get nodesshould list your nodes asReady.- The user-service image built from the Docker guide:
node:24-slim,npm ci --omit=dev, running as thenodeuser, listening on0.0.0.0and the port inPORT(8080), with aGET /healthroute. - A container registry the cluster can pull from, such as GitHub Container Registry or Docker Hub.
- MongoDB the pods can reach: a managed MongoDB service, or a database you run yourself.
Kubernetes can't run on shared hosting (cPanel, DirectAdmin, Webuzo or Windows). It needs root access, its own container runtime and kernel features that shared hosting does not allow. Use a VPS, where you install and manage the cluster yourself, or skip Kubernetes and deploy a single service to the App Platform.
2. Push the image to a registry
Kubernetes nodes pull images from a registry, so tag the image with the registry address and push it. Use a version tag rather than latest, so every rollout points at an exact build and you can roll back.
docker build -t ghcr.io/your-org/user-service:1.0.0 .
docker push ghcr.io/your-org/user-service:1.0.0If the registry is private, give the cluster read-only credentials:
kubectl create namespace user-service
kubectl -n user-service create secret docker-registry regcred \
--docker-server=ghcr.io \
--docker-username=YOUR_USER \
--docker-password=YOUR_READ_ONLY_TOKEN3. Store configuration and secrets
Keep the database address and signing keys out of the image and out of your manifests. Put them in a Secret, created from the same .env file you used with docker run --env-file:
kubectl create namespace user-service # skip if you created it in step 2
kubectl -n user-service create secret generic user-service-env \
--from-env-file=.envKubernetes Secrets are only base64-encoded, not encrypted, in the manifest. Don't commit Secret YAML to Git; limit who can read Secrets with RBAC, and consider encryption at rest or a tool such as Sealed Secrets or External Secrets for production.
4. Write the Deployment
Save this as user-service.yaml. It runs three replicas, sends traffic only to pods that pass their health check, and locks each container down.
apiVersion: apps/v1
kind: Deployment
metadata:
name: user-service
namespace: user-service
labels:
app: user-service
spec:
replicas: 3
selector:
matchLabels:
app: user-service
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 0
maxSurge: 1
template:
metadata:
labels:
app: user-service
spec:
imagePullSecrets:
- name: regcred # remove if the image is public
securityContext:
runAsNonRoot: true
runAsUser: 1000 # the "node" user in official Node.js images
runAsGroup: 1000
seccompProfile:
type: RuntimeDefault
containers:
- name: user-service
image: ghcr.io/your-org/user-service:1.0.0
ports:
- name: http
containerPort: 8080
env:
- name: PORT
value: "8080"
- name: NODE_ENV
value: production
envFrom:
- secretRef:
name: user-service-env
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
memory: 256Mi
readinessProbe:
httpGet:
path: /health
port: http
periodSeconds: 10
livenessProbe:
httpGet:
path: /health
port: http
initialDelaySeconds: 15
periodSeconds: 20
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
volumeMounts:
- name: tmp
mountPath: /tmp
volumes:
- name: tmp
emptyDir: {}| Setting | Why it matters |
|---|---|
| replicas: 3 and maxUnavailable: 0 | New pods must be ready before old ones stop, so an update causes no downtime |
| readinessProbe | A pod receives traffic only while /health answers, for example after it has connected to MongoDB |
| livenessProbe | Kubernetes restarts a container that stops answering |
| resources | Requests let the scheduler place pods sensibly; the memory limit stops one leak from starving the node |
| runAsNonRoot and runAsUser 1000 | Matches the USER node line in the Dockerfile; the pod refuses to start as root |
| readOnlyRootFilesystem with /tmp | The app can't modify its own code; /tmp stays writable for libraries that need it |
Size the requests and limits from real usage: kubectl top pods shows it once metrics-server is running (k3s includes it). The Docker image's own HEALTHCHECK is ignored by Kubernetes; the probes replace it.
5. Add a Service and an Ingress
A Service gives the pods one stable internal address and load-balances across them. An Ingress routes a public hostname to that Service. Add both to the same file, separated by ---:
---
apiVersion: v1
kind: Service
metadata:
name: user-service
namespace: user-service
spec:
selector:
app: user-service
ports:
- name: http
port: 80
targetPort: http
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: user-service
namespace: user-service
spec:
ingressClassName: traefik # k3s default; use nginx or your controller's class
rules:
- host: api.yourdomain.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: user-service
port:
name: httpPoint an A record for api.yourdomain.com to your server's public IP wherever your domain's DNS is managed. For HTTPS, add a tls section with a certificate from cert-manager and Let's Encrypt, or use the Traefik certificate resolver shown in the k3s guide.
6. Deploy and check
- Apply the manifests.
kubectl apply -f user-service.yaml. Kubernetes creates or updates everything in the file; running it again is safe. - Wait for the rollout.
kubectl -n user-service rollout status deployment/user-servicereturns when all replicas are ready. - Check the pods.
kubectl -n user-service get pods -o wideshould show three podsRunningwithREADY 1/1. - Read the logs.
kubectl -n user-service logs deploy/user-serviceshows one pod's output; add-fto follow it. - Test before going public.
kubectl -n user-service port-forward svc/user-service 8080:80, thencurl -i http://localhost:8080/healthfrom the same machine should return200 ok. - Test the public route.Once DNS points at the server,
curl -i https://api.yourdomain.com/health.
If a pod is stuck, kubectl -n user-service describe pod POD_NAME shows the reason at the bottom:
- ImagePullBackOff: wrong image name or tag, or missing registry credentials.
- CrashLoopBackOff: the app exits on start; read
kubectl -n user-service logs POD_NAME --previous, often a missing variable in the Secret. - Running but not Ready:
/healthisn't answering on port 8080 at0.0.0.0. - CreateContainerConfigError: the Secret named in
envFromdoesn't exist in that namespace.
7. Update and roll back
Build and push a new tag, then point the Deployment at it:
docker build -t ghcr.io/your-org/user-service:1.1.0 .
docker push ghcr.io/your-org/user-service:1.1.0
kubectl -n user-service set image deployment/user-service \
user-service=ghcr.io/your-org/user-service:1.1.0
kubectl -n user-service rollout status deployment/user-serviceIf the new version misbehaves, kubectl -n user-service rollout undo deployment/user-service returns to the previous one. For a lasting record, change the tag in user-service.yaml and apply the file, so Git always matches what runs. When the secret values change, recreate the Secret and run kubectl -n user-service rollout restart deployment/user-service, because running pods don't reload environment variables.
For CI-driven deploys, see GitHub Actions CI/CD to a VPS; to package these manifests for reuse, see Helm charts for beginners.
8. Running this on Domain India
| Option | Kubernetes? | Best for |
|---|---|---|
| Shared hosting (cPanel, DirectAdmin, Webuzo, Windows) | No | Websites and PHP apps; containers can't run here |
| App Platform | No, it runs your app for you | One service deployed from GitHub or with a deploy token, without managing a cluster |
| VPS | Yes, you install it | Your own self-managed k3s node or cluster with root access |
A Domain India VPS is self-managed: you install k3s, and you are responsible for updates, the firewall and backups. A single-node k3s cluster with this service and its system components fits comfortably on a VPS with 4 GB of RAM; add more memory if you also run MongoDB, monitoring or other services on the same node. Open ports 80 and 443 in the VPS firewall for the Ingress, and keep the Kubernetes API (6443) closed to the internet or limited to your own IP.
- 2 vCPU
- 4 GB DDR4 RAM
- 128 GB NVMe SSD Storage
- 3 TB Monthly Bandwidth
If you only need to run this one service, the App Platform is simpler: Node.js apps are detected automatically, you deploy with Deploy Now or a deploy token, and there is no cluster to maintain. MongoDB is not included there, so use a hosted MongoDB service and test the connection from your app. See Getting started with the App Platform.
Frequently asked questions
Can I run Kubernetes on Domain India shared hosting?
No. Kubernetes needs root access, a container runtime and kernel features that shared hosting does not provide. Use a Domain India VPS, where you install a distribution such as k3s yourself, or deploy the service to the App Platform instead.
How much RAM does a small k3s cluster need?
k3s itself runs in well under 1 GB, but a practical single-node cluster running an API service, its system components and some headroom is comfortable on 4 GB of RAM. Running MongoDB, monitoring or several services on the same node needs more.
Why is my pod in ImagePullBackOff?
The node can't pull the image. Check that the image name and tag exist in the registry, and for a private registry that the imagePullSecrets entry names a docker-registry Secret in the same namespace with valid read-only credentials.
Why does my pod keep restarting with CrashLoopBackOff?
The container starts and exits. Run kubectl logs on the pod with --previous to see the last error. The usual causes are a missing environment variable in the Secret, a database the pod can't reach, or the app listening on the wrong port or address.
How do I pass MONGODB_URI and JWT_SECRET to the pods?
Create a Kubernetes Secret, for example with kubectl create secret generic user-service-env --from-env-file=.env, and reference it with envFrom in the Deployment. Never put secret values in the image or commit them to Git.
How do I roll back a bad deployment?
Run kubectl rollout undo deployment/user-service in the service's namespace. Kubernetes returns to the previous version using the same rolling update, so the service stays available while it switches back.
Do I need Kubernetes for one small service?
Usually not. Kubernetes pays off when you run several services or need automatic restarts and rolling updates across more than one copy. For a single Node.js service, Docker Compose on a VPS or the App Platform is simpler to run and maintain.
Ready to run your own cluster? Compare VPS plans, set up k3s on a VPS, look at the App Platform if you'd rather not manage a cluster, or open a support ticket if you're not sure which fits.
Root access on a self-managed VPS, so you can install k3s and run your containers the way you want.
See VPS plans