diff --git a/README.md b/README.md
index 76abe89..7ce4db4 100644
--- a/README.md
+++ b/README.md
@@ -74,6 +74,7 @@ A collection of delicious docker recipes.
## Cluster
- [x] ggr
+- [x] ggr-ui
- [x] jsonwire-grid
## Monitor
diff --git a/browserless/docker-stack.yml b/browserless/docker-stack.yml
index 6361ceb..9acfb5a 100644
--- a/browserless/docker-stack.yml
+++ b/browserless/docker-stack.yml
@@ -21,6 +21,16 @@ services:
restart_policy:
condition: on-failure
+ cleanup:
+ image: docker
+ volumes:
+ - /var/run/docker.sock:/var/run/docker.sock
+ command: docker system prune --all --force
+ deploy:
+ mode: global
+ restart_policy:
+ delay: 24h
+
networks:
default:
ipam:
diff --git a/ggr-ui/Dockerfile b/ggr-ui/Dockerfile
new file mode 100644
index 0000000..4d8af6f
--- /dev/null
+++ b/ggr-ui/Dockerfile
@@ -0,0 +1,17 @@
+#
+# Dockerfile for ggr-ui
+#
+
+FROM golang:alpine
+RUN apk add --no-cache git
+RUN go get -v github.com/kelseyhightower/confd
+
+FROM aerokube/ggr-ui:latest-release
+RUN apk add --no-cache curl libxml2-utils supervisor
+COPY --from=0 /go/bin/confd /usr/bin/
+COPY data/confd /etc/confd
+COPY data/supervisor.d /etc/supervisor.d
+COPY data/grid-router /etc/grid-router
+EXPOSE 8888
+ENTRYPOINT ["supervisord", "-n", "-c", "/etc/supervisord.conf"]
+HEALTHCHECK CMD ["curl", "-f", "http://127.0.0.1:8888/ping"]
diff --git a/ggr-ui/README.md b/ggr-ui/README.md
new file mode 100644
index 0000000..a8c96e2
--- /dev/null
+++ b/ggr-ui/README.md
@@ -0,0 +1,23 @@
+ggr-ui
+======
+
+[Ggr UI][1] is a standalone daemon that automatically collects `/status` information
+from multiple Selenoid instances and returns it as a single `/status` API. When
+this daemon is running you can use Selenoid UI to see the state of the entire
+cluster.
+
+```bash
+$ docker-compose up -d
+$ docker run --rm -it alpine sh
+>>> apk update
+>>> apk add bind-tools
+>>> dig tasks.selenoid
+```
+
+```bash
+$ curl http://127.0.0.1:8888/ping
+$ curl http://127.0.0.1:8888/status
+```
+
+[1]: https://github.com/aerokube/ggr-ui
+
diff --git a/ggr-ui/data/confd/conf.d/ggr-ui.toml b/ggr-ui/data/confd/conf.d/ggr-ui.toml
new file mode 100644
index 0000000..990b5be
--- /dev/null
+++ b/ggr-ui/data/confd/conf.d/ggr-ui.toml
@@ -0,0 +1,6 @@
+[template]
+src = "guest.xml.tmpl"
+dest = "/etc/grid-router/quota/guest.xml"
+keys = ["/"]
+check_cmd = "xmllint --noout /etc/grid-router/quota/guest.xml"
+reload_cmd = "supervisorctl signal HUP ggr-ui"
diff --git a/ggr-ui/data/confd/templates/guest.xml.tmpl b/ggr-ui/data/confd/templates/guest.xml.tmpl
new file mode 100644
index 0000000..791a958
--- /dev/null
+++ b/ggr-ui/data/confd/templates/guest.xml.tmpl
@@ -0,0 +1,11 @@
+
+
+
+
+ {{range lookupIP (getenv "BROWSER_HOSTS" "tasks.selenoid")}}
+
+ {{end}}
+
+
+
+
diff --git a/ggr-ui/data/grid-router/quota/guest.xml b/ggr-ui/data/grid-router/quota/guest.xml
new file mode 100644
index 0000000..3f4c4ac
--- /dev/null
+++ b/ggr-ui/data/grid-router/quota/guest.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/ggr-ui/data/supervisor.d/confd.ini b/ggr-ui/data/supervisor.d/confd.ini
new file mode 100644
index 0000000..5d9b1f7
--- /dev/null
+++ b/ggr-ui/data/supervisor.d/confd.ini
@@ -0,0 +1,7 @@
+[program:confd]
+command = confd -watch -backend file -file /dev/null
+redirect_stderr = true
+
+[program:null]
+command = sh -c 'while true; do date > /dev/null; sleep 60; done'
+redirect_stderr = true
diff --git a/ggr-ui/data/supervisor.d/ggr-ui.ini b/ggr-ui/data/supervisor.d/ggr-ui.ini
new file mode 100644
index 0000000..187ad87
--- /dev/null
+++ b/ggr-ui/data/supervisor.d/ggr-ui.ini
@@ -0,0 +1,3 @@
+[program:ggr-ui]
+command = ggr-ui -listen 8888 -limit 10 -quota-dir /etc/grid-router/quota
+redirect_stderr = true
diff --git a/ggr-ui/docker-compose.yml b/ggr-ui/docker-compose.yml
new file mode 100644
index 0000000..190ce0a
--- /dev/null
+++ b/ggr-ui/docker-compose.yml
@@ -0,0 +1,28 @@
+version: "3.7"
+
+services:
+
+ ggr-ui:
+ image: vimagick/ggr-ui
+ ports:
+ - "8888:8888"
+ environment:
+ - BROWSER_NAME=chrome
+ - BROWSER_VERSION=78.0
+ - BROWSER_REGION=1
+ - BROWSER_HOSTS=tasks.selenoid
+ - BROWSER_PORT=4444
+ - BROWSER_COUNT=10
+ extra_hosts:
+ - tasks.selenoid:1.2.3.4
+ - tasks.selenoid:4.3.2.1
+ restart: unless-stopped
+
+ selenoid-ui:
+ image: aerokube/selenoid-ui:latest-release
+ command: --selenoid-uri=http://ggr-ui:8888
+ ports:
+ - "8080:8080"
+ depends_on:
+ - ggr-ui
+ restart: unless-stopped
diff --git a/ggr/README.md b/ggr/README.md
index f9cb2e3..06124b9 100644
--- a/ggr/README.md
+++ b/ggr/README.md
@@ -4,6 +4,14 @@ ggr
Go Grid Router (aka [Ggr][1]) is a lightweight active load balancer used to
create scalable and highly-available Selenium clusters.
+```bash
+$ docker stack deploy -c docker-stack.yml ggr
+$ docker run --rm -it alpine sh
+>>> apk update
+>>> apk add bind-tools
+>>> dig tasks.chrome
+```
+
```bash
$ curl http://127.0.0.1:4444/ping
{
diff --git a/ggr/docker-stack.yml b/ggr/docker-stack.yml
index ee27cc3..a86680a 100644
--- a/ggr/docker-stack.yml
+++ b/ggr/docker-stack.yml
@@ -41,3 +41,8 @@ services:
parallelism: 5
delay: 10s
order: stop-first
+
+networks:
+ default:
+ driver: overlay
+ attachable: true
diff --git a/selenoid/docker-stack.yml b/selenoid/docker-stack.yml
new file mode 100644
index 0000000..c4e20f9
--- /dev/null
+++ b/selenoid/docker-stack.yml
@@ -0,0 +1,75 @@
+version: "3.7"
+
+services:
+
+ selenoid:
+ image: aerokube/selenoid:latest-release
+ command: >
+ -conf config/browsers.json
+ -video-output-dir video
+ -log-output-dir logs
+ -limit 10
+ -timeout 5m
+ -max-timeout 1h
+ -container-network selenoid_default
+ ports:
+ - "4444:4444"
+ volumes:
+ - /var/run/docker.sock:/var/run/docker.sock
+ - selenoid_data:/opt/selenoid
+ environment:
+ - OVERRIDE_VIDEO_OUTPUT_DIR=/mnt/selenoid/video
+ deploy:
+ replicas: 0
+ placement:
+ constraints:
+ - node.role == worker
+ restart_policy:
+ condition: on-failure
+
+ ggr-ui:
+ image: vimagick/ggr-ui
+ ports:
+ - "8888:8888"
+ environment:
+ - BROWSER_NAME=chrome
+ - BROWSER_VERSION=78.0
+ - BROWSER_REGION=1
+ - BROWSER_HOSTS=tasks.selenoid
+ - BROWSER_PORT=4444
+ - BROWSER_COUNT=10
+ deploy:
+ replicas: 1
+ placement:
+ constraints:
+ - node.role == manager
+ restart_policy:
+ condition: on-failure
+
+ selenoid-ui:
+ image: aerokube/selenoid-ui:latest-release
+ command: --selenoid-uri=http://ggr-ui:8888
+ ports:
+ - "8080:8080"
+ depends_on:
+ - ggr-ui
+ deploy:
+ replicas: 1
+ placement:
+ constraints:
+ - node.role == manager
+ restart_policy:
+ condition: on-failure
+
+volumes:
+ selenoid_data:
+ driver: local
+ driver_opts:
+ type: nfs
+ o: "addr=10.0.0.96,nolock,soft,ro"
+ device: ":/export/selenoid"
+
+networks:
+ default:
+ driver: overlay
+ attachable: true
diff --git a/splash/docker-stack.yml b/splash/docker-stack.yml
index 14c413b..9dffb45 100644
--- a/splash/docker-stack.yml
+++ b/splash/docker-stack.yml
@@ -1,7 +1,8 @@
-version: '3.5'
+version: '3.7'
+
services:
splash:
- image: scrapinghub/splash:3.3.1
+ image: scrapinghub/splash:3.4
command: --maxrss 2048 --max-timeout 300 --disable-lua-sandbox --verbosity 1
ports:
- "8050:8050"
@@ -20,6 +21,7 @@ services:
- node.role == worker
restart_policy:
condition: on-failure
+
volumes:
splash_filters:
driver: local
@@ -45,6 +47,7 @@ volumes:
type: nfs
o: "addr=10.0.0.96,nolock,soft,ro"
device: ":/export/splash/proxy-profiles"
+
networks:
default:
ipam: