diff --git a/cookiecutter.json b/cookiecutter.json
index 1f29d355f8fcd56ebb3f1c5a3de8b49d748e60f6..8165b9764968afc310e18f5850f8b960c9c95a88 100644
--- a/cookiecutter.json
+++ b/cookiecutter.json
@@ -1,6 +1,7 @@
 {
     "project_name": "My Awesome App",
     "project_slug": "{{ cookiecutter.project_name.lower()|replace(' ', '_')|replace('-', '_')|replace('.', '_')|trim() }}",
-    "project_url": "https://git.astron.nl/{{cookiecutter.project_slug}}",
+    "project_package": "{{ cookiecutter.project_name.lower()|replace(' ', '-')|replace('_', '-')|replace('.', '-')|trim() }}",
+    "project_url": "https://git.astron.nl/{{ cookiecutter.project_slug }}",
     "description": "An example package for CI/CD working group"
 }
diff --git a/docker/ci-runner/Dockerfile b/docker/ci-runner/Dockerfile
index 8b9621755a0653909c67cf1eda3ff2bfba87063c..497b89a715157c619baadc189a896f79a0731172 100644
--- a/docker/ci-runner/Dockerfile
+++ b/docker/ci-runner/Dockerfile
@@ -1,5 +1,5 @@
-FROM python:3.11
+FROM python:3.12
 
 RUN python -m pip install --upgrade pip
-RUN pip install --upgrade cookiecutter tox twine cibuildwheel==2.13.1 cookiecutter
+RUN python -m pip install --upgrade cookiecutter tox twine cibuildwheel==2.13.1 cookiecutter
 RUN curl -sSL https://get.docker.com/ | sh
