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
IngressRoutefornvme0n1p.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, titleHome | 溴化锂的笔记本 - Frontend service
/blog/apfs-sparsebundle-linux:200 - Public entry
/:200, titleHome | 溴化锂的笔记本 - 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> 22succeedssshhangs or fails during banner exchange
Likely cause:
sshdis overloaded or limiting new unauthenticated sessions
Mitigation:
- Avoid many parallel SSH probes
- Prefer a single session
- Run long builds with
nohupand inspectbuild.log
New Docker image is built but Pods still use the old one
Cause:
- K3s uses containerd, and
imagePullPolicy: Nevermeans 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.