SSD Nodes Learn 🎉 VPS desde $5.50/mes
Guías Matt ConnorPor Matt Connor

k3s en un VPS: cuándo merece la pena

k3s ofrece la API real de Kubernetes en un VPS. Revisa el consumo de RAM, el conflicto del puerto 80 al instalar y cuándo Docker Compose sigue siendo mejor opción.

Qué es k3s y qué ofrece un nodo

k3s es una distribución completa de Kubernetes empaquetada como un único binario. Ejecutarla en un VPS individual proporciona la API real de Kubernetes sin un plano de control de tres máquinas. Es una distribución de Kubernetes certificada. Por tanto, un manifiesto que se aplica aquí también se puede aplicar más adelante en un clúster gestionado. La instalación requiere un solo comando y tarda aproximadamente un minuto. El coste es memoria que deja de estar disponible para las aplicaciones, además de varios modos de fallo que no existen con Docker Compose.

SUSE desarrolla k3s para sitios periféricos e instalaciones pequeñas. Cada diferencia respecto a Kubernetes upstream existe para reducir su tamaño. El almacén de datos predeterminado es sqlite, mediante un shim llamado kine, no etcd. Por tanto, no hay que mantener un quórum de etcd. containerd está integrado en el binario en lugar de instalarse por separado. El mismo binario también incluye CoreDNS para el DNS del clúster, Traefik como controlador de ingress, ServiceLB (también llamado klipper-lb) para que los servicios de tipo LoadBalancer funcionen sin un proveedor cloud, local-path provisioner para volúmenes persistentes, metrics-server y flannel para la red de los pods. Todos ellos se inician de forma predeterminada. Por eso, el conflicto de puertos descrito más abajo es el primer problema más habitual en un VPS que ya estaba ejecutando otros servicios.

Cuándo merece la pena usar un solo nodo de k3s

Aplique esta regla. Use k3s cuando lo que necesita es la API de Kubernetes: está aprendiendo Kubernetes en una máquina que controla, o el software que quiere sólo publica un chart de Helm. La portabilidad de los manifiestos también cuenta, porque un Deployment que escriba aquí puede trasladarse sin cambios a un clúster gestionado. Use Docker Compose cuando lo que necesita son las aplicaciones. Compose inicia los mismos contenedores con muchos menos componentes, y un archivo de Compose en un VPS es más fácil de leer un año después que un directorio de manifiestos.

Debe tener claro qué no ofrece un solo nodo.

  • No ofrece alta disponibilidad. Cuando el VPS se reinicia, se detienen todas las cargas de trabajo. Kubernetes reprograma un pod en otro nodo, pero no existe otro nodo.
  • No ofrece una actualización gradual que mantenga el servicio disponible, a menos que la aplicación admita dos réplicas en una misma máquina que compartan un volumen.
  • Ofrece almacenamiento vinculado a ese equipo, por el motivo descrito en la sección sobre local-path que aparece a continuación.
  • Incluye un plano de control que consume alrededor de un gigabyte de RAM, tanto si despliega algo como si no.

Nada de esto convierte k3s en una mala opción. Lo convierte en una mala opción cuando el motivo es el que se suele mencionar: la fiabilidad. Si lo que realmente quiere son varias máquinas para crear un clúster multinodo real, esa decisión va primero: Proxmox en su propio hardware frente a un VPS alquilado determina de dónde salen los nodos antes de que k3s determine qué se ejecuta en ellos.

Qué coste tienen k3s en RAM y CPU antes de desplegar nada

El proyecto k3s publica cifras medidas, no estimaciones. Léelas con atención, porque la cifra que suele citarse no corresponde a un k3s inactivo.

ChartPublished k3s resource use, 95th percentile, Intel 8375C, k3s v1.26.5
The data behind this chart
[
  {
    "label": "Server, sqlite datastore",
    "ram_mb": "1,596",
    "cpu_percent_of_one_core": 6
  },
  {
    "label": "Server, embedded etcd",
    "ram_mb": "1,606",
    "cpu_percent_of_one_core": 6
  },
  {
    "label": "Agent node only",
    "ram_mb": "275",
    "cpu_percent_of_one_core": 3
  }
]

