diff --git a/.github/workflows/build-publish.yaml b/.github/workflows/build-publish.yaml
new file mode 100644
index 00000000..c6862a5c
--- /dev/null
+++ b/.github/workflows/build-publish.yaml
@@ -0,0 +1,63 @@
+name: Build and publish image
+on:
+  push:
+    tags:
+      - "*"
+  pull_request:
+    branches:
+      - main
+
+env:
+  REGISTRY: ghcr.io
+
+
+jobs:
+  build-and-publish-image:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v3
+        with:
+          fetch-depth: 0
+          submodules: recursive
+      - name: build-web
+        run: |
+          mkdir -p `pwd`/.tmp
+          make build-web UI_FILE=`pwd`/.tmp/ui.tar.gz
+      - uses: docker/setup-qemu-action@v2
+      - uses: docker/setup-buildx-action@v2
+      - uses: docker/login-action@v2
+        with:
+          registry: ${{ env.REGISTRY }}
+          username: ${{ github.actor }}
+          password: ${{ secrets.GITHUB_TOKEN }}
+      - name: Extract metadata (tags, labels) for docker image
+        id: meta
+        uses: docker/metadata-action@v4
+        with:
+          images: ${{ env.REGISTRY }}/${{ github.repository }}
+          tags: |
+            type=raw,enable={{is_default_branch}},value=vnext
+            type=sha,enable={{is_default_branch}},prefix=vnext-,format=short
+            type=ref,event=pr,prefix=vnext-pr,suffix=-{{sha}}
+            type=ref,event=pr,prefix=vnext-pr
+            type=ref,enable={{is_default_branch}},event=branch
+            type=semver,pattern={{version}}
+            type=semver,pattern={{major}}.{{minor}}
+            type=semver,pattern={{major}}
+      - name: Build and publish image
+        uses: docker/build-push-action@v3
+        with:
+          context: .
+          file: ./docker/Dockerfile
+          platforms: linux/amd64,linux/arm64
+          builder: ${{ steps.buildx.outputs.name }}
+          build-args: |
+            UI_FILE=.tmp/ui.tar.gz
+          push: true
+          tags: ${{ steps.meta.outputs.tags }}
+          labels: ${{ steps.meta.outputs.labels }}
+      - name: Clean up
+        if: ${{ always() }}
+        run: |
+          shopt -s dotglob
+          sudo rm -r *
\ No newline at end of file
diff --git a/.github/workflows/buildTestBinaries.yml b/.github/workflows/buildTestBinaries.yml
index c10eccd6..71b7451a 100644
--- a/.github/workflows/buildTestBinaries.yml
+++ b/.github/workflows/buildTestBinaries.yml
@@ -27,11 +27,12 @@ jobs:
         with:
           go-version: "^1.18" # The Go version to download (if necessary) and use.
 
-      - name: Set version
+      - name: Set version and ui_file
         id: vars
         run: |
           git tag $(git describe --tags --abbrev=0)-$(git rev-parse --short HEAD)
           echo "::set-output name=version::$(git describe --tags --abbrev=0)"
+          echo "::set-output name=ui_file::$(pwd)/.tmp/ui.tar.gz"
 
       - name: Build client
         uses: goreleaser/goreleaser-action@v3
@@ -41,6 +42,7 @@ jobs:
           args: release --rm-dist --skip-publish --skip-announce
         env:
           UI_SEPARATOR: "--------UI--------"
+          UI_FILE: ${{ steps.vars.outputs.ui_file }}
 
       - name: Upload assets
         uses: actions/upload-artifact@v3
diff --git a/.github/workflows/ghcr-cleanup.yaml b/.github/workflows/ghcr-cleanup.yaml
new file mode 100644
index 00000000..d65d0f79
--- /dev/null
+++ b/.github/workflows/ghcr-cleanup.yaml
@@ -0,0 +1,37 @@
+name: Delete old ghcr images
+on:
+  schedule:
+    - cron: "15 1 * * *"  # every day at 1:15am
+  pull_request:
+    types: [closed]
+
+jobs:
+  pull-request-ghcr-cleanup:
+    if: ${{ github.event_name == 'pull_request' }}
+    name: Delete images related to closed PR
+    runs-on: ubuntu-latest
+    steps:
+      - name: Delete images related to closed PR
+        uses: snok/container-retention-policy@v1
+        with:
+          image-names: client-application
+          cut-off: now UTC
+          account-type: org
+          org-name: plgd-dev
+          filter-tags: vnext-pr${{ github.event.pull_request.number }}*
+          token: ${{ secrets.GHCR_CLEANUP_PAT }}
+  nightly-ghcr-cleanup:
+    if: ${{ github.event_name == 'schedule' }}
+    name: Delete old vnext images
+    runs-on: ubuntu-latest
+    steps:
+      - name: Delete older than a month vnext images
+        uses: snok/container-retention-policy@v1
+        with:
+          image-names: client-application
+          cut-off: One month ago UTC
+          account-type: org
+          org-name: plgd-dev
+          filter-tags: vnext-*
+          skip-tags: vnext-pr*, main
+          token: ${{ secrets.GHCR_CLEANUP_PAT }}
\ No newline at end of file
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index eec2675a..295c3fb1 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -29,6 +29,11 @@ jobs:
         uses: actions/setup-go@v2
         with:
           go-version: 1.18
