Automate deployment to Linux servers with secure SSH key injection, then level it up using Ansible and the ansiblePlaybook step from Jenkins.

In episode 14 your pipeline ran tests, measured coverage, and enforced a Quality Gate with SonarQube. That means code that reached this point can be trusted. Only one decisive final step remains: sending that code to the production server.
Many teams still deploy manually — scp files, SSH into the server, restart the service, then hope everything works. That approach is slow, inconsistent, and once the team grows, it is impossible to trace who deployed what and when. Manual deploys are like delivering packages on foot when there is an automated courier: the result is the same, but the scale never grows.
In this episode we will build Continuous Deployment to a Linux server through two approaches: secure SSH key injection with the SSH Agent plugin, then full automation with Ansible using the ansiblePlaybook step.
Broadly speaking, there are three common approaches:
| Approach | Plugin/Step | Characteristics |
|---|---|---|
| SSH Agent | sshagent | Injects the SSH key into the environment, flexible for scp/ssh/rsync |
| Publish Over SSH | sshPublisher | Sends files and executes commands through a configured SSH server |
| Ansible | ansiblePlaybook | Declarative, idempotent deployment, can target many hosts at once |
All three are valid, but Ansible wins as servers grow in number because a playbook describes the desired end state of a system, not the steps to get there.
The SSH Agent plugin takes a private key from Jenkins Credentials and injects it into the SSH agent on the job's agent, only for the duration of the sshagent block. The key is never written to the workspace disk:
stage('Deploy via SSH') {
steps {
sshagent(['deploy-key']) {
sh 'rsync -avz --delete dist/ deploy@prod-server:/var/www/my-app/'
sh 'ssh deploy@prod-server "systemctl restart my-app"'
}
}
}sshagent accepts a list of credential IDs stored in Jenkins. Inside the block, all SSH commands automatically use that key. The output remains visible in the console, but the key itself is never exposed.
Warning
Store the private key as an SSH Username with private key credential type in Jenkins; never place it as a file in the repository or workspace. A leaked key means anyone can get into the production server.
The Publish Over SSH plugin manages server connections in Jenkins's global configuration (host, username, key), then the sshPublisher step sends files and runs commands. It suits simple scenarios that only move artifacts:
step([
$class: 'Publisher',
publishers: [
[$class: 'SSHPublisherPlugin',
configName: 'prod-server',
transfers: [[sourceFiles: 'dist/**',
removePrefix: 'dist',
remoteDirectory: '/var/www/my-app']]]
]
])Its syntax is class-based, so it is less pleasant to read than declarative steps. For that reason, most modern teams choose Ansible for more complex deployments.
Ansible deploys by describing the desired end state via a playbook, and it is idempotent: running a playbook twice produces no double effect. To use it from Jenkins, install the Ansible plugin, then register an Ansible installation in Manage Jenkins > Tools — similar to how you register Maven.
Prepare an inventory that defines the production hosts. An example INI-style inventory with SSH-based hosts:
[web]
prod-server ansible_host=203.0.113.10 ansible_user=deploy
[web:vars]
ansible_ssh_private_key_file=/tmp/deploy-keyA deployment playbook for a Java application can be as simple as:
---
- name: Deploy aplikasi ke server produksi
hosts: web
become: true
tasks:
- name: Pastikan direktori target ada
ansible.builtin.file:
path: /opt/my-app
state: directory
- name: Salin artefak dari workspace
ansible.builtin.copy:
src: /tmp/my-app.jar
dest: /opt/my-app/my-app.jar
owner: myapp
group: myapp
mode: '0755'
- name: Restart service aplikasi
ansible.builtin.systemd_service:
name: my-app
state: restarted
- name: Verifikasi health endpoint
ansible.builtin.uri:
url: http://localhost:8080/health
status_code: 200Then call that playbook from the pipeline with the ansiblePlaybook step:
stage('Deploy dengan Ansible') {
steps {
ansiblePlaybook(
playbook: 'ansible/deploy.yml',
inventory: 'ansible/inventory/production.ini',
extras: '--tags deploy'
)
}
}The ansiblePlaybook step automatically picks the Ansible binary from the installation registered in Tools, runs the playbook with the specified inventory, and forwards extra arguments such as --tags to limit which tasks execute.
Tip
Make sure the build artifact is available in the agent workspace before
the playbook runs — for example from an archiveArtifacts stage that is
unstashed back. Ansible sends files from the machine running
ansiblePlaybook, which is the Jenkins agent.
Important
Automated deployment does not mean without control. For major changes,
combine CD with a manual approval gate (input step) in front
of the production deploy stage — as you learned in episode 6.
In episode 15 you automated deployment to a Linux server with secure SSH key injection via sshagent, learned the Publish Over SSH alternative for simple artifact transfers, and built declarative deployment with Ansible using the ansiblePlaybook step. You also got security best practices such as a dedicated deploy user, key-only login, and health verification after deploy.
Deploying to a Linux server is a solid foundation, but in the cloud-native era, applications are more often deployed to Kubernetes with the help of Helm.
In episode 16 we will discuss Continuous Deployment to Kubernetes & Helm — applying manifests with kubectl apply and automating releases with helm upgrade --install. See you there!