Files
blog-astro/docs/deploy-k3s.md
2026-05-10 14:29:49 +08:00

6.1 KiB

blog_astro K3s Deployment

Last updated: 2026-04-06

Overview

This project is deployed to the notion-stack namespace in the existing K3s cluster.

Current traffic topology:

  • Public entry: Traefik IngressRoute for nvme0n1p.dev
  • Public service target: notion-blog-api:3000
  • Frontend upstream inside cluster: http://blog-astro:3000
  • Frontend runtime: Astro SSR in a container listening on 3000
  • Preferred build host: root@10.66.66.3
  • Fallback build host: root@156.239.238.24

Important: the public Ingress does not point directly to blog-astro. The site is exposed through notion-blog-api, which proxies the frontend via FRONTEND_UPSTREAM=http://blog-astro:3000.

Why Build Remotely

Local development may happen on a non-amd64 machine, while the K3s node running this workload is amd64. To avoid cross-architecture image issues, build the image on the amd64 Ubuntu host, then import it into K3s containerd on that same host.

The frontend workload is pinned to the ubuntu node.

Current Kubernetes Layout

  • Namespace: notion-stack
  • Deployment: blog-astro
  • Service: blog-astro
  • Container port: 3000
  • Image pull mode: Never
  • Node selector: kubernetes.io/hostname=ubuntu

Useful checks:

ssh root@10.66.66.3 'kubectl -n notion-stack get deploy,svc,pods -l app=blog-astro -o wide'
ssh root@10.66.66.3 'kubectl -n notion-stack get ingressroute notion-stack -o yaml'
ssh root@10.66.66.3 'kubectl -n notion-stack get deploy notion-blog-api -o yaml'

Deployment Procedure

1. Sync the current working tree to the remote build host

This pushes the current local state, including uncommitted changes.

rsync -az --delete \
  --exclude '.git' \
  --exclude 'node_modules' \
  --exclude 'dist' \
  --exclude '.astro' \
  --exclude '.vercel' \
  --exclude '.DS_Store' \
  ./ root@10.66.66.3:/root/deploy/blog_astro/

If 10.66.66.3 is unavailable, use:

rsync -az --delete \
  --exclude '.git' \
  --exclude 'node_modules' \
  --exclude 'dist' \
  --exclude '.astro' \
  --exclude '.vercel' \
  --exclude '.DS_Store' \
  ./ root@156.239.238.24:/root/deploy/blog_astro/

2. Build a new uniquely tagged image on the remote host

Do not reuse blog-astro:deploy for the build itself. Use a timestamped tag first so the rollout target is explicit.

Example:

ssh root@10.66.66.3 '
  cd /root/deploy/blog_astro &&
  docker build -t blog-astro:deploy-20260406-1251 .
'

If the SSH session is unstable, run the build in the background and log to a file:

ssh root@10.66.66.3 '
  cd /root/deploy/blog_astro &&
  rm -f build.log build.ok build.fail &&
  nohup sh -lc '\''docker build -t blog-astro:deploy-20260406-1251 . > build.log 2>&1 && echo ok > build.ok || echo fail > build.fail'\'' >/dev/null 2>&1 &
'

ssh root@10.66.66.3 '
  cd /root/deploy/blog_astro &&
  if [ -f build.ok ]; then
    echo BUILD_OK
  elif [ -f build.fail ]; then
    echo BUILD_FAIL
  else
    echo BUILD_RUNNING
  fi &&
  tail -n 40 build.log
'

3. Import the built image into K3s containerd

The Docker daemon image is not automatically visible to K3s. Import it explicitly:

ssh root@10.66.66.3 '
  docker save blog-astro:deploy-20260406-1251 | ctr -n k8s.io images import -
'

Verify the imported image exists:

ssh root@10.66.66.3 '
  ctr -n k8s.io images ls | grep "blog-astro.*deploy-20260406-1251"
'

4. Update the Deployment to the new image

ssh root@10.66.66.3 '
  kubectl -n notion-stack set image deployment/blog-astro \
    blog-astro=blog-astro:deploy-20260406-1251 &&
  kubectl -n notion-stack rollout status deployment/blog-astro --timeout=180s
'

Inspect the result:

ssh root@10.66.66.3 '
  kubectl -n notion-stack get deploy blog-astro -o wide &&
  kubectl -n notion-stack get pods -l app=blog-astro -o wide
'

Verification

Verify the frontend service directly

ssh root@10.66.66.3 '
  curl -sS -o /tmp/blog_home.html -w "%{http_code}\n" http://10.43.98.176:3000/ &&
  grep -o "<title>[^<]*</title>" /tmp/blog_home.html | head -n 1
'

Verify through the public entrypoint

This confirms Traefik -> notion-blog-api -> blog-astro is still correct.

ssh root@10.66.66.3 '
  curl -sS -H "Host: nvme0n1p.dev" \
    -o /tmp/ext_home.html \
    -w "%{http_code}\n" \
    http://127.0.0.1:31349/ &&
  grep -o "<title>[^<]*</title>" /tmp/ext_home.html | head -n 1
'

2026-04-06 verification example

  • New image: blog-astro:deploy-20260406-1251
  • New pod: blog-astro-58f9c8cf4c-bj9zv
  • Frontend service /: 200, title Home | 溴化锂的笔记本
  • Frontend service /blog/apfs-sparsebundle-linux: 200
  • Public entry /: 200, title Home | 溴化锂的笔记本
  • Public entry /blog/apfs-sparsebundle-linux: 200

Rollback

List old ReplicaSets and images:

ssh root@10.66.66.3 '
  kubectl -n notion-stack get rs -l app=blog-astro &&
  ctr -n k8s.io images ls | grep blog-astro
'

Rollback by switching the Deployment image back to the previous known-good tag:

ssh root@10.66.66.3 '
  kubectl -n notion-stack set image deployment/blog-astro \
    blog-astro=blog-astro:deploy
'

If the previous version also used a unique tag, set that exact tag instead.

Troubleshooting

SSH connects on port 22 but does not return a banner

Symptom:

  • nc -vz <host> 22 succeeds
  • ssh hangs or fails during banner exchange

Likely cause:

  • sshd is overloaded or limiting new unauthenticated sessions

Mitigation:

  • Avoid many parallel SSH probes
  • Prefer a single session
  • Run long builds with nohup and inspect build.log

New Docker image is built but Pods still use the old one

Cause:

  • K3s uses containerd, and imagePullPolicy: Never means Kubernetes only sees images already imported into containerd

Fix:

ssh root@10.66.66.3 '
  docker save blog-astro:<tag> | ctr -n k8s.io images import -
'

Public site still works even though Ingress does not point to blog-astro

This is expected. Public traffic goes:

Traefik -> notion-blog-api -> blog-astro

So frontend deployments normally do not require Ingress changes.