This guide applies the LwM2M certificate-chain provisioning fix from PR #16179 to ThingsBoard 4.4, builds a Debian package, and installs it on an Ubuntu server. Use the same process for your own committed changes.
The server already runs 4.4. For an upgrade from 4.3, see Upgrade ThingsBoard CE 4.3.1.6 to 4.4 on Ubuntu.
Environment
- Build machine: Ubuntu with Git and Docker Engine.
- Source: ThingsBoard
v4.4. - Build runtime: JDK 25 and Maven 3.9 in
maven:3.9-eclipse-temurin-25. - Server: Ubuntu 26.04, standalone ThingsBoard DEB, local PostgreSQL 18, and Kafka.
Use a separate build machine with Docker installed. The build provisions Node.js and Yarn; Docker also runs the test databases.
1. Check out the release and create a custom branch
Check the installed version with dpkg-query -W thingsboard, then use its matching source tag. Continue an existing custom branch if it already contains fixes you need.
Run the build steps in one Bash session:
set -euo pipefail
mkdir -p ~/src
cd ~/src
git clone https://github.com/thingsboard/thingsboard.git thingsboard-4.4-custom
cd thingsboard-4.4-custom
git fetch --tags origin
git switch -c v4.4-custom v4.4
git status --short --branch
git rev-parse HEAD2. Fix the issue on the selected release
Write and commit your fix with a regression test, or cherry-pick an existing commit. For PR #16179:
git fetch origin 9a8ebae83ccebdd6eda97c22f227c3fa5e746063
git show --stat --oneline FETCH_HEAD
git cherry-pick -x 9a8ebae83ccebdd6eda97c22f227c3fa5e746063
git diff --stat v4.4..HEAD
git diff --check v4.4..HEADResolve any conflicts and review the diff. The -x option records the original commit. This fix changes both core and LwM2M transport; the standalone package includes both.
To identify the build as 4.4.0+m-a, save the following patch as custom-version.patch:
Custom application version source patch
diff --git a/application/pom.xml b/application/pom.xml
index a665ef439f..e485699a67 100644
--- a/application/pom.xml
+++ b/application/pom.xml
@@ -584,6 +584,11 @@
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
+ <configuration>
+ <additionalProperties>
+ <version>${tb.custom.version}</version>
+ </additionalProperties>
+ </configuration>
</plugin>
<plugin>
<groupId>org.thingsboard</groupId>
diff --git a/application/src/test/java/org/thingsboard/server/service/install/ProjectInfoTest.java b/application/src/test/java/org/thingsboard/server/service/install/ProjectInfoTest.java
new file mode 100644
index 0000000000..d2cb7a8b4b
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/service/install/ProjectInfoTest.java
@@ -0,0 +1,46 @@
+// SPDX-FileCopyrightText: Copyright The ThingsBoard Authors
+// SPDX-FileCopyrightText: Modifications Copyright ThingsBoard, Inc.
+// SPDX-License-Identifier: Apache-2.0 AND BUSL-1.1
+package org.thingsboard.server.service.install;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+import org.springframework.boot.info.BuildProperties;
+import org.springframework.jdbc.core.JdbcTemplate;
+
+import java.util.Optional;
+import java.util.Properties;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+class ProjectInfoTest {
+
+ @ParameterizedTest
+ @ValueSource(strings = {"4.4.0", "4.4.0+m-a", "4.4.0+m-b"})
+ void buildVersionPreservesCompatibilityAndSchemaVersion(String version) {
+ Properties properties = new Properties();
+ properties.setProperty("version", version);
+ BuildProperties buildProperties = new BuildProperties(properties);
+ ProjectInfo projectInfo = new ProjectInfo(Optional.of(buildProperties));
+ JdbcTemplate jdbcTemplate = mock(JdbcTemplate.class);
+ DefaultDatabaseSchemaSettingsService schemaService = new DefaultDatabaseSchemaSettingsService(projectInfo, jdbcTemplate);
+
+ assertThat(buildProperties.getVersion()).isEqualTo(version);
+ assertThat(projectInfo.getProjectVersion()).isEqualTo("4.4.0");
+ assertThat(projectInfo.getProductType()).isEqualTo("PE");
+ assertThat(schemaService.getPackageSchemaVersion()).isEqualTo("4.4.0.0");
+ schemaService.updateSchemaVersion();
+ verify(jdbcTemplate).execute("UPDATE tb_schema_settings SET schema_version = 4004000000, product = 'PE'");
+ }
+
+ @Test
+ void missingBuildPropertiesKeepsTheExistingUnknownVersion() {
+ ProjectInfo projectInfo = new ProjectInfo(Optional.empty());
+
+ assertThat(projectInfo.getProjectVersion()).isEqualTo("unknown");
+ }
+
+}
diff --git a/pom.xml b/pom.xml
index bd1102fe8b..732a661971 100755
--- a/pom.xml
+++ b/pom.xml
@@ -29,6 +29,8 @@
<maven.compiler.source>25</maven.compiler.source>
<maven.compiler.target>25</maven.compiler.target>
<main.dir>${basedir}</main.dir>
+ <!-- Display version for this custom build; Maven artifact coordinates remain at 4.4.0. -->
+ <tb.custom.version>4.4.0+m-a</tb.custom.version>
<!-- Set to true to skip only the UI builds: the Angular application in ui-ngx and the
`yarn run pkg` launchers of msa/web-ui and msa/web-report. It deliberately does NOT
cover msa/js-executor, whose yarn install and sandbox tests keep running.
diff --git a/ui-ngx/package.json b/ui-ngx/package.json
index 29375bfb2b..40d91be630 100644
--- a/ui-ngx/package.json
+++ b/ui-ngx/package.json
@@ -1,6 +1,6 @@
{
"name": "thingsboard",
- "version": "4.4.0",
+ "version": "4.4.0+m-a",
"scripts": {
"ng": "ng",
"start": "node --max_old_space_size=8192 ./node_modules/@angular/cli/bin/ng serve --configuration development --host 0.0.0.0 --open",
Apply it after the provisioning fix:
git apply --check custom-version.patch
git apply --index custom-version.patch
git diff --cached --stat
git diff --cached --check
mv custom-version.patch ../custom-version.patchThe changes are staged for the commit in step 4.
3. Prepare the build runtime
Create the build directories:
export TB_SOURCE_ROOT="$PWD"
export TB_BUILD_ROOT="$HOME/tb-build/v4.4-custom"
mkdir -p "$TB_BUILD_ROOT/maven-cache" \
"$TB_BUILD_ROOT/gradle-home/init.d" "$TB_BUILD_ROOT/artifacts"
docker version
docker pull maven:3.9-eclipse-temurin-25Define the Maven command:
tb_mvn() {
docker run --rm --network host \
--user "$(id -u):$(id -g)" \
--group-add "$(stat -c '%g' /var/run/docker.sock)" \
-v /var/run/docker.sock:/var/run/docker.sock \
-v "$TB_SOURCE_ROOT:$TB_SOURCE_ROOT" \
-v "$TB_BUILD_ROOT:$TB_BUILD_ROOT" \
-w "$TB_SOURCE_ROOT" \
-e GRADLE_USER_HOME="$TB_BUILD_ROOT/gradle-home" \
-e NODE_OPTIONS="--max_old_space_size=4096" \
-e MAVEN_OPTS="-Xmx1024m -Duser.home=/tmp -Dgradle.sys.org.gradle.workers.max=1 -Dgradle.sys.org.gradle.daemon=false -Dgradle.sys.org.gradle.jvmargs=-Xmx2g -Dgradle.sys.maven.repo.local=$TB_BUILD_ROOT/maven-cache" \
--entrypoint /usr/share/maven/bin/mvn \
maven:3.9-eclipse-temurin-25 \
-B -ntp -Dmaven.repo.local="$TB_BUILD_ROOT/maven-cache" "$@"
}
tb_mvn -versionConfirm Maven 3.9 and Java 25. Run builds sequentially.
4. Check the headers and run focused tests
For this PR, correct the copyright line in application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/X509ProvisionLwM2MIntegrationTest.java:
// SPDX-FileCopyrightText: Copyright The ThingsBoard AuthorsCheck the headers and stage the correction:
tb_mvn -pl application license:format license:check
git diff --check
git diff --cached
git add application/src/test/java/org/thingsboard/server/transport/lwm2m/security/sql/X509ProvisionLwM2MIntegrationTest.java
git status --short --branchRun the tests for this fix. Choose the relevant tests when applying a different change:
tb_mvn -pl application -am \
-DskipTests -Dpkg.skip=true -Dskip.ui.build=true test-compile
tb_mvn -pl application -am \
-Dpkg.skip=true -Dskip.ui.build=true \
-DforkCount=1 -Dsurefire.failIfNoSpecifiedTests=false \
-Dtest=X509ProvisionLwM2MIntegrationTest,X509_NoTrustLwM2MIntegrationTest,X509_TrustLwM2MIntegrationTest,DeviceProvisionServiceTest,DefaultTransportApiServiceTest,ProjectInfoTest \
testAll 35 selected tests passed.
Review and commit the changes:
git diff --cached --check
git diff --cached
git commit -m "Set custom ThingsBoard version and correct test license header"
git status --short --branch5. Set the custom version and Debian revision
The patch sets the application version to 4.4.0+m-a. Keep Maven coordinates at 4.4.0 and give the Debian package its own revision:
test -z "$(git status --porcelain)"
export TB_SOURCE_COMMIT="$(git rev-parse HEAD)"
export TB_DEB_REVISION=3
export TB_CUSTOM_RELEASE=makson.a
export TB_DEB_RELEASE="$TB_DEB_REVISION+$TB_CUSTOM_RELEASE+g$(git rev-parse --short=10 HEAD)"
cat > "$TB_BUILD_ROOT/gradle-home/init.d/custom-debian-revision.gradle" <<EOF
gradle.projectsEvaluated {
gradle.rootProject.allprojects { project ->
project.tasks.matching { task -> task.name == 'buildDeb' }.configureEach { task ->
task.release = '$TB_DEB_RELEASE'
}
}
}
EOFThe package version contains the revision and Git hash. Increase TB_DEB_REVISION for each later build.
6. Build the complete production package
Clean old UI assets, then build the application and production UI:
tb_mvn -pl ui-ngx cleantb_mvn -pl packaging,application -am \
-DskipTests -Dpkg.skip=true installRefresh the packaging helper to include its Gradle plugin descriptor:
tb_mvn -pl packaging -Dmaven.jar.forceCreation=true \
resources:resources jar:jar install:installBuild the Debian package:
tb_mvn -pl application -DskipTests \
-Dpkg.skip.rpm=true -Dpkg.skip.zip=true packageAfter BUILD SUCCESS, the package is application/target/thingsboard.deb.
7. Inspect the artifact and transfer it
Check the package version and save it with a checksum:
dpkg-deb -f application/target/thingsboard.deb Package Version Architecture
TB_PACKAGE_VERSION="$(dpkg-deb -f application/target/thingsboard.deb Version)"
dpkg --compare-versions "$TB_PACKAGE_VERSION" gt '4.4.0-1'
cp application/target/thingsboard.deb \
"$TB_BUILD_ROOT/artifacts/thingsboard-4.4-custom.deb"
git rev-parse HEAD > "$TB_BUILD_ROOT/artifacts/source-commit.txt"
(
cd "$TB_BUILD_ROOT/artifacts"
sha256sum thingsboard-4.4-custom.deb > thingsboard-4.4-custom.deb.sha256
sha256sum -c thingsboard-4.4-custom.deb.sha256
)The checksum should report OK. Replace the example SSH target and transfer the files:
export TB_PRODUCTION_HOST=administrator@tb.example.com
ssh "$TB_PRODUCTION_HOST" 'mkdir -p ~/tb-update'
scp "$TB_BUILD_ROOT/artifacts/thingsboard-4.4-custom.deb" \
"$TB_BUILD_ROOT/artifacts/thingsboard-4.4-custom.deb.sha256" \
"$TB_BUILD_ROOT/artifacts/source-commit.txt" \
"$TB_PRODUCTION_HOST:~/tb-update/"8. Check production and prepare the rollback package
On the server, verify the upload and version:
cd ~/tb-update
sha256sum -c thingsboard-4.4-custom.deb.sha256
dpkg-query -W thingsboard
TB_INSTALLED_VERSION="$(dpkg-query -W -f='${Version}' thingsboard)"
TB_INCOMING_VERSION="$(dpkg-deb -f thingsboard-4.4-custom.deb Version)"
dpkg --compare-versions "$TB_INCOMING_VERSION" gt "$TB_INSTALLED_VERSION"
sudo systemctl status thingsboard --no-pagerThe running service must use Java 25. Check its Java process:
sudo bash -c '
for TB_PID in $(cat /sys/fs/cgroup/system.slice/thingsboard.service/cgroup.procs); do
TB_JAVA_BIN=$(readlink -f "/proc/$TB_PID/exe" 2>/dev/null) || continue
if [[ "$TB_JAVA_BIN" == */bin/java ]]; then
"$TB_JAVA_BIN" -version
fi
done'Confirm the current UI and telemetry work. Keep the currently installed DEB for rollback. For an official v4.4 installation:
curl -fL --retry 3 -o thingsboard-4.4.deb \
https://github.com/thingsboard/thingsboard/releases/download/v4.4/thingsboard-4.4.deb
dpkg-deb -f thingsboard-4.4.deb Package Version ArchitectureFor an existing custom installation, use its exact DEB as the rollback file and name it thingsboard-4.4.deb.
9. Stop ThingsBoard and back up the installation
Open a root shell. Adjust the upload path and keep this shell open through installation:
sudo -i
set -euo pipefail
umask 077
TB_INSTALL_DIR=/home/administrator/tb-update
TB_BACKUP_DIR="/var/backups/thingsboard-custom-$(date -u +%Y%m%dT%H%M%SZ)"
install -d -m 0700 "$TB_BACKUP_DIR" "$TB_BACKUP_DIR/rollback"
cp "$TB_INSTALL_DIR/thingsboard-4.4.deb" \
"$TB_BACKUP_DIR/rollback/thingsboard-4.4.deb"
systemctl stop thingsboard
test "$(systemctl show thingsboard --property=ActiveState --value)" = inactiveLeave PostgreSQL and Kafka running. Back up the local database named thingsboard:
sudo -u postgres pg_dump -Fc -d thingsboard \
> "$TB_BACKUP_DIR/thingsboard.pgdump"
sudo -u postgres pg_dumpall --globals-only --no-role-passwords \
> "$TB_BACKUP_DIR/postgres-globals-no-passwords.sql"
test -s "$TB_BACKUP_DIR/thingsboard.pgdump"
pg_restore --list "$TB_BACKUP_DIR/thingsboard.pgdump" > /dev/null
pg_restore --file=/dev/null "$TB_BACKUP_DIR/thingsboard.pgdump"Back up configuration, extensions, data files, and service settings:
TB_BACKUP_PATHS=()
for TB_PATH in \
usr/share/thingsboard/conf usr/share/thingsboard/extensions \
usr/share/thingsboard/data etc/thingsboard var/lib/thingsboard \
etc/systemd/system/thingsboard.service.d \
etc/systemd/system/thingsboard.service \
usr/lib/systemd/system/thingsboard.service etc/default/thingsboard; do
if [[ -e "/$TB_PATH" || -L "/$TB_PATH" ]]; then
TB_BACKUP_PATHS+=("$TB_PATH")
fi
done
tar --acls --xattrs --numeric-owner \
-czf "$TB_BACKUP_DIR/config-state-service.tar.gz" \
-C / "${TB_BACKUP_PATHS[@]}"
tar -tzf "$TB_BACKUP_DIR/config-state-service.tar.gz" \
> "$TB_BACKUP_DIR/config-archive-members.txt"
for TB_CONF in thingsboard.conf thingsboard.yml logback.xml actor-system.conf; do
if [[ -f "/usr/share/thingsboard/conf/$TB_CONF" ]]; then
sha256sum "/usr/share/thingsboard/conf/$TB_CONF"
fi
done > "$TB_BACKUP_DIR/conffiles.sha256"Include certificates or custom files stored elsewhere. Keep a protected copy off the server. If backup validation fails, restart the existing service and resolve the failure before continuing.
10. Install the custom package and verify the service
Install in the same root shell:
cd "$TB_INSTALL_DIR"
sha256sum -c thingsboard-4.4-custom.deb.sha256
dpkg --force-confold -i ./thingsboard-4.4-custom.deb
sha256sum -c "$TB_BACKUP_DIR/conffiles.sha256"
systemctl daemon-reload
systemctl enable thingsboard
systemctl start thingsboard
systemctl status thingsboard --no-pager
dpkg-query -W thingsboard--force-confold retains modified package configuration files. This fix needs no database migration; on an existing 4.4 server, do not run install.sh or upgrade.sh.
Sign in and confirm that Home shows 4.4.0+m-a.
The official update notice may still offer 4.4.0; review an official replacement against your custom fix and test plan before installing it.
Check startup and HTTPS:
systemctl show thingsboard \
--property=ActiveState --property=SubState \
--property=MainPID --property=NRestarts
journalctl -u thingsboard -n 100 --no-pager
tail -n 100 /var/log/thingsboard/thingsboard.log
TB_URL=https://tb.example.com
curl -fsS -o /dev/null -w '%{http_code}\n' "$TB_URL/"
curl -fsS "$TB_URL/api/noauth/setup/state"
curl -sS -o /dev/null -w '%{http_code}\n' "$TB_URL/api/auth/user"
ss -lnt
ss -lnuExpect HTTP 200, setup READY, and 401 from the unauthenticated protected API. Confirm that devices and dashboards are present, telemetry arrives, and the original listeners are active. Test the provisioning fix with a controlled device.
11. Return to the previous package if needed
If verification fails, reinstall the saved package. Use the same root shell, or restore TB_BACKUP_DIR to the backup path from step 9:
set -euo pipefail
test -d "$TB_BACKUP_DIR"
systemctl stop thingsboard
dpkg --force-confold -i "$TB_BACKUP_DIR/rollback/thingsboard-4.4.deb"
sha256sum -c "$TB_BACKUP_DIR/conffiles.sha256"If configuration hashes differ, restore the affected files before starting:
systemctl daemon-reload
systemctl enable thingsboard
systemctl start thingsboard
systemctl status thingsboard --no-pager
dpkg-query -W thingsboardRepeat the service checks. This fix leaves the schema unchanged, so rollback needs no database restore. A schema-changing fix requires its own recovery plan.
