diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
new file mode 100644
index 0000000000000000000000000000000000000000..8e0738069de2c47eb9f561115a308e4becff2305
--- /dev/null
+++ b/.gitlab-ci.yml
@@ -0,0 +1,45 @@
+# Example of basic pipeline triggered on commit to the main branch. We assume
+# to use a minimalistic Alpine Linux Docker image.
+---
+variables:
+  # PRODUCTION_IP: "<production-server-IP>"
+  APP_PAGE: "public/index.html"
+  CLONE_URL_SSH: "ssh://git@ssh.hesge.ch:${CI_PROJECT_ID}"
+  CLONE_URL_TOK: "https://gitlab-ci-token:${CI_JOB_TOKEN}@gitedu.hesge.ch"
+
+stages:
+  - test
+  - deploy
+
+# Commmon to all stages -- needed because the image's storage is not persistent across them
+before_script:
+  # uncomment for debugging. Watch out! It *will* expose any secret passed from an unprotected variable.
+  # - export
+  - apk -U add git openssh-client rsync
+  - mkdir -p -m 0700 ~/.ssh
+  - touch ~/.ssh/known_hosts && chmod 644 ~/.ssh/known_hosts
+  - '[[ -f /.dockerenv ]] && echo -e "Host *\n\tStrictHostKeyChecking no\n\n" >> ~/.ssh/config'
+  - git config --global user.email "deployer@ci-cd.lab"
+  - git config --global user.name "The Deployer"
+  - eval $(ssh-agent -s)
+  - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -
+  - echo "$SSH_KNOWN_HOSTS" >> ~/.ssh/known_hosts
+
+unit_test1:
+  stage: test
+  script:
+    - echo "This job shall test that the version string in '${APP_PAGE}' is well formatted"
+    - git clone --depth=1  ${CLONE_URL_TOK}/${CI_PROJECT_PATH}
+    - '[[ $(sed -nr "/^\s*Version:\s+([[:digit:]]\.){2}[[:digit:]]\s*\$/p" ${CI_PROJECT_NAME}/${APP_PAGE} | wc -l) -eq 1 ]]
+        || { echo "[error] Version string badly formatted or not found"; exit 1; }'
+
+deploy_prod:
+  stage: deploy
+  environment:
+    name: production
+    url: http://${PRODUCTION_IP}
+  script:
+    - echo "Deploying from ${CI_REPOSITORY_URL}::${CI_COMMIT_REF_NAME}"
+    - rsync --rsync-path="sudo rsync" -Cavh ${CI_PROJECT_DIR}/public/ debian@${PRODUCTION_IP}:/var/www/html/
+  only:
+    - main
diff --git a/README.md b/README.md
index 592d43412e23180791fffab94cc0f87f9d33f346..81151d4de0869e31f1671ab789cd2684918565b8 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,124 @@
 # Lab-GitLab-CI-CD
 
