diff --git a/.github/workflows/base-ci-goreleaser.yaml b/.github/workflows/base-ci-goreleaser.yaml
index 3418d13b..a75c16b9 100644
--- a/.github/workflows/base-ci-goreleaser.yaml
+++ b/.github/workflows/base-ci-goreleaser.yaml
@@ -74,3 +74,10 @@ jobs:
           GOOS: ${{ matrix.GOOS }}
           GOARCH: ${{ matrix.GOARCH }}
           GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }}
+
+      - name: Upload linux service packages
+        if: ${{ matrix.GOOS == 'linux' && matrix.GOARCH == 'amd64' }}
+        uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4
+        with:
+          name: linux-packages
+          path: distributions/${{ inputs.distribution }}/dist/linux_amd64/*
diff --git a/.github/workflows/ci-goreleaser-contrib.yaml b/.github/workflows/ci-goreleaser-contrib.yaml
index 9d01d015..ad7db0a6 100644
--- a/.github/workflows/ci-goreleaser-contrib.yaml
+++ b/.github/workflows/ci-goreleaser-contrib.yaml
@@ -31,3 +31,11 @@ jobs:
       goos: '[ "linux", "windows", "darwin" ]'
       goarch: '[ "386", "amd64", "arm64", "ppc64le", "arm", "s390x" ]'
     secrets: inherit
+
+  package-tests:
+     name: Package tests
+     needs: check-goreleaser
+     uses: ./.github/workflows/package-tests.yaml
+     with:
+       distribution: otelcol-contrib
+       type: '[ "deb", "rpm" ]'
diff --git a/.github/workflows/ci-goreleaser-core.yaml b/.github/workflows/ci-goreleaser-core.yaml
index 5b447676..50410eb7 100644
--- a/.github/workflows/ci-goreleaser-core.yaml
+++ b/.github/workflows/ci-goreleaser-core.yaml
@@ -32,3 +32,11 @@ jobs:
       goos: '[ "linux", "windows", "darwin" ]'
       goarch: '[ "386", "amd64", "arm64", "ppc64le", "arm", "s390x" ]'
     secrets: inherit
+
+  package-tests:
+     name: Package tests
+     needs: check-goreleaser
+     uses: ./.github/workflows/package-tests.yaml
+     with:
+       distribution: otelcol
+       type: '[ "deb", "rpm" ]'
diff --git a/.github/workflows/package-tests.yaml b/.github/workflows/package-tests.yaml
new file mode 100644
index 00000000..90429064
--- /dev/null
+++ b/.github/workflows/package-tests.yaml
@@ -0,0 +1,30 @@
+name: Package Tests
+
+on:
+  workflow_call:
+    inputs:
+      type:
+        required: true
+        type: string
+      distribution:
+        required: true
+        type: string
+
+jobs:
+  package-tests:
+    name: Package Tests
+    runs-on: ubuntu-latest
+    strategy:
+      matrix:
+        type: ${{ fromJSON(inputs.type) }}
+    steps:
+      - name: Checkout
+        uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
+
+      - name: Download built artifacts
+        uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
+        with:
+          name: linux-packages
+
+      - name: Test ${{ matrix.type }} package
+        run: ./scripts/package-tests/package-tests.sh ./otelcol*-SNAPSHOT-*_linux_amd64.${{ matrix.type }} ${{ inputs.distribution }}
diff --git a/.github/workflows/shellcheck.yml b/.github/workflows/shellcheck.yml
index 845cb6d7..65794f52 100644
--- a/.github/workflows/shellcheck.yml
+++ b/.github/workflows/shellcheck.yml
@@ -13,3 +13,5 @@ jobs:
       - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
       - name: Run ShellCheck
         uses: ludeeus/action-shellcheck@00cae500b08a931fb5698e11e79bfbd38e612a38 # 2.0.0
+        env:
+          SHELLCHECK_OPTS: -x
diff --git a/.goreleaser.yaml b/.goreleaser.yaml
index 0d0c3d3b..8a776314 100644
--- a/.goreleaser.yaml
+++ b/.goreleaser.yaml
@@ -1,3 +1,4 @@
+version: 2
 release:
   header: |
     Check the [v{{.Version}} contrib changelog](https://github.com/open-telemetry/opentelemetry-collector-contrib/releases/tags/v{{.Version}}) and the [v{{.Version}} core changelog](https://github.com/open-telemetry/opentelemetry-collector/releases/tags/v{{.Version}}) for changelogs on specific components.
diff --git a/distributions/otelcol-k8s/.goreleaser.yaml b/distributions/otelcol-k8s/.goreleaser.yaml
index f7d91cc2..8e6aa0f0 100644
--- a/distributions/otelcol-k8s/.goreleaser.yaml
+++ b/distributions/otelcol-k8s/.goreleaser.yaml
@@ -1,5 +1,6 @@
 partial:
   by: target
+version: 2
 project_name: opentelemetry-collector-releases
 env:
   - COSIGN_YES=true
diff --git a/go.mod b/go.mod
index 93f95434..0cb7003d 100644
--- a/go.mod
+++ b/go.mod
@@ -9,6 +9,6 @@ require (
 
 require (
 	github.com/kr/pretty v0.3.1 // indirect
-	github.com/rogpeppe/go-internal v1.11.0 // indirect
+	github.com/rogpeppe/go-internal v1.12.0 // indirect
 	gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
 )
diff --git a/go.sum b/go.sum
index 0ced732f..0ff890e8 100644
--- a/go.sum
+++ b/go.sum
@@ -10,8 +10,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
 github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
 github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
-github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
-github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
+github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
+github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
diff --git a/scripts/package-tests/Dockerfile.test.deb b/scripts/package-tests/Dockerfile.test.deb
new file mode 100644
index 00000000..fd17c383
--- /dev/null
+++ b/scripts/package-tests/Dockerfile.test.deb
@@ -0,0 +1,14 @@
+# A debian image with systemd enabled.  Must be run with:
+# `-d --privileged -v /sys/fs/cgroup:/sys/fs/cgroup:ro` flags
+FROM debian:12
+
+ENV container docker
+ENV DEBIAN_FRONTEND noninteractive
+
+# Enable systemd.
+RUN apt-get update ; \
+    apt-get install -y systemd systemd-sysv procps; \
+    apt-get clean ; \
+    rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
+
+CMD ["/lib/systemd/systemd"]
diff --git a/scripts/package-tests/Dockerfile.test.rpm b/scripts/package-tests/Dockerfile.test.rpm
new file mode 100644
index 00000000..9e7cf481
--- /dev/null
+++ b/scripts/package-tests/Dockerfile.test.rpm
@@ -0,0 +1,9 @@
+# A rockylinux image with systemd enabled.  Must be run with:
+# `-d --privileged -v /sys/fs/cgroup:/sys/fs/cgroup:ro` flags
+FROM rockylinux:9.3
+
+ENV container docker
+
+RUN dnf install -y initscripts
+
+CMD ["/usr/sbin/init"]
diff --git a/scripts/package-tests/README.md b/scripts/package-tests/README.md
new file mode 100644
index 00000000..924565ac
--- /dev/null
+++ b/scripts/package-tests/README.md
@@ -0,0 +1,26 @@
+# Build and test deb/rpm/apk packages
+
+## Prerequisites
+
+Tools:
+
+- [Go](https://go.dev/)
+- [GoReleaser](https://goreleaser.com/)
+- [Podman](https://podman.io/)
+- make
+
+## How to build and test
+
+To build the Collector Linux packages, a few steps are required:
+
+- Run `make generate` to (re-)generate sources and GoReleaser files
+- Go to the distribution folder that you want to build (under the `distributions` folder)
+- Run `goreleaser release --snapshot --clean --skip sbom,sign,archive,docker`
+    - This will build the necessary release assets with all architectures and packaging types into the `dist` folder inside your
+      current folder. (We can skip many parts of the release build that we don't need for running the package tests locally)
+    - We use GoReleaser Pro only features in CI. If you want to run this locally, and you run into `unmarshal` errors, 
+    you may have to remove the parts that goreleaser complains about or use a pro license.
+- Go back to the root of the repo
+- To start the package tests,
+  run: `./scripts/package-tests/package-tests.sh ./distributions/<otelcol|otelcol-contrib>/dist/<otelcol|otelcol-contrib>_*-SNAPSHOT-*_linux_amd64.<deb|rpm> <otelcol|otelcol-contrib>`
+
diff --git a/scripts/package-tests/common.sh b/scripts/package-tests/common.sh
new file mode 100755
index 00000000..18c859e2
--- /dev/null
+++ b/scripts/package-tests/common.sh
@@ -0,0 +1,44 @@
+#!/bin/bash
+
+# Copyright The OpenTelemetry Authors
+# SPDX-License-Identifier: Apache-2.0
+
+podman_cp() {
+    local container="$1"
+    local src="$2"
+    local dest="$3"
+    local dest_dir
+    dest_dir="$( dirname "$dest" )"
+
+    echo "Copying $src to $container:$dest ..."
+    podman exec "$container" mkdir -p "$dest_dir"
+    podman cp "$src" "$container":"$dest"
+}
+
+install_pkg() {
+    local container="$1"
+    local pkg_path="$2"
+    local pkg_base
+    pkg_base=$( basename "$pkg_path" )
+
+    echo "Installing $pkg_base ..."
+    podman_cp "$container" "$pkg_path" /tmp/"$pkg_base"
+    if [[ "${pkg_base##*.}" = "deb" ]]; then
+        podman exec "$container" dpkg -i /tmp/"$pkg_base"
+    else
+        podman exec "$container" rpm -ivh /tmp/"$pkg_base"
+    fi
+}
+
+uninstall_pkg() {
+    local container="$1"
+    local pkg_type="$2"
+    local pkg_name="$3"
+
+    echo "Uninstalling $pkg_name ..."
+    if [[ "$pkg_type" = "deb" ]]; then
+        podman exec "$container" dpkg -r "$pkg_name"
+    else
+        podman exec "$container" rpm -e "$pkg_name"
+    fi
+}
diff --git a/scripts/package-tests/package-tests.sh b/scripts/package-tests/package-tests.sh
new file mode 100755
index 00000000..543f626f
--- /dev/null
+++ b/scripts/package-tests/package-tests.sh
@@ -0,0 +1,99 @@
+#!/bin/bash
+
+# Copyright The OpenTelemetry Authors
+# SPDX-License-Identifier: Apache-2.0
+
+set -euov pipefail
+
+SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
+REPO_DIR="$( cd "$SCRIPT_DIR/../../../../" && pwd )"
+export REPO_DIR
+PKG_PATH="${1:-}"
+DISTRO="${2:-}"
+
+SERVICE_NAME=$DISTRO
+PROCESS_NAME=$DISTRO
+
+# shellcheck source=scripts/package-tests/common.sh
+source "$SCRIPT_DIR"/common.sh
+
+if [[ -z "$PKG_PATH" ]]; then
+    echo "usage: ${BASH_SOURCE[0]} DEB_OR_RPM_PATH" >&2
+    exit 1
+fi
+
+if [[ ! -f "$PKG_PATH" ]]; then
+    echo "$PKG_PATH not found!" >&2
+    exit 1
+fi
+
+
+pkg_base="$( basename "$PKG_PATH" )"
+pkg_type="${pkg_base##*.}"
+if [[ ! "$pkg_type" =~ ^(deb|rpm)$ ]]; then
+    echo "$PKG_PATH not supported!" >&2
+    exit 1
+fi
+image_name="otelcontribcol-$pkg_type-test"
+container_name="$image_name"
+container_exec="podman exec $container_name"
+
+trap 'podman rm -fv $container_name >/dev/null 2>&1 || true' EXIT
+
+podman build -t "$image_name" -f "$SCRIPT_DIR/Dockerfile.test.$pkg_type" "$SCRIPT_DIR"
+podman rm -fv "$container_name" >/dev/null 2>&1 || true
+
+# test install
+CRUN_VER='1.14.4'
+mkdir -p "${HOME}/.local/bin"
+curl -L "https://github.com/containers/crun/releases/download/${CRUN_VER}/crun-${CRUN_VER}-linux-amd64" -o "${HOME}/.local/bin/crun"
+chmod +x "${HOME}/.local/bin/crun"
+mkdir -p "${HOME}/.config/containers"
+cat << EOF > "${HOME}/.config/containers/containers.conf"
+[engine.runtimes]
+crun = [
+  "${HOME}/.local/bin/crun",
+  "/usr/bin/crun"
+]
+EOF
+
+echo
+podman run --name "$container_name" -d "$image_name"
+install_pkg "$container_name" "$PKG_PATH"
+
+# ensure service has started and still running after 5 seconds
+sleep 5
+echo "Checking $SERVICE_NAME service status ..."
+$container_exec systemctl --no-pager status "$SERVICE_NAME"
+
+echo "Checking $PROCESS_NAME process ..."
+if [ "$DISTRO" = "otelcol" ]; then
+  $container_exec pgrep -a -u otel "$PROCESS_NAME"
+else
+  $container_exec pgrep -a -u otelcol-contrib "$PROCESS_NAME"
+fi
+
+# test uninstall
+echo
+uninstall_pkg "$container_name" "$pkg_type" "$DISTRO"
+
+echo "Checking $SERVICE_NAME service status after uninstall ..."
+if $container_exec systemctl --no-pager status "$SERVICE_NAME"; then
+    echo "$SERVICE_NAME service still running after uninstall" >&2
+    exit 1
+fi
+echo "$SERVICE_NAME service successfully stopped after uninstall"
+
+echo "Checking $SERVICE_NAME service existence after uninstall ..."
+if $container_exec systemctl list-unit-files --all | grep "$SERVICE_NAME"; then
+    echo "$SERVICE_NAME service still exists after uninstall" >&2
+    exit 1
+fi
+echo "$SERVICE_NAME service successfully removed after uninstall"
+
+echo "Checking $PROCESS_NAME process after uninstall ..."
+if $container_exec pgrep "$PROCESS_NAME"; then
+    echo "$PROCESS_NAME process still running after uninstall"
+    exit 1
+fi
+echo "$PROCESS_NAME process successfully killed after uninstall"