Un nodo servidor de esa prueba usó 1,596 MB de RAM en el percentil 95 y aproximadamente 6 por ciento de un núcleo. Son cifras publicadas, no mediciones de esta guía. La prueba ejecutó k3s v1.26.5 con todos los componentes incluidos habilitados, además de una pila de monitorización con Prometheus y Grafana. Por tanto, la cifra incluye una carga de trabajo real y no un clúster vacío. Sustituir sqlite por embedded etcd elevó el consumo a 1,606 MB. Un nodo agente, que ejecuta kubelet y containerd sin plano de control, usó 275 MB. El mínimo documentado para un servidor es de 2 núcleos y 2 GB de RAM. Ese mínimo cubre k3s y sus componentes incluidos antes de ejecutar sus cargas de trabajo.

La interpretación práctica es la siguiente: en un VPS de 2 GB, el plano de control y sus complementos incluidos dejan muy pocos recursos libres. Cuando aumenta la presión, lo primero que ocurre es que kubelet expulsa pods. 4 GB es un mínimo cómodo para un nodo con algunos servicios pequeños. Mide tu propio equipo en lugar de confiar en cualquier cifra publicada, incluida esta.

free -h
sudo k3s kubectl top node
sudo k3s kubectl get pods -A

Ejecuta free -h antes de instalar y de nuevo cuando todos los pods de kube-system indiquen Running. La diferencia muestra cuánto cuesta el plano de control en tu hardware. k3s kubectl top node devuelve error: Metrics API not available durante el primer minuto o los dos primeros minutos después de la instalación, porque metrics-server todavía no ha recopilado datos. No es un error. Si vas a dimensionar un solo equipo para esto y para otras tareas al mismo tiempo, aquí se aplica sin cambios el cálculo de RAM y CPU para un VPS.

Instalar k3s fijado a una versión, no a latest

La línea de inicio rápido que copia todo el mundo instala lo que indique el canal stable el día que se ejecuta. En una máquina que vaya a conservar, fíjelo. k3s publica un canal por cada versión minor de Kubernetes, por lo que INSTALL_K3S_CHANNEL=v1.36 sigue las versiones patch dentro de v1.36 y nunca cambia por sí solo a otra versión minor. En agosto de 2026, el canal stable apunta a v1.36.3+k3s1.

Escriba primero el archivo de configuración y después instale. k3s lee /etc/rancher/k3s/config.yaml al iniciarse, por lo que todo lo definido allí se aplica tanto al primer arranque como a los siguientes.

sudo mkdir -p /etc/rancher/k3s
sudo tee /etc/rancher/k3s/config.yaml >/dev/null <<'EOF'
tls-san:
  - k3s.example.com
EOF
curl -sfL https://get.k3s.io | INSTALL_K3S_CHANNEL=v1.36 sh -

Para fijar una versión exacta en lugar de un canal, use INSTALL_K3S_VERSION=v1.36.3+k3s1. El signo más forma parte de la etiqueta. Después, compruebe que se haya iniciado correctamente.

k3s --version
sudo systemctl status k3s
sudo k3s kubectl get node
sudo k3s kubectl get pods -A

kubectl get node debería mostrar un nodo con STATUS Ready en unos treinta segundos, y todos los pods de kube-system deberían alcanzar el estado Running o Completed. Un nodo atascado en NotReady suele indicar que el runtime de contenedores nunca se inició; por tanto, revise sudo journalctl -u k3s -n 100 --no-pager. En una imagen de VPS poco habitual, ejecute sudo k3s check-config antes de depurar cualquier otra cosa: informa de las funciones del kernel que faltan y proporciona una respuesta mucho más rápida que revisar los registros.

Por qué el puerto 80 ya está en uso y a qué debe renunciar para solucionarlo

Este es el fallo que suele aparecer en un VPS que ya estaba sirviendo algo. La instalación termina correctamente. Después, Traefik nunca obtiene una dirección y el sitio que ya estaba ejecutándose sigue funcionando. Por eso no se detecta ningún problema hasta que se intenta acceder a un recurso de entrada.

