diff --git a/.ci/Dockerfile b/.ci/Dockerfile
new file mode 100644
index 0000000000000000000000000000000000000000..1846ed32ddb99e38481179408f2d69d328322b09
--- /dev/null
+++ b/.ci/Dockerfile
@@ -0,0 +1,6 @@
+FROM osrf/ros:kinetic-desktop-xenial
+RUN apt-get update && apt-get install -y \
+    clang \
+    clang-format \
+    clang-tidy \
+    ros-kinetic-ros-control
diff --git a/.ci/debug.sh b/.ci/debug.sh
new file mode 100755
index 0000000000000000000000000000000000000000..ba152be05a5fe4d094796c0a639d863c24f81887
--- /dev/null
+++ b/.ci/debug.sh
@@ -0,0 +1,22 @@
+#!/bin/sh
+
+set -e
+
+. /opt/ros/kinetic/setup.sh
+
+rm -f src/CMakeLists.txt
+catkin_init_workspace src
+
+rm -rf build-debug
+mkdir build-debug
+cd build-debug
+
+cmake -DCMAKE_BUILD_TYPE=Debug -DCMAKE_EXPORT_COMPILE_COMMANDS=ON ../src
+. devel/setup.sh
+
+cmake --build .
+cmake --build . --target tests
+ctest -V
+
+cmake --build . --target check-format
+cmake --build . --target check-tidy
diff --git a/Jenkinsfile b/Jenkinsfile
index ffce21c24ef80ee0f704b50d6ea734f960808bb7..457a5079a7c52942851ff32904619922cb5e9263 100644
--- a/Jenkinsfile
+++ b/Jenkinsfile
@@ -3,50 +3,48 @@
 node {
   step([$class: 'StashNotifier'])
 
-  def gitCommit
   try {
-    stage('Checkout') {
-      dir('catkin_ws/src') {
-        checkout scm
-        gitCommit = sh(script: 'git rev-parse HEAD', returnStdout: true).trim()
-      }
-
-      dir('libfranka') {
-        // TODO(walc_fl): Remove hard-coded repository URL
-        checkout resolveScm(source: [$class: 'GitSCMSource', remote: 'https://github.com/frankaemika/libfranka.git',
-                                     credentialsId: '6a639baf-566a-4e66-b089-74cd9ecb38a8', includes: '*', excludes: '',
-                                     extensions: [[$class: 'SubmoduleOption', parentCredentials: true, recursiveSubmodules: true]]],
-                            targets: [BRANCH_NAME, 'master'])
-      }
+    dir('src/franka_ros') {
+      checkout scm
     }
 
-    stage('Build libfranka') {
-      dir('libfranka/build') {
-        sh 'cmake -DCMAKE_BUILD_TYPE=Debug -DBUILD_COVERAGE=OFF -DBUILD_TESTS=OFF -DBUILD_EXAMPLES=OFF -DBUILD_DOCUMENTATION=OFF ..'
-        sh 'cmake --build .'
+    sh 'rm -rf dist'
+    dir('dist') {
+      try {
+        step([$class: 'CopyArtifact',
+              filter: 'libfranka-*-amd64.tar.gz',
+              fingerprintArtifacts: true,
+              projectName: "SWDEV/libfranka/${BRANCH_NAME}",
+              selector: [$class: 'StatusBuildSelector', stable: false]])
+      } catch (e) {
+        // Fall back to master branch.
+        step([$class: 'CopyArtifact',
+              filter: 'libfranka-*-amd64.tar.gz',
+              fingerprintArtifacts: true,
+              projectName: "SWDEV/libfranka/master",
+              selector: [$class: 'StatusBuildSelector', stable: false]])
       }
+      sh '''
+        tar xfz libfranka-*-amd64.tar.gz
+        ln -sf libfranka-*-amd64 libfranka
+      '''
     }
 
-    dir('catkin_ws') {
-      stage('Build in debug mode') {
-        env.FRANKA_DIR = "${pwd()}/../libfranka/build"
-        sh 'src/scripts/ci/debug-build.sh'
-      }
-
-      stage('Archive results') {
-        junit 'build/test_results/**/*.xml'
+    docker.build('franka_ros-ci-worker', 'src/franka_ros/.ci').inside {
+      withEnv(["CMAKE_PREFIX_PATH+=${env.WORKSPACE}/dist/libfranka/lib/cmake/Franka",
+               "ROS_HOME=${env.WORKSPACE}/ros-home"]) {
+        stage('Build & Lint (Debug)') {
+          sh 'src/franka_ros/.ci/debug.sh'
+          junit 'build-debug/test_results/**/*.xml'
+        }
       }
     }
+
     currentBuild.result = 'SUCCESS'
   } catch (e) {
     currentBuild.result = 'FAILED'
     throw e;
   } finally {
-    // Explicitly specify commit hash, otherwise Stash will get notified about the `libfranka` commit as well.
-    if (gitCommit) {
-      step([$class: 'StashNotifier', commitSha1: gitCommit])
-    } else {
-      step([$class: 'StashNotifier'])
-    }
+    step([$class: 'StashNotifier'])
   }
 }
diff --git a/scripts/ci/debug-build.sh b/scripts/ci/debug-build.sh
deleted file mode 100755
index ffd606ea26a5393ee6fc66a47aae15019ba32ed8..0000000000000000000000000000000000000000
--- a/scripts/ci/debug-build.sh
+++ /dev/null
@@ -1,12 +0,0 @@
-#!/bin/sh
-
-set -e
-
-. /opt/ros/kinetic/setup.sh
-
-rm -rf build devel
-catkin_make --cmake-args -DFranka_DIR:PATH=$FRANKA_DIR -DCMAKE_EXPORT_COMPILE_COMMANDS=ON
-. devel/setup.sh
-catkin_make -j1 check-format
-catkin_make -j1 check-tidy
-catkin_make run_tests