diff --git a/.env b/.env
new file mode 100644
index 0000000000000000000000000000000000000000..19b4c33ac3ac8aa5fd3835d9d5fa7109ecddd08c
--- /dev/null
+++ b/.env
@@ -0,0 +1 @@
+COMPOSE_PROJECT_NAME=gitlab
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
new file mode 100644
index 0000000000000000000000000000000000000000..23796b52625068807a771bcd515d96cc3905ef3e
--- /dev/null
+++ b/.gitlab-ci.yml
@@ -0,0 +1,46 @@
+---
+image: adarnimrod/ci-images:docker
+
+stages:
+  - test
+  - build
+  - run
+
+pre-commit:
+  stage: test
+  image: adarnimrod/ci-images:pre-commit
+  variables:
+    XDG_CACHE_HOME: "$CI_PROJECT_DIR/.cache"
+    # Disabled until https://github.com/pre-commit/pre-commit/issues/1387 is
+    # resolved.
+    SKIP: "hadolint,docker-compose"
+  script:
+    - pre-commit run --all-files
+  cache:
+    paths:
+      - .cache/
+
+build:
+  stage: build
+  tags: ["host01.shore.co.il"]
+  variables:
+    COMPOSE_DOCKER_CLI_BUILD: "1"
+    DOCKER_BUILDKIT: "1"
+  script:
+    - docker-compose build --no-cache --pull
+    - docker-compose pull --quiet
+
+run:
+  stage: run
+  tags: ["host01.shore.co.il"]
+  when: manual
+  script:
+    - docker-compose up --detach --remove-orphans
+    # yamllint disable rule:line-length
+    - |
+        for i in $(seq 12)
+        do
+            docker container inspect --format '{{ .State.Health.Status }}' $(docker-compose ps -q) | grep -v '^healthy$' || break
+            sleep 10
+        done
+        ! docker container inspect --format '{{ .State.Health.Status }}' $(docker-compose ps -q) | grep -v '^healthy$'
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..7e7b05015b44e90fc6a50fda6f5dc17143615422
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,29 @@
+# vim:ff=unix ts=2 sw=2 ai expandtab
+---
+repos:
+  - repo: https://github.com/pre-commit/pre-commit-hooks
+    rev: v2.3.0
+    hooks:
+      - id: check-added-large-files
+      - id: check-merge-conflict
+      - id: detect-private-key
+      - id: trailing-whitespace
+  - repo: https://github.com/adrienverge/yamllint
+    rev: v1.17.0
+    hooks:
+      - id: yamllint
+  - repo: https://github.com/amperser/proselint/
+    rev: 0.10.2
+    hooks:
+      - id: proselint
+        types: [plain-text]
+        exclude: LICENSE
+  - repo: https://github.com/Yelp/detect-secrets
+    rev: v0.13.0
+    hooks:
+      - id: detect-secrets
+  - repo: https://git.shore.co.il/nimrod/docker-pre-commit.git/
+    rev: v0.3.0
+    hooks:
+      - id: docker-compose
+      - id: hadolint
diff --git a/README.md b/README.md
index 6c2c3fedffd5268a482eeeeecc1d419252214b96..24508395b81d5b8c354ed4ce25245ed152fb557c 100644
--- a/README.md
+++ b/README.md
@@ -7,6 +7,17 @@
 - Docker
 - Docker Compose
 
+## Usage
+
+Although I can deploy through GitLab CI, there's a chicken and egg issue there.
+So, to manually deploy run:
+```
+export DOCKER_HOST=ssh://host01.shore.co.il
+docker-compose build --pull
+docker-compose pull
+docker-compose up -d
+```
+
 ## License
 
 This software is licensed under the MIT license (see `LICENSE.txt`).