diff --git a/{{cookiecutter.project_slug}}/.dockerignore b/{{cookiecutter.project_slug}}/.dockerignore
new file mode 100644
index 0000000000000000000000000000000000000000..141f90da2fac6459ee10150e4cd3e12aff08a48b
--- /dev/null
+++ b/{{cookiecutter.project_slug}}/.dockerignore
@@ -0,0 +1,4 @@
+.tox
+build
+*.egg-info
+.venv
diff --git a/{{cookiecutter.project_slug}}/.gitignore b/{{cookiecutter.project_slug}}/.gitignore
index 5513c1bf7cbca7a522e1ea44f306bda82a80975d..885470b5e93bf922433ae1741179f1c7ae07918a 100644
--- a/{{cookiecutter.project_slug}}/.gitignore
+++ b/{{cookiecutter.project_slug}}/.gitignore
@@ -1,13 +1,19 @@
 dist/*
 *.egg-info
 *.pyc
+.tox
+.venv
 
 .coverage
 coverage.xml
 htmlcov/*
+build
+dist
 
-# Tox
-.tox
+# Documentation
+docs/source/source_documentation
+!docs/source/source_documentation/index.rst
+docs/build
 
 # setuptools-scm
 src/{{cookiecutter.project_slug}}/_version.py
diff --git a/{{cookiecutter.project_slug}}/.gitlab-ci.yml b/{{cookiecutter.project_slug}}/.gitlab-ci.yml
index 429482664900652365935d2c323b79eda4698a92..5071e25812340c37592101c92d1c91b5f7804088 100644
--- a/{{cookiecutter.project_slug}}/.gitlab-ci.yml
+++ b/{{cookiecutter.project_slug}}/.gitlab-ci.yml
@@ -14,6 +14,7 @@ stages:
   - lint
   - test
   - package
+  - images
   - integration
   - publish # publish instead of deploy
 
@@ -35,22 +36,10 @@ trigger_prepare:
     strategy: depend
     include: .prepare.gitlab-ci.yml
 
-run_black:
+run_lint:
   stage: lint
   script:
-    - tox -e black
-  allow_failure: true
-
-run_flake8:
-  stage: lint
-  script:
-    - tox -e pep8
-  allow_failure: true
-
-run_pylint:
-  stage: lint
-  script:
-    - tox -e pylint
+    - tox -e lint
   allow_failure: true
 
 sast:
@@ -74,7 +63,7 @@ secret_detection:
   before_script:
     - python --version # For debugging
     - python -m pip install --upgrade pip
-    - pip install --upgrade tox twine
+    - python -m pip install --upgrade tox twine
 
 # Run all unit tests for Python versions except the base image
 run_unit_tests:
@@ -156,8 +145,7 @@ publish_on_test_pypi:
     - package_files
   when: manual
   rules:
-    - if: $CI_COMMIT_TAG
-  allow_failure: true
+    - if: '$CI_COMMIT_TAG && $CI_COMMIT_REF_PROTECTED == "true"'
   script:
     - echo "run twine for test pypi"
     # - |
@@ -175,8 +163,7 @@ publish_on_pypi:
     - package_files
   when: manual
   rules:
-    - if: $CI_COMMIT_TAG
-  allow_failure: true
+    - if: '$CI_COMMIT_TAG && $CI_COMMIT_REF_PROTECTED == "true"'
   script:
     - echo "run twine for pypi"
     # - |
@@ -195,7 +182,7 @@ publish_to_readthedocs:
     - package_docs
   when: manual
   rules:
-    - if: $CI_COMMIT_TAG
+    - if: '$CI_COMMIT_TAG && $CI_COMMIT_REF_PROTECTED == "true"'
   script:
     - echo "scp docs/* ???"
     - exit 1
diff --git a/{{cookiecutter.project_slug}}/.pre-commit-config.yaml b/{{cookiecutter.project_slug}}/.pre-commit-config.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..ce46e6463265d2ad9704a24ae7f467cb7b50d8d9
--- /dev/null
+++ b/{{cookiecutter.project_slug}}/.pre-commit-config.yaml
@@ -0,0 +1,22 @@
+default_stages: [ pre-commit, pre-push ]
+default_language_version:
+  python: python3
+exclude: '^docs/.*\.py$'
+
+repos:
+  - repo: https://github.com/pre-commit/pre-commit-hooks
+    rev: v4.5.0
+    hooks:
+      - id: end-of-file-fixer
+      - id: trailing-whitespace
+      - id: check-yaml
+      - id: check-toml
+      - id: detect-private-key
+  - repo: local
+    hooks:
+      - id: tox-lint
+        name: tox-lint (local)
+        entry: tox
+        language: python
+        types: [file, python]
+        args: ["-e",  "lint", "--"]
diff --git a/{{cookiecutter.project_slug}}/bin/install-hooks/pre-commit.sh b/{{cookiecutter.project_slug}}/bin/install-hooks/pre-commit.sh
new file mode 100755
index 0000000000000000000000000000000000000000..792a3aabef83dc4ebf0c94635ae1de0e7412c479
--- /dev/null
+++ b/{{cookiecutter.project_slug}}/bin/install-hooks/pre-commit.sh
@@ -0,0 +1,8 @@
+#!/bin/bash
+
+if [ ! -f "setup.sh" ]; then
+  echo "pre-commit.sh must be executed with repository root as working directory!"
+  exit 1
+fi
+
+pre-commit install --hook-type pre-push
diff --git a/{{cookiecutter.project_slug}}/docker/ci-runner/Dockerfile b/{{cookiecutter.project_slug}}/docker/ci-runner/Dockerfile
index 48d4586f1eb2c4755b370b9bb4c5fe079c97fed8..2deb674a75484e11700e7f97b21bd80fe558e3f9 100644
--- a/{{cookiecutter.project_slug}}/docker/ci-runner/Dockerfile
+++ b/{{cookiecutter.project_slug}}/docker/ci-runner/Dockerfile
@@ -1,5 +1,5 @@
 FROM python:3.13
 
 RUN python -m pip install --upgrade pip
-RUN pip install --upgrade tox twine cibuildwheel==2.13.1
+RUN python -m pip install --upgrade tox twine cibuildwheel==2.13.1
 RUN curl -sSL https://get.docker.com/ | sh
diff --git a/{{cookiecutter.project_slug}}/docker/{{cookiecutter.project_slug}}/Dockerfile b/{{cookiecutter.project_slug}}/docker/{{cookiecutter.project_slug}}/Dockerfile
new file mode 100644
index 0000000000000000000000000000000000000000..1f7eb275020d13ed45cb7b0983949c12161ff953
--- /dev/null
+++ b/{{cookiecutter.project_slug}}/docker/{{cookiecutter.project_slug}}/Dockerfile
@@ -0,0 +1,18 @@
+ARG BUILD_ENV=no_copy
+
+FROM python:3.13 AS build_no_copy
+ADD ../../requirements.txt .
+COPY ../.. /work
+RUN rm -r /work/dist | true
+RUN python -m pip install --user tox
+WORKDIR /work
+RUN python -m tox -e build
+
+FROM python:3.13 AS build_copy
+COPY dist /work/dist
+
+FROM build_${BUILD_ENV} AS build
+
+FROM python:3.13-slim
+COPY --from=build /work/dist /dist
+RUN python -m pip install /dist/*.whl
diff --git a/{{cookiecutter.project_slug}}/docs/cleanup.py b/{{cookiecutter.project_slug}}/docs/cleanup.py
new file mode 100644
index 0000000000000000000000000000000000000000..3a4508d859234544bea35b1008e3c8e4f73d7cc0
--- /dev/null
+++ b/{{cookiecutter.project_slug}}/docs/cleanup.py
@@ -0,0 +1,23 @@
+#!/usr/bin/env python3
+
+#  Copyright (C) 2023 ASTRON (Netherlands Institute for Radio Astronomy)
+#  SPDX-License-Identifier: Apache-2.0
+
+import os
+
+file_dir = os.path.dirname(os.path.realpath(__file__))
+
+clean_dir = os.path.join(file_dir, "source", "source_documentation")
+print(f"Cleaning.. {clean_dir}/*")
+
+if not os.path.exists(clean_dir):
+    exit()
+
+for file_name in os.listdir(clean_dir):
+    file = os.path.join(clean_dir, file_name)
+    
+    if file_name == "index.rst":
+        continue
+
+    print(f"Removing.. {file}")
+    os.remove(file)
diff --git a/{{cookiecutter.project_slug}}/docs/cleanup.sh b/{{cookiecutter.project_slug}}/docs/cleanup.sh
deleted file mode 100644
index aac4cef22a134a44b69e99369755a58507b3794f..0000000000000000000000000000000000000000
--- a/{{cookiecutter.project_slug}}/docs/cleanup.sh
+++ /dev/null
@@ -1,14 +0,0 @@
-#!/bin/sh
-
-FILE_DIR=$(dirname -- "$(readlink -f -- "${0}")")
-
-echo "Cleaning.. ${FILE_DIR}/source/source_documentation/*"
-
-for f in "${FILE_DIR}"/source/source_documentation/*
-do
-
-  case $f in
-    */index.rst) true;;
-    *) echo "Removing.. ${f}"; rm "${f}";;
-  esac
-done
diff --git a/{{cookiecutter.project_slug}}/pyproject.toml b/{{cookiecutter.project_slug}}/pyproject.toml
index db601f122b76f82a3f3037ee7524cfb28c4a75ff..3eefe305c58c025c89fd8914fca262091cd11966 100644
--- a/{{cookiecutter.project_slug}}/pyproject.toml
+++ b/{{cookiecutter.project_slug}}/pyproject.toml
@@ -46,20 +46,19 @@ archs = ["x86_64", "i686"]
 [tool.cibuildwheel.windows]
 archs = ["AMD64", "x86"]
 
