https://www.jenkins.io/doc/book/installing/linux/
java --version
apt install openjdk-11-jre-headless
curl -fsSL https://pkg.jenkins.io/debian-stable/jenkins.io-2023.key | tee /usr/share/keyrings/jenkins-keyring.asc > /dev/null
echo deb [signed-by=/usr/share/keyrings/jenkins-keyring.asc] https://pkg.jenkins.io/debian-stable binary/ | tee /etc/apt/sources.list.d/jenkins.list > /dev/null
apt-get update
apt install openjdk-17-jre
apt-get install jenkins
dnf install openjdk-11-jre-headless
wget -O /etc/yum.repos.d/jenkins.repo https://pkg.jenkins.io/redhat-stable/jenkins.repo
rpm --import https://pkg.jenkins.io/redhat-stable/jenkins.io-2023.key
dnf upgrade
dnf install java-17-openjdk
dnf install Jenkins
systemctl daemon-reload
systemctl enable jenkins
systemctl restart Jenkins
Редактируем лимиты и добавляем секцию управления nano /etс/security/limits.conf nano /lib/systemd/system/jenkins.service
Environment="JENKINS_PORT=9000"
Environment="JAVA_OPTS=-Djava.awt.headless=true -Duser.timezone=Europe/Berlin -Dorg.apache.commons.jelly.tags.fmt.timeZone=Europe/Berlin -Dhudson.security.csrf.GlobalCrumbIssuerConfiguration.DISABLE_CSRF_PROTECTION=true -XX:+AlwaysPreTouch -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/lib/jenkins/log -XX:+UseG1GC -XX:+UseStringDeduplication -XX:+ParallelRefProcEnabled -XX:+DisableExplicitGC -XX:+UnlockDiagnosticVMOptions -XX:+UnlockExperimentalVMOptions -Xlog:gc*=info,gc+heap=debug,gc+ref*=debug,gc+ergo*=trace,gc+age*=trace:file=/var/lib/jenkins/gc.log:utctime,pid,level,tags:filecount=2,filesize=100M -Xms512m -Xmx8g"
systemctl daemon-reload
systemctl restart jenkins
server {
listen 80;
server_name _;
keepalive_timeout 0;
access_log off;
allow 127.0.0.1;
allow ::1;
deny all;
location ~ ^/(status|ping)$ {
fastcgi_param SCRIPT_FILENAME fastcgi_script_name;
fastcgi_index index.php;
include fastcgi_params;
#fastcgi_pass 127.0.0.1:9000;
fastcgi_pass unix:/var/run/php-fpm/www.sock;
}
location /basic_status {
stub_status on;
}
}
server {
server_name jenkins.symfio.net;
location / {
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass http://127.0.0.1:9000/;
proxy_read_timeout 90;
}
listen 443 ssl; # managed by Certbot
ssl_certificate /etc/letsencrypt/live/jenkins.symfio.net/fullchain.pem; # managed by Certbot
ssl_certificate_key /etc/letsencrypt/live/jenkins.symfio.net/privkey.pem; # managed by Certbot
include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
}
server {
if ($host = jenkins.symfio.net) {
return 301 https://request_uri;
} # managed by Certbot
listen 80;
server_name jenkins.symfio.net 88.198.68.180;
return 404; # managed by Certbot
}
user jenkins;
worker_processes 8;
worker_rlimit_nofile 40000;
error_log /var/log/nginx/error.log;
pid /var/run/nginx.pid;
events {
worker_connections 8192;
multi_accept on;
use epoll;
}
http {
include /etc/nginx/mime.types;
include /etc/nginx/conf.d/maps/webp.conf;
# proxy_cache_path /var/cache/nginx/cache levels=1:2 keys_zone=my-cache:8m max_size=1000m inactive=600m;
# proxy_temp_path /var/cache/nginx/tmp;
# fastcgi_cache_path /tmp/fcgi-cache/ levels=1:2 keys_zone=one:10m;
fastcgi_buffers 32 32k;
fastcgi_buffer_size 64k;
underscores_in_headers on;
default_type application/octet-stream;
log_format main '$remote_addr - remote_user \[time_local] "$request" '
'$status http_referer" '
'"http_x_forwarded_for"';
# access_log off;
# log_not_found off;
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/nginx-error.log warn;
sendfile_max_chunk 128k;
postpone_output 1460;
types_hash_max_size 2048;
server_tokens off;
gzip on;
gzip_disable "msie6";
gzip_comp_level 6;
gzip_min_length 1100;
gzip_buffers 64 8k;
gzip_http_version 1.1;
gzip_proxied any;
gzip_types text/plain text/css application/json application/x-javascript application/javascript
text/xml application/xml application/xml+rss text/javascript;
gzip_static on;
# Load config files from the /etc/nginx/conf.d directory
# The default server is in conf.d/default.conf
# Cache information about frequently accessed files
open_file_cache max=20000 inactive=20s;
open_file_cache_valid 30s;
open_file_cache_min_uses 2;
open_file_cache_errors on;
chunked_transfer_encoding on; # need for docker registry to avoid limits
# Adjust client timeouts
client_max_body_size 0; # need for docker registry to avoid limits
client_body_buffer_size 128k;
client_body_timeout 15;
client_header_timeout 15;
keepalive_timeout 20;
keepalive_requests 100000;
send_timeout 15;
sendfile on;
# tcp_nopush on;
# tcp_nodelay on;
#Adjust output buffers
reset_timedout_connection on;
server_names_hash_bucket_size 100;
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/conf.d/maps/*.conf;
include /etc/nginx/conf.d/ssl/*.conf;
}
yum install -y yum-utils
yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
dnf install php-opcache -y
dnf install helm
dnf module reset ruby -y
yum install @ruby:3.0
ruby --version
curl -fsSL https://github.com/rbenv/rbenv-installer/raw/HEAD/bin/rbenv-installer | bash
echo 'export PATH="PATH"' >> ~/.bashrc
echo 'eval "$(rbenv init -)"' >> ~/.bashrc
source ~/.bashrc
rbenv -v
rbenv install -l
rbenv install 3.2.2
rbenv global 3.2.2
ruby --version
Генерируем сертификаты для jenkins.symfio.net
certbot run --nginx --register-unsafely-without-email
Копируем /var/lib/jenkins по ftp (лучше tar'ом сжать и потом скопировать и развернуть, иначе очень долго может быть - обилие мелких файлов - на целевом сервере было больше 1 миллиона и около 7 часов копирования).
Разархивировать
Проверка целостности архива
tar -tvzf jenkins.tar.gz >/dev/null && echo "Backup is good"
Копируем /var/www/html тоже tar'ом
Копируем /mnt/symfio-dev, /root/.npm /root/.gem /root/.docker также tar'ом
Монтируем /mnt/symfio-dev/uploads
168.119.251.210:/opt/nfs/symfio-dev-symfio-dev-uploads-pvc-79a12324-073d-48ed-aace-86852409e1fa /mnt/symfio-dev/uploads nfs defaults,timeo=900,retrans=5,_netdev 0 0
Делаем линки:
ln -s /var/lib/jenkins/workspace/ /wsp
ln -s /var/www/html/sy3/ /sy3
ln -s /var/www/html/ /www
ln -s /wsp/1_Deploy_Portal_DEV/ /p
ln -s /var/www/html/isa/ /isa
ln -s /var/lib/jenkins/ /home/jenkins
Не меняет порт, только выше, хотя тут тоже задается и opts для java
--httpPort=9000
Переносим все параметры запуска сюда (имеют больший приоритет, даже если в других конфигах другое)(Кирилл перенес)
[Service]
Environment="JAVA_OPTS=-Djava.awt.headless=true -Duser.timezone=Europe/Berlin -Dorg.apache.commons.jelly.tags.fmt.timeZone=Europe/Berlin -Dhudson.security.csrf.GlobalCrumbIssuerConfiguration.DISABLE_CSRF_PROTECTION=true -Dhudson.model.DirectoryBrowserSupport.CSP="default-src 'self' 'unsafe-inline' 'unsafe-eval'; img-src 'self' 'unsafe-inline' data:;" -XX:+AlwaysPreTouch -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/lib/jenkins/log -XX:+UseG1GC -XX:+UseStringDeduplication -XX:+ParallelRefProcEnabled -XX:+DisableExplicitGC -XX:+UnlockDiagnosticVMOptions -XX:+UnlockExperimentalVMOptions -Xlog:gc*=info,gc+heap=debug,gc+ref*=debug,gc+ergo*=trace,gc+age*=trace:file=/var/lib/jenkins/gc.log:utctime,pid,level,tags:filecount=2,filesize=100M -Xms512m -Xmx8g"
Environment="JENKINS_PORT=9000"
Environment="JENKINS_DEBUG_LEVEL=3"
Environment="JENKINS_OPTS=--sessionTimeout=10080 --sessionEviction=43200"
Environment="JENKINS_LOG=/var/log/jenkins/jenkins.log"
cat /proc/http://Jenkins.s043218.edu.slurm.io
cat /proc/67043/cmdline
nano /var/lib/jenkins/secrets/initialAdminPassword
Тут хранятся конфиги, плагины, задания
ls -la /var/lib/Jenkins
Установить плагин
Командная строка:
Плагин-менеджер
https://github.com/jenkinsci/plugin-installation-manager-tool
Настройка CSRF
Ошибка: No valid crumb was included in request
Реверс-прокси под nginx
https://www.jenkins.io/doc/book/system-administration/reverse-proxy-configuration-nginx/
Для авторизации подключается к БД sy-redmine по порту 10085 Если отвалилась авторизация, то проверяем, БД, коннект, сетевые проблемы к хосту/порту, настройки.
nano /var/lib/jenkins/config.xml
<mode>NORMAL</mode>
<useSecurity>true</useSecurity>
<authorizationStrategy class="hudson.security.FullControlOnceLoggedInAuthorizationStrategy">
<denyAnonymousReadAccess>true</denyAnonymousReadAccess>
</authorizationStrategy>
<securityRealm class="hudson.plugins.redmine.RedmineSecurityRealm" plugin="redmine@0.21">
<dbms>MySQL</dbms>
<dbServer>task.symfio.net</dbServer>
<databaseName>redmine_production</databaseName>
<port>10085</port>
<dbUserName>redmine</dbUserName>
Если все верно и работает, то пробуем переписать пароль и настройки к самой БД redmine. Чтобы зайти, ставим параметр useSecurity в false
<useSecurity>false</useSecurity>
Заходим в веб-интерфейс в Dashboard -> Manage Jenkins -> Security Проставляем: Security Realm

Для исправления косяка и удаления лишней даты в интерфейсе, которая мешает читать commit, делаем:
nano /var/lib/jenkins/userContent/theme.css
.app-builds-container__item__time {
display: none !important;
}
Pipeline-скрипты Groovy выполняются в sandbox. Любой вызов внешнего API (Jenkins-классов, java.io.File, Groovy-extension методы) сначала летит в pending signatures, билд останавливается на этой строке, нужно одобрить и запустить заново. На каждую новую сигнатуру — отдельный запуск.
⚠️ Jenkins на pending пишет «Approving this signature may introduce a security vulnerability! You are advised to deny it.» — это дженериковая ремарка, ставится почти на все API-вызовы. Решение всегда «доверяю ли я тому, кто пишет этот pipeline».
⚠️ @NonCPS НЕ обходит sandbox. Аннотация отключает только CPS-transformation closures (нужна для итерации по Jenkins-объектам без mismatch-ошибок), но Script Security всё равно проверяет каждый вызов.
Manage Jenkins → In-process Script Approval — там список pending с кнопкой Approve на каждой строке. Этот путь работает всегда, потому что UI закрывает ровно тот pending entry с привязкой к context, который завёл pipeline.
https://jenkins.symfio.net/script — можно запушить пакет:
import org.jenkinsci.plugins.scriptsecurity.scripts.ScriptApproval
def sa = ScriptApproval.get()
def sigs = [
'method jenkins.model.Jenkins getItemByFullName java.lang.String',
'method hudson.model.Job getBuilds',
'method hudson.model.PersistenceRoot getRootDir',
'new java.io.File java.io.File java.lang.String',
'method java.io.File exists',
'staticMethod org.codehaus.groovy.runtime.DefaultGroovyMethods drop java.lang.Iterable int',
'staticMethod org.codehaus.groovy.runtime.DefaultGroovyMethods deleteDir java.io.File'
]
sigs.each { sa.approveSignature(it) }
sa.save()
println "Approved: ${sigs.size()}"
⚠️ Эмпирически approveSignature(String) иногда тихо не закрывает уже существующий pending entry (он привязан к контексту). Если после Console-Run pipeline всё ещё ругается — добей через UI (см. выше).
Script Security показывает класс ровно там, где метод объявлен, а не где вызывается. Это легко промахнуться:
|. В коде |. Кажется |_. На самом деле | | b.getNumber() (для Run) | method hudson.model.Run getNumber | method jenkins.model.HistoricalBuild getNumber | | b.getRootDir() (для Run) | method hudson.model.Run getRootDir | method hudson.model.PersistenceRoot getRootDir | | f.deleteDir() | method java.io.File deleteDir | staticMethod org.codehaus.groovy.runtime.DefaultGroovyMethods deleteDir java.io.File — это Groovy-extension | | f.absolutePath | method java.io.File getAbsolutePath | да, оно — но не нужно для самой ротации, лучше убрать из кода |
Точное имя класса всегда в строке ошибки в логе билда: Scripts not permitted to use method X.Y.Z foo… — копировать оттуда.
ScriptApproval.get().getApprovedSignatures().each { println it }
Если связываться со Script Security не хочется (особенно для deleteDir/delete/Files.write) — то же самое делается системным cron на самом master (sy-jenkins), вне pipeline. Там Script Security не действует.
Pipeline-jobs с большими артефактами (Playwright reports, скриншоты, исходники видео) быстро занимают десятки GB. Пример с Screenshot.update.compare: 17 билдов × 5.8 GB = 68 GB на один job, причём половина — это дубликаты одного отчёта в archive/playwright-report/ и htmlreports/Playwright_20Report/.
/var/lib/jenkins/jobs/<...>/builds/<N>/
├── archive/ ← archiveArtifacts (Jenkins core)
├── htmlreports/ ← publishHTML plugin (keepAll: true)
├── build.xml, log, junitResult.xml, workflow/ ← метаданные (мелочь, ~2 MB)
archive/ ротируется родным механизмом logRotator(artifactNumToKeepStr: N). htmlreports/ родного N-ротатора не имеет — он либо хранит копию у каждого билда (keepAll: true), либо только у последнего (keepAll: false).
В properties([...]) в начале Jenkinsfile:
buildDiscarder(logRotator(
numToKeepStr: '50', // метаданные (build.xml, log) держим у 50 билдов
artifactNumToKeepStr: '3' // тяжёлый archive/ — только у 3 последних
))
junit 'test-results/**/*.xml'
// playwright-report/** НЕ архивируем: уже опубликован через publishHTML
// как htmlreports/Playwright_20Report (~2.9 GB). Это был дубликат.
archiveArtifacts artifacts: '__screenshots__/**', allowEmptyArchive: true
archiveArtifacts artifacts: "${triageDir}/**", allowEmptyArchive: true
htmlreports/ через Groovy в post-стадииpublishHTML с keepAll: true нужен, чтобы по URL /job/.../<ReportName> Jenkins мог показать отчёт за последние N билдов. Но из коробки ротации нет — пишем @NonCPS функцию в самом верху Jenkinsfile (после import, вне node{}):
@NonCPS
int pruneHtmlReports(int keepCount, List<String> reportDirNames, String jobName) {
int count = 0
def project = Jenkins.instance.getItemByFullName(jobName)
if (project == null) return count
// getBuilds() возвращает RunList уже отсортированный newest-first → .drop(N) хватает
project.getBuilds().drop(keepCount).each { b ->
reportDirNames.each { rn ->
def f = new File(b.rootDir, "htmlreports/${rn}")
if (f.exists()) {
f.deleteDir()
count = count + 1
}
}
}
return count
}
И вызов внутри post-блока:
try {
int n = pruneHtmlReports(3,
['Playwright_20Report', 'Codex_20Triage_20Report', 'Baseline_20Manifest'],
env.JOB_NAME
)
echo "Pruned htmlreports from ${n} stale build folder(s)"
} catch (Throwable t) {
echo "htmlreports rotation skipped: ${t.message}"
}
⚠️ Несколько важных моментов:
@NonCPS обязателен, иначе CPS-transformer ломает closures поверх RunList (expected to call hudson.util.RunList.sort but wound up catching ...CpsClosure2.call). См. https://www.jenkins.io/doc/book/pipeline/cps-method-mismatches/.sort { -it.number } — getBuilds() уже отсортирован новый→старый, .drop(N) сразу даёт хвост. Это убирает зависимость от HistoricalBuild.getNumber..absolutePath — логируем только число удалённых папок (int count), без путей. Иначе тянется лишняя сигнатура getAbsolutePath + leftShift.try/catch гарантирует, что забытое одобрение сигнатуры не уронит билд — просто ротация в этот раз пропустится.Сигнатуры, которые нужно одобрить (см. раздел Script Approval):
staticMethod jenkins.model.Jenkins getInstancemethod jenkins.model.Jenkins getItemByFullName java.lang.Stringmethod hudson.model.Job getBuildsmethod hudson.model.PersistenceRoot getRootDir (не Run!)new java.io.File java.io.File java.lang.Stringmethod java.io.File existsstaticMethod org.codehaus.groovy.runtime.DefaultGroovyMethods drop java.lang.Iterable intstaticMethod org.codehaus.groovy.runtime.DefaultGroovyMethods deleteDir java.io.File (не java.io.File deleteDir!)Реальный кейс — job Symfio/Frontend/Dealer-Websites/Tests/Screenshot.update.compare: было 68 GB на job (17 билдов × 5.8 GB), стало ~9 GB.
Если связываться со Script Security не хочется — то же самое cron'ом на sy-jenkins:
#!/bin/bash
# /etc/cron.daily/jenkins-prune-screenshot-htmlreports
set -e
JOB=/var/lib/jenkins/jobs/<full-path-to-job>/builds
KEEP=3
REPORTS="Playwright_20Report Codex_20Triage_20Report Baseline_20Manifest"
ls -1 "$JOB" | grep -E '^[0-9]+$' | sort -n | head -n -$KEEP | while read b; do
for r in $REPORTS; do
rm -rf "$JOB/$b/htmlreports/$r"
done
done
build.xml — UI-карточка билда в Stage Viewlog, log-index — лог консолиjunitResult.xml — для Tests trend графикаworkflow/ — внутренние данные pipelineТо есть в Jenkins UI билд остаётся видимым, кликабельным, показывает Result и историю — но без больших артефактов. Это покрывает запрос «билды не удалять, артефакты старше N — да».