diff --git a/.github/workflows/maven-build.yml b/.github/workflows/maven-build.yml
index 6c38e84264dd791f075728c170058958abe5761c..ca00582aefad65f9b1f448d03ec2886ca5bf5ef0 100644
--- a/.github/workflows/maven-build.yml
+++ b/.github/workflows/maven-build.yml
@@ -1,3 +1,4 @@
+---
 name: Java CI with Maven
 
 on:
@@ -17,7 +18,7 @@ jobs:
         uses: actions/setup-java@v2
         with:
           java-version: '17'
-          distribution: 'adopt'
+          distribution: 'temurin'
           cache: maven
       - name: Build with Maven
         run: mvn -B install --file pom.xml -Djacoco.skip=true -DdisableXmlReport=true
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
new file mode 100644
index 0000000000000000000000000000000000000000..876083889e4c04999741c27f96d13cd897aab10a
--- /dev/null
+++ b/.gitlab-ci.yml
@@ -0,0 +1,139 @@
+---
+# Based on the Maven CI/CD template from GitLab: https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Maven.gitlab-ci.yml
+variables:
+  # This will suppress any download for dependencies and plugins or upload messages which would clutter the console log.
+  # `showDateTime` will show the passed time in milliseconds. You need to specify `--batch-mode` to make this work.
+  MAVEN_OPTS: >
+    -Dhttps.protocols=TLSv1.2
+    -Dmaven.repo.local=$CI_PROJECT_DIR/.m2/repository
+    -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=WARN
+    -Dorg.slf4j.simpleLogger.showDateTime=true
+    -Djava.awt.headless=true
+  # As of Maven 3.3.0 instead of this, you may define these options in `.mvn/maven.config` so the same config is used
+  # when running from the command line.
+  # `installAtEnd` and `deployAtEnd` are only effective with the recent version of the corresponding plugins.
+  MAVEN_CLI_OPTS: >
+    --batch-mode
+    --errors
+    --fail-at-end
+    --show-version
+    -DinstallAtEnd=true
+    -DdeployAtEnd=true
+
+# Check the list of available templates at https://gitlab.com/gitlab-org/gitlab/-/tree/master/lib/gitlab/ci/templates
+include:
+  - template: Jobs/Secret-Detection.gitlab-ci.yml
+  - template: Workflows/MergeRequest-Pipelines.gitlab-ci.yml
+
+stages:
+  - build
+  - test
+  - deploy
+  - report
+
+.upstream-deploy-production-rules:
+  rules:
+    - if: $CI_PROJECT_NAMESPACE != "pmpl/examples"
+      when: never
+    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
+      when: always
+      allow_failure: true
+
+build:
+  stage: build
+  image: docker.io/library/maven:3.8.6-eclipse-temurin-17-focal
+  before_script:
+    - java -version && javac --version && mvn --version
+    - pwd
+  script:
+    - mvn $MAVEN_CLI_OPTS -DskipTests
+      -Dmaven.repo.local=$CI_PROJECT_DIR/.m2/repository package
+  cache:
+    key:
+      files:
+        - pom.xml
+    paths:
+      - .m2/repository
+  artifacts:
+    paths:
+      - target/
+
+test:
+  stage: test
+  image: docker.io/library/maven:3.8.6-eclipse-temurin-17-focal
+  needs:
+    - build
+  before_script:
+    - java -version && javac --version && mvn --version
+    - pwd
+  script:
+    # Run test suites and generate test reports
+    - mvn clean verify
+    # Get line coverage
+    - grep -o "Total[^%]*%" target/site/jacoco/index.html
+  coverage: '/Total.*?(\d{1,3})%/'
+  cache:
+    key:
+      files:
+        - pom.xml
+    paths:
+      - .m2/repository
+  artifacts:
+    paths:
+      - target/*.exec
+      - target/site/jacoco/
+    reports:
+      junit:
+        - target/surefire-reports/TEST-*.xml
+
+deploy:
+  stage: deploy
+  image: docker.io/dokku/ci-docker-image:0.9.3
+  rules: !reference [.upstream-deploy-production-rules, rules]
+  variables:
+    GIT_DEPTH: "0"
+    GIT_REMOTE_URL: "ssh://dokku@dokku-ppl.cs.ui.ac.id:22/spring-petclinic-rest"
+    SSH_PRIVATE_KEY: $PRODUCTION_SSH_PRIVATE_KEY
+    BRANCH: $CI_DEFAULT_BRANCH
+  script:
+    - dokku-deploy
+  after_script:
+    - dokku-unlock
+  environment:
+    name: production
+    url: https://spring-petclinic-rest.dokku-ppl.cs.ui.ac.id
+  needs:
+    - test
+  dependencies: []
+
+visualize-coverage:
+  stage: report
+  image: registry.gitlab.com/haynes/jacoco2cobertura:1.0.9
+  before_script: []
+  script:
+    - python /opt/cover2cover.py target/site/jacoco/jacoco.xml $CI_PROJECT_DIR/src/main/java > target/cobertura.xml
+  needs:
+    - test
+  dependencies:
+    - test
+  artifacts:
+    reports:
+      coverage_report:
+        coverage_format: cobertura
+        path: target/cobertura.xml
+
+sonarqube-check:
+  stage: report
+  image: docker.io/library/maven:3.8.6-eclipse-temurin-17-focal
+  variables:
+    SONAR_USER_HOME: "${CI_PROJECT_DIR}/.sonar"
+    GIT_DEPTH: "0"
+  rules: !reference [.upstream-deploy-production-rules, rules]
+  script:
+    - mvn -DskipTests verify sonar:sonar
+  cache:
+    key: "${CI_JOB_NAME}"
+    paths:
+      - .sonar/cache
+
+# TODO: Add manual CI job to re-deploy or re-create the deployed app on Dokku PPL
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000000000000000000000000000000000000..fbfaa878548d288f88d4ff7b6d24da39f0ec828a
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,33 @@
+# Use JDK & Maven image to build the application
+FROM docker.io/library/maven:3.9.4-eclipse-temurin-17-alpine AS builder
+
+# Set the working directory inside the container
+WORKDIR /src
+
+# Copy the source code into the container
+COPY . .
+
+# Build the application JAR file
+RUN mvn -DskipTests package
+
+# Use JRE image for running the application
+FROM docker.io/library/eclipse-temurin:17.0.8.1_1-jre-alpine
+
+# Create a non-root user named "app" to own and run the application
+RUN addgroup app \
+    && adduser -s /bin/false -G app -D -H app
+
+# Switch to the "app" user, so the application does not run as root
+USER app
+
+# Set the working directory inside the container to /opt/app
+WORKDIR /opt/app
+
+# Copy the app into the container
+COPY --chown=app:app --from=builder /src/target/sitodo-*.jar .
+
+# Expose port 9966
+EXPOSE 9966
+
+# Run the application JAR file
+CMD ["/bin/sh", "-c", "java -jar spring-petclinic-rest-*.jar"]
diff --git a/pom.xml b/pom.xml
index c95b91e873bdd31cc8afebe8cabd43c4539decbf..bdb11afd031fd6945e0a2eb697ba9b7133d022b1 100644
--- a/pom.xml
+++ b/pom.xml
@@ -18,7 +18,7 @@
     </parent>
 
     <properties>
-        <!-- Third librairies -->
+        <!-- Third-party libraries -->
         <spring-data-jdbc.version>1.2.1.RELEASE</spring-data-jdbc.version>
         <springdoc-openapi-ui.version>2.0.2</springdoc-openapi-ui.version>
         <jackson-databind-nullable.version>0.2.1</jackson-databind-nullable.version>
@@ -29,11 +29,19 @@
         <jacoco.version>0.8.8</jacoco.version>
         <openapi-generator-maven-plugin.version>6.3.0</openapi-generator-maven-plugin.version>
         <build-helper-maven-plugin.version>3.2.0</build-helper-maven-plugin.version>
+        <sonar-maven-plugin.version>3.9.1.2184</sonar-maven-plugin.version>
 
         <!-- Docker -->
-        <docker.jib-maven-plugin.version>1.3.0</docker.jib-maven-plugin.version>
+        <docker.jib-maven-plugin.version>3.4.0</docker.jib-maven-plugin.version>
         <docker.image.prefix>springcommunity</docker.image.prefix>
         <maven-compiler-plugin.version>3.8.1</maven-compiler-plugin.version>
+
+        <!-- Sonar Scanner -->
+        <sonar.host.url>https://sonarqube.cs.ui.ac.id</sonar.host.url>
+        <sonar.projectKey>pmpl_examples_spring-petclinic-rest_AYtlCaXx94kwVhMRxVzs</sonar.projectKey>
+        <sonar.coverage.jacoco.xmlReportPaths>target/site/jacoco/*.xml</sonar.coverage.jacoco.xmlReportPaths>
+        <sonar.junit.reportPaths>target/surefire-reports/*.xml</sonar.junit.reportPaths>
+        <sonar.qualitygate.wait>true</sonar.qualitygate.wait>
     </properties>
 
     <dependencies>
@@ -339,6 +347,11 @@
                     </compilerArgs>
                 </configuration>
             </plugin>
+            <plugin>
+                <groupId>org.sonarsource.scanner.maven</groupId>
+                <artifactId>sonar-maven-plugin</artifactId>
+                <version>${sonar-maven-plugin.version}</version>
+            </plugin>
         </plugins>
     </build>
 </project>