-[tool.pylint]
-ignore = "_version.py"
 
-# This does nothing until the issue below is resolved:
-# https://github.com/PyCQA/flake8/issues/234
-[tool.flake8]
-max-line-length = 88
-extend-ignore = ["E203"]
-exclude = ["_version.py"]
+[tool.ruff]
+exclude = [
+    ".venv",
+    ".git",
+    ".tox",
+    "dist",
+    "docs",
+    "*lib/python*",
+    "*egg",
+    "_version.py"
+]
+
+[tool.ruff.lint]
+ignore = ["E203"]
 
-[tool.black]
-line-length = 88
-extend-exclude = '''
-(
-    _version.py
-)
-'''
diff --git a/{{cookiecutter.project_slug}}/tests/requirements.txt b/{{cookiecutter.project_slug}}/tests/requirements.txt
index 82f1ee820e4b57fd3e720ad74078688cd87c2ead..b507faf8c6cc09660c41c58caa26d318e94cfd4d 100644
--- a/{{cookiecutter.project_slug}}/tests/requirements.txt
+++ b/{{cookiecutter.project_slug}}/tests/requirements.txt
@@ -1,8 +1,2 @@
-autopep8 >= 1.7.0 # MIT
-black >= 22.0.0 # MIT
-build >= 0.8.0 # MIT
-flake8 >= 5.0.0 # MIT
-pyproject-flake8 >= 5.0.4 # Unlicense
-pylint >= 2.15.0 # GPLv2
 pytest >= 7.0.0 # MIT
 pytest-cov >= 3.0.0 # MIT
diff --git a/{{cookiecutter.project_slug}}/tests/test_cool_module.py b/{{cookiecutter.project_slug}}/tests/test_cool_module.py
index 19e5cd2bd0bff16fa00560bdb678f9345e4e79f4..2cacc8e76f00c2d40f10bf9ff2bd28f61ca6b59a 100644
--- a/{{cookiecutter.project_slug}}/tests/test_cool_module.py
+++ b/{{cookiecutter.project_slug}}/tests/test_cool_module.py
@@ -1,3 +1,6 @@
+#  Copyright (C) 2025 ASTRON (Netherlands Institute for Radio Astronomy)
+#  SPDX-License-Identifier: Apache-2.0
+
 """Testing of the Cool Module"""
 from unittest import TestCase