El mecanismo es el siguiente: el chart de Traefik incluido crea un Service de tipo LoadBalancer en los puertos 80 y 443. ServiceLB responde a esa solicitud creando un DaemonSet de pods pequeños, cuyos nombres empiezan por svclb-, que reclaman esos números de puerto como hostPort en cada nodo. hostPort publica el puerto del contenedor directamente en el espacio de nombres de red del propio nodo, exactamente igual que docker run -p 80:80. Si nginx, Caddy, Apache u otro contenedor ya utiliza el puerto 80, el kernel no puede asignarlo dos veces y el planificador no tiene ningún nodo donde colocar el pod.

sudo k3s kubectl -n kube-system get pods
sudo k3s kubectl -n kube-system get svc traefik
sudo ss -lntp '( sport = :80 or sport = :443 )'

Verá el pod svclb en estado Pending y el servicio sin dirección externa:

svclb-traefik-8f2c1a-r6k9x   0/2   Pending   0   3m
traefik   LoadBalancer   10.43.62.11   <pending>   80:31480/TCP,443:30219/TCP

La ejecución de kubectl -n kube-system describe pod svclb-traefik-... muestra directamente la causa:

0/1 nodes are available: 1 node(s) didn't have free ports for the requested pod ports.

ss -lntp indica qué proceso ocupa el puerto. Hay varias soluciones y cada una implica renunciar a algo.

Asigne los puertos a k3s. Detenga y deshabilite el servidor web existente y permita que Traefik administre los puertos 80 y 443. Esta es la opción adecuada cuando el VPS se utilizará exclusivamente como nodo de k3s y todo lo que se estaba sirviendo se trasladará detrás de un Ingress.

Deshabilite ServiceLB y conserve el proxy existente. Instale con --disable=servicelb. Un Service de tipo LoadBalancer sigue asignando un NodePort, por lo que Traefik continuará siendo accesible en un puerto alto, como 31480, y su nginx o Caddy hará proxy hacia 127.0.0.1:31480. A lo que renuncia es a la dirección externa: el servicio mostrará <pending> permanentemente. Esto parece un fallo, aunque en realidad es una decisión configurada por usted.

Deshabilite Traefik y enrute con su propio proxy. Instale con --disable=traefik. En ese caso no tendrá un controlador de entrada, por lo que los objetos Ingress no harán nada: permanecerán en la API sin ningún controlador que los supervise. Esto es correcto si enruta desde un proxy del host hacia los NodePorts y es la opción adecuada si ya sabe cómo quiere gestionar HTTP. Si aún no ha decidido qué debe quedar delante, resuelva la elección entre nginx, Caddy y Traefik como proxy inverso antes de deshabilitar nada.

Ambas opciones deben especificarse en el instalador o en el archivo de configuración:

tls-san:
  - k3s.example.com
disable:
  - traefik
  - servicelb

También puede editar ese archivo después de la instalación y ejecutar sudo systemctl restart k3s, porque --disable no sólo omite un componente durante la instalación. También elimina un componente que ya está desplegado, por lo que el cambio se aplica en un clúster en ejecución.

Para conservar Traefik pero cambiar la configuración del chart, no edite /var/lib/rancher/k3s/server/manifests/traefik.yaml. k3s reescribe ese archivo con los valores predeterminados cada vez que se inicia. Añada otro archivo en el mismo directorio, porque todo lo que se encuentra en /var/lib/rancher/k3s/server/manifests se aplica automáticamente durante el arranque y cada vez que cambia en el disco.

apiVersion: helm.cattle.io/v1
kind: HelmChartConfig
metadata:
  name: traefik
  namespace: kube-system
spec:
  valuesContent: |-
    ports:
      web:
        forwardedHeaders:
          trustedIPs:
            - 10.0.0.0/8

Este ejemplo establece un valor del chart de Traefik: las direcciones de proxy de confianza. El mismo mecanismo permite establecer cualquier otro valor que exponga el chart, incluidos sus puertos.