+    
+      - name: Set ui_file
+        id: vars
+        run: |
+          echo "::set-output name=ui_file::$(pwd)/.tmp/ui.tar.gz"
       -
         name: Run GoReleaser
         uses: goreleaser/goreleaser-action@v2
@@ -40,5 +45,6 @@ jobs:
         env:
           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
           UI_SEPARATOR: "--------UI--------"
+          UI_FILE: ${{ steps.vars.outputs.ui_file }}
           # Your GoReleaser Pro key, if you are using the 'goreleaser-pro' distribution
           # GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }}
\ No newline at end of file
diff --git a/.goreleaser.yaml b/.goreleaser.yaml
index 49212654..76e60d13 100644
--- a/.goreleaser.yaml
+++ b/.goreleaser.yaml
@@ -2,7 +2,7 @@
 # Make sure to check the documentation at https://goreleaser.com
 before:
   hooks:
-   - make build-web
+   - bash -c "test -f {{.Env.UI_FILE}} || make build-web UI_FILE={{.Env.UI_FILE}}"
 dist: .tmp/dist
 builds:
   - binary: client-application
@@ -33,7 +33,7 @@ builds:
     hooks:
       post:
         - bash -c "test {{.Os}} == "windows" || test {{.Os}} == "darwin" || upx --best --lzma {{.Path}}"
-        - make inject-web CLIENT_APPLICATION_BINARY_PATH={{.Path}} UI_SEPARATOR={{.Env.UI_SEPARATOR}}
+        - make inject-web CLIENT_APPLICATION_BINARY_PATH={{.Path}} UI_SEPARATOR={{.Env.UI_SEPARATOR}} UI_FILE={{.Env.UI_FILE}}
 archives:
   - replacements:
       darwin: macOS
diff --git a/Makefile b/Makefile
index 8531e4d9..b1930e35 100644
--- a/Makefile
+++ b/Makefile
@@ -10,6 +10,7 @@ WWW_PATH=$(TMP_PATH)/www
 CLIENT_APPLICATION_VERSION_PATH_VARIABLE = main.Version
 CLIENT_APPLICATION_UI_SEPARATOR_PATH_VARIABLE = main.UISeparator
 CLIENT_APPLICATION_BINARY_PATH ?= 
+UI_FILE ?= $(TMP_PATH)/ui.tar.gz
 
 CERT_TOOL_IMAGE ?= ghcr.io/plgd-dev/hub/cert-tool:vnext
 DEVSIM_IMAGE ?= ghcr.io/iotivity/iotivity-lite/cloud-server-discovery-resource-observable-debug:master
@@ -93,17 +94,17 @@ build-web:
 	mkdir -p $(WWW_PATH)
 	docker build --tag build-web:latest --target build-web -f ./web/Dockerfile ./web
 	CONTAINER_ID=`docker create build-web:latest` && docker cp $$CONTAINER_ID:/web/build/ $(WWW_PATH)/ && docker rm -f $$CONTAINER_ID
-	cd $(WWW_PATH)/build && tar -czf $(TMP_PATH)/ui.tar.gz .
+	cd $(WWW_PATH)/build && tar -czf $(UI_FILE) .
 .PHONY: build-web
 
 inject-web: $(CLIENT_APPLICATION_BINARY_PATH)
 	printf "\n$(UI_SEPARATOR)\n" >> $(CLIENT_APPLICATION_BINARY_PATH)
 	wc -c $(CLIENT_APPLICATION_BINARY_PATH)
-	cat $(TMP_PATH)/ui.tar.gz >> $(CLIENT_APPLICATION_BINARY_PATH)
+	cat $(UI_FILE) >> $(CLIENT_APPLICATION_BINARY_PATH)
 .PHONY: inject-web
 
