diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 83362734b12451c32ec27b482ccb4ad39fd3c6d7..efa0c2cf9f1d4f06ec5e2595adc87c612f6fee0f 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -108,7 +108,7 @@ unit-tests:
   tags:
     - heavyweight
   services:
-    - name: docker:stable-dind
+    - name: docker:28-dind
       alias: docker
   variables:
     DOCKER_DRIVER: overlay2
@@ -134,6 +134,11 @@ unit-tests:
         worker:
           image: $CI_REGISTRY_IMAGE/worker:$TAG_SHA
       volumes:
+        aptly-storage:
+          driver_opts:
+            type: none
+            o: bind
+            device: './storage/aptly'
         backend-storage:
           driver_opts:
             type: none
@@ -169,7 +174,7 @@ unit-tests:
     - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" "$CI_REGISTRY"
     - docker info
     - echo "$COMPOSE_OVERLAY" > docker-compose.override.yml
-    - mkdir -p logs/backend logs/frontend logs/worker storage/backend
+    - mkdir -p logs/backend logs/frontend logs/worker storage/backend storage/aptly
     - |
       cat << EOF > ~/.oscrc
       [general]
@@ -179,6 +184,7 @@ unit-tests:
       pass = opensuse
       EOF
     - docker-compose up -d
+    - docker-compose ps
     - wait-for-it docker:5252 -s --timeout=180 -- echo "The OBS backend is up"
     - wait-for-it docker:80 -s --timeout=180 -- echo "The OBS frontend is up"
     - PATH=$PWD/tests/scripts:$PATH
diff --git a/tests/04-test-aptly b/tests/04-test-aptly
new file mode 100755
index 0000000000000000000000000000000000000000..2a8483f194067e2d0fdda7e01310d2df74d41589
--- /dev/null
+++ b/tests/04-test-aptly
@@ -0,0 +1,122 @@
+#!/bin/sh
+
+. $(dirname $0)/scripts/common.sh
+
+set +x
+
+prj_arch="x86_64"
+prj_components="target development sdk"
+prj_distro="v2030"
+prj_osname="apertis"
+prj_repo="default"
+prj_public_prefix="shared/apertis/public/apertis"
+
+get_prjconf () {
+	prj_distro="$1"
+	cat << EOF
+Repotype: debian
+type: dsc
+release: b$prj_distro.0b<B_CNT>
+buildengine: debootstrap
+EOF
+}
+
+get_empty_prjmeta() {
+	cat << EOF
+<project name="$1">
+  <title>$1 repository</title>
+</project>
+EOF
+}
+
+APTLY_API_URL=http://aptly:8080
+
+aptlyctl() {
+	echo + APTLY_API_URL=$APTLY_API_URL aptlyctl "$@"
+	docker-compose --project-directory "$OBSDIR" exec -T -e APTLY_API_URL=$APTLY_API_URL backend aptlyctl "$@"
+}
+
+wait_for_aptly() {
+	iterations=5
+	wait=5
+
+	echo "Checking aptly: $@"
+
+	for i in $(seq $iterations)
+	do
+		if aptlyctl "$@"
+		then
+			break
+		fi
+		if [ $i -eq $iterations ]
+		then
+			return 1
+		fi
+		echo "Sleeping ${wait}s (iteration $i/$iterations)"
+		sleep $wait
+	done
+}
+
+cleanup() {
+	aptlyctl publish drop --ignore-if-missing $prj_public_prefix $prj_distro
+	for prj_component in $prj_components
+	do
+		project="$prj_osname:$prj_distro:$prj_component"
+		# Newer osc can do osc repo remove
+		get_empty_prjmeta $project | osc meta prj "$project" -F -
+	done
+
+	tap_end
+}
+
+trap cleanup EXIT
+
+for prj_component in $prj_components
+do
+	project="$prj_osname:$prj_distro:$prj_component"
+
+	create-project --publish Debian:$debian_release:main/main "$project/$prj_repo"
+
+	get_prjconf $prj_distro | osc meta prjconf "$project" -F -
+
+	repo="$project/$prj_repo"
+
+	wait_for_aptly repo test-exists "$repo"
+	tap_ok $? "aptly repo for $prj_component exists"
+done
+
+echo "Checking aptly distribution publish: $prj_public_prefix $prj_distro"
+wait_for_aptly publish test-exists $prj_public_prefix $prj_distro
+tap_ok $? "aptly publish exists"
+
+test_component=${prj_components%% *}
+pkg_version=$(date +%+4Y%m%d.%H%M%S)
+
+cat > test-pkg.equivs <<EOF
+Package: test-pkg
+Version: $pkg_version
+Build-Depends: debhelper-compat (= 12)
+EOF
+
+bin_files=test-pkg
+
+equivs-build --source test-pkg.equivs
+
+osc dput $prj_osname:$prj_distro:$test_component test-pkg_*.dsc
+tap_ok $? "upload and create test package"
+
+wait-for-pkg $prj_osname:$prj_distro:$test_component test-pkg $prj_repo x86_64
+tap_ok $? "build test package"
+
+echo Waiting for the package to publish
+sleep 5
+
+for bin_file in $bin_files; do
+	echo "Checking package binary: $bin_file"
+	aptlyctl repo search --exit-code "$prj_osname:$prj_distro:$test_component/$prj_repo" "$bin_file" | grep "$pkg_version"
+	tap_ok $? "binary package $bin_file present in aptly repo"
+done
+
+echo DONE
+
+
diff --git a/tests/backend-config/BSConfig.local.pm b/tests/backend-config/BSConfig.local.pm
index baa3fe27b3ecc9526156727eee5ff2ff2e7f80a7..0db3cff0e929c1eae0519f907ac40be4cf7069e4 100644
--- a/tests/backend-config/BSConfig.local.pm
+++ b/tests/backend-config/BSConfig.local.pm
@@ -1,5 +1,5 @@
 my @reprepro_releases = ("v2022");
-my @aptly_releases = ("v2023");
+my @aptly_releases = ("v2030");
 my @apertis_components = ("target", "development", "sdk");
 my $apertis_prefix = "shared/apertis/public/apertis";
 my $aptly_server = {