Almacenamiento persistente en un nodo

k3s incluye una StorageClass predeterminada llamada local-path, respaldada por el provisionador local-path de Rancher. Un PersistentVolumeClaim sin storageClassName la utiliza. Los volúmenes se almacenan en /var/lib/rancher/k3s/storage, con un subdirectorio por volumen, en el disco local del nodo.

sudo k3s kubectl get storageclass
sudo k3s kubectl get pvc -A
sudo ls -l /var/lib/rancher/k3s/storage

La expresión «en el disco local del nodo» tiene dos consecuencias. Ambas suelen causar problemas más adelante, no de inmediato.

La StorageClass usa volumeBindingMode: WaitForFirstConsumer, por lo que un PVC nuevo permanece en estado Pending hasta que un pod lo monta. kubectl describe pvc muestra:

waiting for first consumer to be created before binding

Esto es normal. Por tanto, crear un PVC por separado y esperar no hará que cambie de estado.

Una vez enlazado, el volumen incluye una afinidad de nodo hacia el nodo que lo creó. Esto obliga a que todos los pods que usen esa solicitud permanezcan en ese nodo durante toda la vida del volumen. En un clúster de un solo nodo no lo notará. Si añade un segundo nodo más adelante, un pod que no puede moverse parecerá un error del planificador hasta que ejecute kubectl get pv -o yaml y encuentre el nombre de host en nodeAffinity.

Las copias de seguridad son responsabilidad suya. Reconstruir el VPS elimina ese directorio, al igual que el script de desinstalación que aparece más adelante. Haga una copia de /var/lib/rancher/k3s/storage y también del almacén de datos sqlite en /var/lib/rancher/k3s/server/db/state.db, copiándolo con el servicio detenido, porque es una base de datos activa. La alternativa es tratar el clúster como desechable y mantener todos los manifiestos en git.

Entrada y TLS

Con Traefik habilitado, sólo necesita un objeto Ingress estándar.

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: hello
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt
spec:
  ingressClassName: traefik
  tls:
    - hosts:
        - hello.example.com
      secretName: hello-tls
  rules:
    - host: hello.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: hello
                port:
                  number: 80

Los certificados no aparecen por sí solos. La opción habitual es cert-manager, instalado desde su manifiesto publicado, junto con un ClusterIssuer. La versión v1.21.1 es la actual a agosto de 2026.

sudo k3s kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.21.1/cert-manager.yaml
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt
spec:
  acme:
    email: you@example.com
    server: https://acme-v02.api.letsencrypt.org/directory
    privateKeySecretRef:
      name: letsencrypt-account-key
    solvers:
      - http01:
          ingress:
            ingressClassName: traefik

El desafío HTTP-01 requiere que el servidor ACME (entorno de gestión automática de certificados) se conecte a http://hello.example.com/.well-known/acme-challenge/... desde Internet público. Por tanto, el registro DNS A ya debe apuntar al VPS y el puerto 80 debe llegar a Traefik. Si deshabilitó ServiceLB y colocó su propio proxy delante, ese proxy también debe reenviar la ruta del desafío. De lo contrario, cert-manager se queda bloqueado y muestra esto en el objeto Challenge:

Waiting for HTTP-01 challenge propagation: wrong status code '404', expected '200'

Supervise la emisión con sudo k3s kubectl describe certificate hello-tls y sudo k3s kubectl get order,challenge -A.

El kubeconfig y por qué el servidor de API permanece privado

k3s escribe las credenciales de administrador en /etc/rancher/k3s/k3s.yaml. El archivo pertenece a root y, de forma predeterminada, se escribe con el modo 600. Contiene un certificado de cliente con permisos de cluster-admin, por lo que cualquier persona que pueda leerlo controla el clúster.

sudo k3s kubectl get node
export KUBECONFIG=/etc/rancher/k3s/k3s.yaml
kubectl get node

Un kubectl instalado por separado sin KUBECONFIG definido falla con The connection to the server localhost:8080 was refused - did you specify the right host or port? porque recurre a un valor predeterminado que no tiene relación con k3s. Defina KUBECONFIG o use sudo k3s kubectl, que lee por sí solo el archivo correcto.