diff --git a/crond/.dockerignore b/crond/.dockerignore
new file mode 100644
index 0000000000000000000000000000000000000000..780ece0ba38bebbc9981d893ff31ffc6cb6815e2
--- /dev/null
+++ b/crond/.dockerignore
@@ -0,0 +1,2 @@
+*
+!crontab
diff --git a/crond/Dockerfile b/crond/Dockerfile
new file mode 100644
index 0000000000000000000000000000000000000000..994fbce7b5c680497c7a0744376b361f2404b2a6
--- /dev/null
+++ b/crond/Dockerfile
@@ -0,0 +1,8 @@
+FROM docker:19.03 as docker
+
+# hadolint ignore=DL3006
+FROM adarnimrod/cron as supersonic
+COPY --from=docker /usr/local/bin/docker /usr/local/bin/
+COPY --chown=root:root crontab /crontab
+# hadolint ignore=DL3002
+USER root
diff --git a/crond/README.md b/crond/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..3e077a911f41da99c68be7a271f7bff8ed8f3f59
--- /dev/null
+++ b/crond/README.md
@@ -0,0 +1,21 @@
+# crond
+
+> Cron container image.
+
+## Description
+
+This container periodically runs the GitLab application backup. The command runs
+in the gitlab container so the `dockerd` socket is bind mounted to this
+container and the commands are executed via `docker exec` in the other
+containers.
+
+## License
+
+This software is licensed under the MIT license (see `LICENSE.txt`).
+
+## Author Information
+
+Nimrod Adar, [contact me](mailto:nimrod@shore.co.il) or visit my [website](
+https://www.shore.co.il/). Patches are welcome via [`git send-email`](
+http://git-scm.com/book/en/v2/Git-Commands-Email). The repository is located
+at: <https://www.shore.co.il/git/>.
diff --git a/crond/crontab b/crond/crontab
new file mode 100644
index 0000000000000000000000000000000000000000..a0a40b5e92998bceb174989334057df4f1e72630
--- /dev/null
+++ b/crond/crontab
@@ -0,0 +1,2 @@
+@daily docker exec -t gitlab_gitlab_1 gitlab-backup
+@daily docker exec find /var/backups -mtime +7 -delete
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000000000000000000000000000000000000..7fb4dc279a4dd4d618f1280bdc2ebe92029f3974
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,93 @@
+---
+version: '3.7'
+services:
+  gitlab:
+    image: gitlab/gitlab-ce:13.4.6-ce.0
+    restart: always
+    hostname: gitlab.shore.co.il
+    environment:
+      # yamllint disable rule:line-length
+      GITLAB_OMNIBUS_CONFIG: |
+        #gitlab_rails['initial_root_password'] = "${INITIAL_ROOT_PASSWORD:-qwerty123}"  # pragma: allowlist secret
+        #
+        # Deal with the Nginx web proxy.
+        external_url 'https://git.shore.co.il'
+        gitlab_rails['trusted_proxies'] = ['127.0.0.1/8', "172.16.0.0/12", "192.168.0.0/16"]
+        nginx['listen_port'] = 80
+        nginx['listen_https'] = false
+        nginx['real_ip_trusted_addresses'] = ['127.0.0.1/8', "172.16.0.0/12", "192.168.0.0/16"]
+        nginx['real_ip_header'] = 'X-Forwarded-For'
+        nginx['real_ip_recursive'] = 'on'
+        letsencrypt['enable'] = false
+        #
+        # Backups
+        gitlab_rails['backup_path'] = '/var/backups'
+        #
+        # SSH configuration since we already have SSH running on the host.
+        gitlab_rails['gitlab_ssh_host'] = 'git.shore.co.il'
+        #
+        # Allow bigger uploads
+        nginx['client_max_body_size'] = '250m'
+        #
+        # Mail configuration.
+        #gitlab_rails['smtp_enable'] = true
+        #gitlab_rails['smtp_address'] = "smtp"
+        #gitlab_rails['gitlab_email_from'] = 'noreply@shore.co.il'
+        #gitlab_rails['incoming_email_enabled'] = false
+        #
+        # LDAP configuration.
+        gitlab_rails['ldap_enabled'] = true
+        gitlab_rails['prevent_ldap_sign_in'] = false
+        gitlab_rails['ldap_servers'] = YAML.load <<-'EOS'
+          main: # 'main' is the GitLab 'provider ID' of this LDAP server
+            label: 'LDAP'
+            host: 'ldap'
+            port: 389
+            uid: 'uid'
+            encryption: 'plain'
+            base: 'dc=shore,dc=co,dc=il'
+            allow_username_or_email_login: true
+            user_filer: '(objectclass=inetOrgPerson)'
+        EOS
+        #
+        # Disable monitoring.
+        prometheus['enable'] = false
+        alertmanager['enable'] = false
+        grafana['enable'] = false
+        gitlab_exporter['enable'] = false
+        redis_exporter['enable'] = false
+        postgres_exporter['enable'] = false
+        node_exporter['enable'] = false
+        redis_exporter['enable'] = false
+        mattermost['enable'] = false
+
+    # yamllint disable rule:line-length
+    ports:
+      - '2222:22'
+    volumes:
+      - config:/etc/gitlab
+      - data:/var/opt/gitlab
+      - logs:/var/log/gitlab
+      - backups:/var/backups
+      - _run_slapd:/run/slapd
+
+  crond:
+    build:
+      context: crond/
+    restart: always
+    volumes:
+      - /run/docker.sock:/run/docker.sock
+
+volumes:
+  config:
+  data:
+  logs:
+  backups:
+  _run_slapd:
+    external: true
+    name: run_slapd
+
+networks:
+  default:
+    name: shore
+    external: true