Patch ThingsBoard, Build Your Own Debian Package, and Deploy It

On this page12 sections

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 HEAD

2. 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..HEAD

Resolve 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.patch

The 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-25

Define 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 -version

Confirm 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 Authors

Check 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 --branch

Run 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 \
  test

All 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 --branch

5. 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'
        }
    }
}
EOF

The 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 clean
tb_mvn -pl packaging,application -am \
  -DskipTests -Dpkg.skip=true install

Refresh the packaging helper to include its Gradle plugin descriptor:

tb_mvn -pl packaging -Dmaven.jar.forceCreation=true \
  resources:resources jar:jar install:install

Build the Debian package:

tb_mvn -pl application -DskipTests \
  -Dpkg.skip.rpm=true -Dpkg.skip.zip=true package

After 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-pager

The 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 Architecture

For 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)" = inactive

Leave 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 -lnu

Expect 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 thingsboard

Repeat the service checks. This fix leaves the schema unchanged, so rollback needs no database restore. A schema-changing fix requires its own recovery plan.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top