Verá que se recomienda --write-kubeconfig-mode 644 para que un usuario normal pueda ejecutar kubectl. Debe entender qué implica: hace que una credencial de cluster-admin sea legible para todas las cuentas locales del equipo. En un servidor con un único administrador, puede ser una concesión aceptable. En un servidor compartido, no lo es. Copiar el archivo proporciona acceso a un solo usuario sin publicarlo:

mkdir -p ~/.kube
sudo install -o $USER -g $USER -m 600 /etc/rancher/k3s/k3s.yaml ~/.kube/config

La línea server: de ese archivo contiene https://127.0.0.1:6443. Para usar kubectl desde su portátil, no abra 6443 a Internet. Una API de Kubernetes pública es un objetivo permanente, y una API expuesta permite que clústeres pequeños terminen minando criptomonedas para otra persona. Cree un túnel mediante SSH y deje intacta la dirección del archivo:

ssh -N -L 6443:127.0.0.1:6443 user@your-vps
KUBECONFIG=./k3s.yaml kubectl get node

Si debe acceder a la API mediante una dirección de red privada, instale con tls-san indicando ese nombre o dirección y, después, edite la línea server: del archivo copiado para que coincida. Sin la entrada SAN (nombre alternativo del sujeto), kubectl rechaza la conexión:

x509: certificate is valid for 127.0.0.1, 10.43.0.1, not 203.0.113.10

Los puertos de entrada documentados para un clúster son TCP 6443 para la API, UDP 8472 para VXLAN de flannel entre nodos y TCP 10250 para las métricas de kubelet. En un nodo único, ninguno debe estar abierto a Internet.

containerd no es Docker

k3s ejecuta su propio containerd integrado y no comparte el almacén de imágenes con Docker. Una imagen que acaba de crear con docker build no es visible para k3s, por lo que el pod falla con ErrImagePull aunque docker images la muestre. Impórtela explícitamente:

docker save myapp:0.1 | sudo k3s ctr images import -
sudo k3s crictl images

Después, evite la etiqueta :latest en ese contenedor, porque :latest usa de forma predeterminada un imagePullPolicy de Always y kubelet intenta acceder de todos modos a un registro. Cualquier otra etiqueta usa de forma predeterminada IfNotPresent, que utiliza la imagen importada. Ejecutar ambos en un VPS funciona, y conviene saber que una instalación normal de Docker en un VPS y k3s mantienen cada uno su propio almacén de imágenes y sus propias reglas de iptables en la misma máquina.

Cómo eliminar k3s

El instalador crea un script de desinstalación. No existe una eliminación parcial ni una opción para deshacerla.

sudo /usr/local/bin/k3s-uninstall.sh

Detiene y elimina el servicio, borra el almacén de datos, elimina los datos de los volúmenes persistentes, quita la configuración del nodo y elimina las herramientas que añadió el instalador. En un nodo agente, el script es k3s-agent-uninstall.sh. Copie primero fuera del equipo todo lo que esté bajo /var/lib/rancher/k3s/storage, porque ese directorio también se elimina. Después, compruebe que no quede ningún proceso reteniendo puertos o interfaces con ip link show y sudo ss -lntp. Una interfaz cni0 o flannel.1 restante desaparece en el siguiente reinicio.

Decidir que un nodo de Kubernetes aportaba más complejidad de la necesaria es un resultado normal, no un fallo. Trasladar esas cargas de trabajo de nuevo a Compose suele llevar una tarde.

Modos de fallo y mensajes que verá

Node NotReady o k3s se reinicia continuamente. Lea primero sudo journalctl -u k3s -n 200 --no-pager. En un VPS pequeño, la causa habitual es que el kernel finalice el proceso por falta de memoria, lo que aparece en dmesg como una línea que menciona k3s-server. El mínimo documentado de 2 GB es un límite real.