-Simple app deployment exercise (DevOps) with GitLab CI/CD
\ No newline at end of file
+Simple app deployment exercise (DevOps) with GitLab CI/CD.
+
+## Pedagogical objectives ##
+
+  * Become familiar with DevOps practices
+  * Learn how to automatically deploy things via GitLab CD/CD
+
+## Scenario  ##
+
+A simple Web application composed of just one HTML page has to be
+automatically deployed to a production Web server, whenever a new commit is
+pushed to the `main` branch, but only if an app unit test passes.
+
+The production server is supposed to be already provisioned and installed.
+
+The Web app page `public/index.html` just shows an app version number. The app
+unit test shall check that the above number is correctly formatted.
+
+The goal is to set up a simple GitLab C/CD pipeline to implement the scenario
+above.
+
+### Workflow ###
+
+This is a conceptual description of the workflow that the GitLab pipeline
+shall implement. Each phase below is prefixed by a 'prompt' specifying *who*
+is playing:
+
+* M = Maintainer -- manual action
+* G = GitLab -- automatic action
+
+1. M: increment the version number written in `public/index.html`. E.g..:
+   `x.y.z => x.y.z+1`.
+1. M: commit and push.
+1. G: trigger a pipeline, whose stages are:
+   1. *test*: check the version number format. If OK,
+   1. *deploy*: push the new app to the production server.
+
+
+## Tasks ##
+
+In this lab you will perform a number of tasks and document your progress in a
+lab report. Each task specifies one or more deliverables to be
+produced. Collect all the deliverables in your lab report.
+
+**N.B.** Some tasks require interacting with your local machine's OS: any
+related commands are supposed to be run into a terminal with the following
+conventions about the *command line prompt*:
+
+  * `#`: execution with super user's (root) privileges
+  * `$`: execution with normal user's privileges
+  * `lcl`: your local machine
+  * `ins`: your VM instance
+
+
+### Task #1: GitLab CI/CD setup ###
+
+**Goal:** set up your GitLab CI/CD via its Web GUI.
+
+Our GitLab instance comes with a shared Docker-based CI/CD *runner*.
+
+Deploy [SSH keys for the Docker
+executor](https://docs.gitlab.com/ee/ci/ssh_keys/). :bulb: All secrets should
+be **protected** and possibly **masked**. The following variables have to be added:
+`SSH_PRIVATE_KEY`, `SSH_KNOWN_HOSTS` and `PRODUCTION_IP`.
+
+Configure the CI/CD system as it follows in section **Settings > CI/CD >
+General pipelines**:
+- [ ] Public pipelines
+- [x] Auto-cancel redundant pipelines
+- [x] Skip outdated deployment jobs
+- Git strategy: fetch
+- Git shallow clone: 1
+- Timeout: 10m
+
+
+### Task #2: CI/CD pipeline ###
+
+**Goal:** write a GitLab-style pipeline file `.gitlab-ci.yml`.
+
+For our simple DevOps scenario, the following structure suffices:
+
+``` yaml
+---
+# Stages are job collections and run sequentially
+stages:
+  - test
+  - deploy
+
+# Do here anything common to all jobs' scripts
+before_script:
+  - # install needed packages in this runner
+  - # configure the git client on this runner
+  - # set up SSH keys via ssh-agent
+  - # launch the ssh-agent and feed it our key from var $SSH_PRIVATE_KEY
+  - echo "$SSH_KNOWN_HOSTS" >> ~/.ssh/known_hosts
+
+# Jobs run in parallel within the same stage, unless dependencies are expressed
+unit_test1:
+  stage: test
+  script:
+    - # clone our repo locally
+    - # check the app version number and exit != 0 if it's wrong
+
+deploy_prod:
+  stage: deploy
+  # optional. The IP comes from a CI/CD variable
+  environment:
+    name: production
+    url: http://${PRODUCTION_IP}
+  script:
+    - echo "Deploying from ${CI_REPOSITORY_URL}::${CI_COMMIT_REF_NAME}"
+    - # sync "public/" to the target production server via SSH
+  # <condition> to trigger only on a given branch
+```
+
+### Task #3: test operation ###
+
+**Goal:** Test the pipeline by pushing a commit to the main branch.
+
+Verify that:
+  1. The pipeline is correctly triggered on push/commit.
+  1. The app is not deployed if the version number is broken.
diff --git a/public/index.html b/public/index.html
new file mode 100644
index 0000000000000000000000000000000000000000..2b73ac2e64a37317c18202129b6532769b477a0f
--- /dev/null
+++ b/public/index.html
@@ -0,0 +1,36 @@
+<!doctype html>
+<html>
+	<head>
+		<title>Simple CI/CD pipeline with GitLab</title>
+		<meta charset="utf-8" />
+		<style type="text/css">
+		.version {
+			font-family: monospace;
+			font-size: 50px;
+			margin: 100px auto;
+			width: 100%;
+			text-align: center;
+		}
+		.commit {
+			font-family: monospace;
+			font-size: 30px;
+			margin: 100px auto;
+			width: 100%;
+			text-align: center;
+		}
+		</style>
+	</head>
+	<body>
+		<h1>Simple CI/CD pipeline with GitLab</h1>
+		<p>
+			Please increase the version number below before merging. Never
+			touch the 'Id' string--it will be automatically populated by Git
+			checkout.</p>
+		<div class="version">
+			Version: 2.3.5
+		</div>
+		<div class="commit">
+			Commit: $Id: 242e2db4c0bee7e7362e14e371d72189884d0d8a $
+		</div>
+	</body>
+</html>