Loading...
Loading...
Deploy a docker-compose app via a Jenkins INBOUND AGENT running on the target server (no SSH from controller), with Docker-secrets "L3" hardening — runtime secrets live on tmpfs and never appear in `docker inspect` nor in `.env`. Use when the user wants: "deploy via jenkins agent", "no-ssh deploy", "agent thay vì ssh", "docker secrets L3", "ẩn secret khỏi docker inspect", "secret không nằm trên đĩa", or is wiring a Jenkins pipeline that deploys docker-compose to a Linux host. Encodes the gotchas learned from a real 1:1 run (Java 21, 0444 secret perms, 0400 file-credential, agent user needs docker+sudo).
npx skill4agent add rheinmir/setup jenkins-agent-l3-deploydeploy.pydocker compose/run/payroll-sec/run/secrets/<name>docker inspect.envdocker inspect.envshSecret filessh/scp/rsyncSSH Username with private keysecret source (cred bundle)
→ deploy provisioner writes 7 files to tmpfs /run/payroll-sec (file 0444, dir 0700 root)
→ docker-compose `secrets:` mounts each at /run/secrets/<name> (tmpfs, ro)
→ secrets-entrypoint.sh in each image: for f in /run/secrets/*; export $(UPPER basename)=$(cat f); exec "$@"
⇒ secret NOT in `docker inspect` (set at runtime), NOT on disk (RAM only)postgresPOSTGRES_PASSWORD_FILEfs| Field | Required? | Ý nghĩa | |
|---|---|---|---|
| In | Jenkins controller reachable | có | agent phải kết nối được |
| In | target Linux: Java 21, user thuộc docker group + sudo NOPASSWD | có | xem Gotchas 1, 4 |
| In | | có | không cần SSH credential |
| In | repo app có | có | qua SCM checkout hoặc workspace có sẵn |
| In | tick chọn service (WEB/ETL/DB/NGINX/ALL) | không | selective deploy; mặc định full |
| Out | stack chạy trên target | có | build console |
| Out | tín hiệu verify | có | |
environment:docker inspect.envfile://LEAK <c>NOMOUNT <c>UnsupportedClassVersionError| Step | Type | Inputs | Action | Outputs/exit | Failure/next |
|---|---|---|---|---|---|
| W01 | judgment | hạ tầng target | Chọn agent (ưu tiên) hay SSH variant | quyết định | không chạy được agent → dừng, SSH variant |
| W02 | effect | Jenkins + target | Setup inbound agent (once): node, Java 21, agent.jar, user docker+sudo, | agent online | gotcha 1/4 → sửa, lặp |
| W03 | effect | repo app | Bake | image + provisioner | — |
| W04 | effect | job | Pipeline template (Sync → Deploy → Verify), code từ SCM | job chạy | checkout lỗi → B02 |
| W05 | deterministic | build | Verify success (real signals) | | LEAK/NOMOUNT → failed |
<target>/home/<user>/jenkins-agent:8080:50000sudo apt-get install -y openjdk-21-jre-headless # Java 21 — NOT 17 (see gotcha 1)
J21=$(ls /usr/lib/jvm/java-21-openjdk-*/bin/java | head -1)
mkdir -p ~/jenkins-agent && cd ~/jenkins-agent
curl -sO http://<CONTROLLER>:8080/jnlpJars/agent.jar
nohup "$J21" -jar agent.jar -url http://<CONTROLLER>:8080/ \
-secret <AGENT_SECRET> -name <target> -workDir ~/jenkins-agent > agent.log 2>&1 &nohupRestart=alwaysSecret filepipeline {
agent { label '<target>' } // runs ON the target, no ssh
environment { APP = '/home/<user>/<app-dir>' }
stages {
stage('Sync') { steps { sh '''
set -e
cp -f docker-compose.yml deploy.py.example "$APP/"
cp -f web/Dockerfile web/secrets-entrypoint.sh "$APP/web/"
cp -f etl/Dockerfile etl/secrets-entrypoint.sh "$APP/etl/"
cd "$APP" && cp -f deploy.py.example deploy.py
''' } }
stage('Deploy') { steps { withCredentials([file(credentialsId:'app-cred', variable:'CRED')]) { sh '''
set -e
mkdir -p /dev/shm/pl && chmod 700 /dev/shm/pl
cp "$CRED" /dev/shm/pl/cred.txt && chmod 600 /dev/shm/pl/cred.txt # gotcha 3
cd "$APP" && ln -sf /dev/shm/pl/cred.txt cred.txt
sudo python3 deploy.py --no-nginx --etl-url /etl
rm -f cred.txt; shred -uf /dev/shm/pl/cred.txt 2>/dev/null || rm -f /dev/shm/pl/cred.txt
''' } } }
stage('Verify') { steps { sh '''
set -e
for c in <containers>; do
docker inspect "$c" --format "{{range .Config.Env}}{{println .}}{{end}}" \
| grep -iqE "SESSION_SECRET=|AUTH_USERS=|PASSWORD=|DATABASE_URL=" && { echo "LEAK $c"; exit 1; } || echo "CLEAN $c"
docker exec "$c" ls /run/secrets >/dev/null 2>&1 && echo "MOUNT $c" || { echo "NOMOUNT $c"; exit 1; }
done
''' } }
}
}#!/bin/sh
set -e
if [ -d /run/secrets ]; then
for f in /run/secrets/*; do [ -f "$f" ] || continue
name=$(basename "$f" | tr '[:lower:]' '[:upper:]'); export "$name=$(cat "$f")"
done
fi
exec "$@"deploy.pydocker compose up -d| # | Symptom | Fix |
|---|---|---|
| 1 | Agent dies | Jenkins 2.5xx agent.jar needs Java 21, not 17. |
| 2 | App container crash-loops | Compose non-swarm bind-mounts the source file's perms; write secret files 0444 (dir 0700) so the non-root container user can read. |
| 3 | | Jenkins |
| 4 | | The agent process user must be in the docker group and have sudo NOPASSWD. |
| 5 | Reboot → containers fail to mount secret | |
Running on <target>CLEAN <c>docker inspectMOUNT <c>/run/secrets/dev/shm| ID | Kind | Guard | Hành vi | Skip / failure | Rejoin |
|---|---|---|---|---|---|
| B01 | user_optional | chỉ deploy vài service (tick WEB/ETL/DB/NGINX thay ALL) | Selective deploy: | ALL → full; không tick gì → | W05 |
| B02 | conditional_required | code lấy từ git (production: Pipeline script from SCM) | cấu hình SCM; repo private → SSH deploy key + known_hosts trên controller VÀ agent | lab không git server → agent | W04 |
| B03 | recovery | target reboot ( | chạy lại deploy (hoặc boot hook) để tái tạo secret trước | — | W05 |
booleanParam--no-depsif [ "$ALL" = true ]; then SVC=""; else
S=""; [ "$WEB" = true ] && S="$S web"; [ "$ETL" = true ] && S="$S etl"; [ "$DB" = true ] && S="$S postgres"
[ -z "$(echo $S|xargs)" ] && SVC=none || SVC=$(echo $S|xargs)
fi
deploy.py --services "$SVC" # "" = full ; "web etl" = those ; "none" = secrets only, no up
# provisioner runs: docker compose up -d --build --no-deps $SVC
[ "$NGINX" = true ] && docker restart <proxy-container> # proxy is a separate container, not composeweb → etl → postgres--no-depscheckout scmfile://git clone file:///…/repo.gitSSH Username with private keygitgit@github.com:owner/repo.gitgithub.comssh-keyscan github.com >> ~/.ssh/known_hostsssh-keygen -lf key.pubLEAKNOMOUNTRunning on <target>/dev/shm<target>Running on <target>CLEANMOUNT/dev/shmPermission denied /run/secrets/*--no-depsweb → etl → postgres