-build: clean
-	UI_SEPARATOR=$(UI_SEPARATOR) goreleaser build --rm-dist
+build:
+	UI_FILE=$(UI_FILE) UI_SEPARATOR=$(UI_SEPARATOR) goreleaser build --rm-dist --single-target --skip-validate
 .PHONY: build
 
 test: env
diff --git a/README.md b/README.md
index 52a51027..b55bc5f4 100644
--- a/README.md
+++ b/README.md
@@ -32,6 +32,16 @@ git tag -f v0.0.1-myversion
 make build
 ```
 
+## Docker
+
+You can run the client application in a docker container by executing the following command.
+
+```sh
+docker run --rm -it -e NUM_DEVICES=1 -p 8080:8080 ghcr.io/plgd-dev/client-application:latest
+```
+
+Via environment variable NUM_DEVICES you can specify the number of devices simulators. The default value is 1. The client application will be available at [http://locahost:8080](http://locahost:8080).
+
 ## YAML Configuration
 
 A configuration template is available on [config.yaml](./config.yaml).
diff --git a/docker/Dockerfile b/docker/Dockerfile
new file mode 100644
index 00000000..73086f07
--- /dev/null
+++ b/docker/Dockerfile
@@ -0,0 +1,16 @@
+FROM goreleaser/goreleaser AS builder
+ARG UI_FILE
+
+RUN apk add upx
+WORKDIR /go/src/github.com/plgd-dev/client-application
+COPY go.mod go.sum ./
+COPY ${UI_FILE} ${UI_FILE}
+COPY . .
+RUN make build BUILD_UI=false UI_FILE=${UI_FILE}
+
+FROM ghcr.io/iotivity/iotivity-lite/cloud-server-discovery-resource-observable:latest AS service
+COPY ./docker/run.sh /usr/local/bin/run.sh
+COPY --from=builder /go/src/github.com/plgd-dev/client-application/.tmp/dist/*/client-application /usr/local/bin/client-application
+RUN mkdir -p /plgd
+WORKDIR /plgd
+ENTRYPOINT [ "/usr/local/bin/run.sh" ]
\ No newline at end of file
diff --git a/docker/run.sh b/docker/run.sh
new file mode 100755
index 00000000..e664cd0d
--- /dev/null
+++ b/docker/run.sh
@@ -0,0 +1,41 @@
+#!/usr/bin/env bash
+set -e
+
+PREFIX_EXEC=""
+echo Spawning $NUM_DEVICES devices
+
+umask 0000
+pids=()
+for ((i=0;i<$NUM_DEVICES;i++)); do
+    LD_PRELOAD=/usr/local/lib/faketime/libfaketimeMT.so.1 /iotivity-lite/port/linux/service "device-$i" > /tmp/$i.log 2>&1 &
+    pids+=($!)
+done
+
+/usr/local/bin/client-application --config /plgd/config.yaml $@ > /tmp/client-application.log 2>&1 &
+pids+=($!)
+
+terminate()
+{
+    echo "Terminate"
+    for (( i=0; i<${#pids[@]}; i++ )); do
+        kill -SIGTERM ${pids[$i]}
+    done
+}
+
+trap terminate SIGTERM
+
+# Naive check runs checks once a minute to see if either of the processes exited.
+# This illustrates part of the heavy lifting you need to do if you want to run
+# more than one service in a container. The container exits with an error
+# if it detects that either of the processes has exited.
+# Otherwise it loops forever, waking up every 60 seconds
+while sleep 10; do
+for (( i=0; i<${#pids[@]}; i++ ));
+do
+    if ! kill -0 ${pids[$i]} 2>/dev/null; then
+        echo "service[$i] with pid=${pids[$i]} is dead"
+        exit 1
+    fi
+done
+echo checking running devices
+done
\ No newline at end of file
diff --git a/tools/validate/validateYaml.py b/tools/validate/validateYaml.py
index ac7802dd..b98d7883 100755
--- a/tools/validate/validateYaml.py
+++ b/tools/validate/validateYaml.py
@@ -74,7 +74,7 @@ def find_and_validate_yaml_files(dir = ROOT_DIRECTORY):
   """Find all yaml files in directory and validate them."""
 
   valid = True
-  exclude_dirs = set(["dependency", "templates"])
+  exclude_dirs = set(["dependency", "templates", ".github"])
   exclude_filenames = set(["swagger.yaml"])
   for root, dirnames, filenames in os.walk(dir, topdown=True):
     dirnames[:] = [d for d in dirnames if d not in exclude_dirs]