Un Pod permanece en Pending. kubectl describe pod lo indica siempre. Insufficient memory o Insufficient cpu significa que el nodo ya no tiene espacio disponible. didn't have free ports corresponde a la colisión de hostPort anterior. waiting for first consumer en un PVC significa que WaitForFirstConsumer está funcionando correctamente.

ImagePullBackOff. La etiqueta no existe en ningún registro al que el nodo pueda acceder, o creó la imagen con Docker y nunca la importó en containerd.

Traefik responde, pero la aplicación no. Un cuerpo de respuesta con 404 page not found procede del propio Traefik e indica que la petición llegó, pero ningún router coincidió. Compruebe que el host de Ingress coincida con el nombre que escribió y que ingressClassName sea traefik.

El DNS del clúster falla, pero el host resuelve correctamente. Compruebe CoreDNS con sudo k3s kubectl -n kube-system logs -l k8s-app=kube-dns. Un mensaje como plugin/loop: Loop ... detected for zone "." impide que CoreDNS se inicie. Ocurre porque CoreDNS reenvía las consultas a un resolver que las reenvía de vuelta a CoreDNS, que es lo que provoca una dirección de loopback en /etc/resolv.conf. Indique a k3s el archivo de upstream real mediante --resolv-conf /run/systemd/resolve/resolv.conf.

FAQ

¿Merece la pena ejecutar k3s en un único VPS?

Sí, cuando necesita la API de Kubernetes: para aprender a usarla en una máquina que controla, mantener las implementaciones como manifiestos portables, ejecutar software que sólo publica un chart de Helm o crear algo que después trasladará a un clúster gestionado. No merece la pena si sólo quiere ejecutar contenedores, porque Docker Compose lo hace con mucho menos mantenimiento y deja aproximadamente un gigabyte más de RAM libre. Un solo nodo no proporciona alta disponibilidad, por lo que la fiabilidad nunca es un motivo para elegirlo.

¿Por qué mi servicio LoadBalancer de k3s permanece en Pending?

ServiceLB crea pods svclb- que reservan los puertos del servicio como hostPort en el nodo, por lo que sólo se programan donde esos puertos están libres. Si nginx u otro proxy ya ocupa el puerto 80, el pod permanece en Pending y el servicio nunca recibe una dirección externa. kubectl -n kube-system describe pod svclb-... informa de 0/1 nodes are available: 1 node(s) didn't have free ports for the requested pod ports. Libere el puerto o reinstale con --disable=servicelb y haga proxy al NodePort que el servicio sigue asignando.

¿Cuánta RAM necesita k3s en un VPS?

El mínimo documentado para un nodo servidor es de 2 cores y 2 GB, y eso cubre k3s y sus componentes incluidos antes de ejecutar sus cargas de trabajo. El perfilado del propio proyecto midió 1,596 MB para un nodo servidor con una pila de monitorización en ejecución, así que considere 2 GB como el mínimo y 4 GB como el primer tamaño con el que un nodo funciona con margen. Mida su propia máquina con free -h antes de la instalación y de nuevo después de que cada pod de kube-system indique Running.

¿Puedo ejecutar Docker y k3s en el mismo VPS?

Sí, y permanecen separados. k3s usa su propio containerd integrado, por lo que una imagen creada con docker build no estará visible para k3s hasta que ejecute docker save myapp:0.1 | sudo k3s ctr images import -. Cada uno escribe sus propias reglas de iptables y sus propias redes puente. Vigile la memoria total, porque Docker más k3s y sus contenedores no cabrán en una máquina de 2 GB.

¿Cómo elimino k3s por completo?

Ejecute sudo /usr/local/bin/k3s-uninstall.sh en un nodo servidor o sudo /usr/local/bin/k3s-agent-uninstall.sh en un agent. Detiene el servicio, elimina el almacén de datos, borra los datos de los volúmenes persistentes bajo /var/lib/rancher/k3s/storage y elimina las herramientas incluidas. Copie primero fuera de la máquina cualquier dato que quiera conservar, porque no hay forma de deshacer la operación. Una interfaz de red cni0 o flannel.1 que quede se elimina en el siguiente reinicio.

#kubernetes#k3s#docker-compose#ingress#sizing