diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 0b3abd5876540ff17f8edaa07c778368121cebeb..43f787a207e221728a418bee0cd462a90a143766 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,20 +1,62 @@
 stages:
   - test
   - build
+  - deploy
+
 test:
   stage: test
   image: node:15-buster-slim
   cache:
-    key: "pnpm-cache"
+    key: "pnpm-store"
     paths:
-      - "$CI_PROJECT_DIR/.pnpm-store"
+      - ".pnpm-store"
   before_script:
-    - npm install -g pnpm 
-    - pnpm install 
-    - pnpm install -g js-yaml jest
+    - npm install --global pnpm
+    - pnpm config set store-dir .pnpm-store
+    - pnpm install
   script:
-    - jest src/ --coverage --coverageReporters cobertura
+    - pnpx jest src/ --coverage --coverageReporters cobertura
   coverage: /All\sfiles.*?\s+(\d+.\d+)/
   artifacts:
     reports:
       cobertura: "$CI_PROJECT_DIR/coverage/cobertura-coverage.xml"
+
+build:
+  stage: build
+  image: node:15-buster-slim
+  cache:
+    key: "pnpm-store"
+    paths:
+      - ".pnpm-store"
+  before_script:
+    - npm install --global pnpm
+    - pnpm config set store-dir .pnpm-store
+    - pnpm install
+  script:
+    - npm run build
+  artifacts:
+    paths:
+      - build
+
+# Deploy main branch to contact.greenhost.net
+deploy-staging:
+  stage: deploy
+  # Only run this job for the main branch.
+  rules:
+    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
+  image: debian:10
+  environment:
+    name: staging
+    url: https://contact.greenhost.net/staging
+  variables:
+    FTP_USER: 'webmaster_greenhost_nl'
+    FTP_HOST: 'ftp.greenhost.nl'
+    DOMAIN_NAME: 'greenhost.net'
+    SUBDOMAIN: 'contact'
+    SUBFOLDER: '/staging'
+    BUILD_FOLDER: './build'
+  before_script:
+    - apt-get update
+    - apt-get install -y rsync openssh-client
+  script:
+    - deploy/deploy.sh
diff --git a/deploy/deploy.sh b/deploy/deploy.sh
new file mode 100755
index 0000000000000000000000000000000000000000..849545612144098d8b10d763a7c9e17874f242b7
--- /dev/null
+++ b/deploy/deploy.sh
@@ -0,0 +1,19 @@
+#!/bin/bash
+
+set -o errexit
+set -euvo pipefail
+
+eval $(ssh-agent -s)
+# Register ssh key with ssh-agent.
+ssh-add - <<<"$SSH_KEY"
+
+# Add server's ssh host key to known hosts.
+mkdir -p ~/.ssh
+[[ -f /.dockerenv ]] && printf "%s" "$SSH_SERVER_HOSTKEYS" > ~/.ssh/known_hosts
+
+sshHost="${FTP_USER}@${FTP_HOST}"
+targetDir='${HOME}'"${DOMAIN_NAME}/${SUBDOMAIN}${SUBFOLDER}"
+rsyncTarget="${sshHost}:${targetDir}"
+
+# Upload site.
+rsync -HAXa -vi --delete ${BUILD_FOLDER}/ "${rsyncTarget}"
diff --git a/public/index.html b/public/index.html
index 08db1bbaf318134ae9eb457f3a36bb988c716c49..7419aad3eeb3e2eb2f8664c18d220827581aab39 100644
--- a/public/index.html
+++ b/public/index.html
@@ -8,6 +8,10 @@
     <title>Snowpack App</title>
   </head>
   <body>
+    <div style="border: 2pt solid red; font-size: 200%; padding: 5pt;">Warning: this is a development version of
+      this contact form, and any messages sent using it will be ignored! Please
+      visit <a href="https://greenhost.net/contact">our live contact form</a>
+      for actual support.</div>
     <div lang="en" id="helpful-contact-form" style="width: 40rem; margin:10rem auto 0 auto"></div>
     <noscript>You need to enable JavaScript to run this app.</noscript>
     <script type="module" src="/dist/index.js"></script>