diff --git a/ci/gitlab-ci.yml b/ci/gitlab-ci.yml
new file mode 100644
index 0000000000000000000000000000000000000000..25f2dcc7a8a8998322bf9a94fdeaefcd5dcba9d2
--- /dev/null
+++ b/ci/gitlab-ci.yml
@@ -0,0 +1,49 @@
+.pre-deploy-uber-space-setup: &pre-deploy-uber-space-setup
+  - apk add rsync openssh-client
+  - eval $(ssh-agent -s)
+  - echo "$CI_DEPLOYMENT_PRIVATE_KEY" | tr -d '\r' | ssh-add -
+  - mkdir --mode 700 -p ~/.ssh
+  - echo "$UBERSPACE_KNOWN_HOST" >> ~/.ssh/known_hosts
+
+cache:
+  paths:
+    - node_modules/
+
+stages:
+  - test
+  - build
+  - deploy
+
+trivy:filesystem:
+  stage: test
+  image:
+    name: $DOCKER_PULL_REGISTRY/aquasec/trivy:latest
+    entrypoint: [""]
+  script:
+    - trivy filesystem --security-checks vuln,config --severity HIGH,CRITICAL --exit-code 1 .
+    - trivy filesystem --security-checks vuln,config --severity UNKNOWN,LOW,MEDIUM .
+
+build:
+  stage: build
+  image: node:lts-alpine
+  before_script:
+    - yarn install
+  script:
+    - yarn export
+  artifacts:
+    paths:
+      - out/
+    expire_in: 1 hour
+
+deploy:
+  stage: deploy
+  image:
+    name: alpine:latest
+  dependencies:
+    - build
+  rules:
+    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
+  before_script:
+    - *pre-deploy-uber-space-setup
+  script:
+    - rsync -rLvz --delete --checksum -e "ssh -o CheckHostIP=no" --ipv4 --progress ./out/. fitko@dorado.uberspace.de:docs-preview.fitko.dev
diff --git a/package.json b/package.json
index 690954bf5cac0b8007e4ba59ff5b28a358e770cc..6c9d6f6cd01513ff7682af9d4c1b0a59f8e09e3f 100644
--- a/package.json
+++ b/package.json
@@ -8,7 +8,8 @@
     "build": "next build",
     "start": "next start",
     "lint": "next lint",
-    "preview": "next build && next start"
+    "preview": "next build && next start",
+    "export": "next build && next export"
   },
   "dependencies": {
     "@contentful/rich-text-html-renderer": "^15.11.1",