diff --git a/Dockerfile b/Dockerfile
index 65cb8fda90ccd176b2158bbb53dfa87273cc8712..f7281415451a14024da0b3d01d483fb499321b85 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,9 +1,9 @@
-FROM debian:stable AS build
+FROM debian:stable-slim AS build
 RUN apt-get -q update && env DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends git ca-certificates
 RUN git clone --depth=1 https://git.schokokeks.org/git/freewvs.git /src
 
 FROM debian:stable-slim
-RUN apt-get -q update && env DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends python3 python3-pip python3-requests xxd ca-certificates && ln -s python3 /usr/bin/python && pip3 install backoff
+RUN apt-get -q update && env DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends python3 python3-pip python3-requests xxd && ln -s python3 /usr/bin/python && pip3 install backoff && rm -fr /var/lib/apt/lists/*
 COPY --from=build /src/freewvs /usr/bin/freewvs
 COPY --from=build /src/freewvsdb/ /var/lib/freewvs
 COPY --from=build /src/update-freewvsdb /usr/bin/update-freewvsdb
diff --git a/README.md b/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..84972e9b272bf4485e9bc048133b8235a279a361
--- /dev/null
+++ b/README.md
@@ -0,0 +1,5 @@
+Container image to run *freewvs* periodically and upload the results
+to the aux-db.
+
+Since we have to resolve resource IDs (website names) from the
+filesystem paths, we need access to the homedirs.json file.
diff --git a/cron.sh b/cron.sh
index 7586bc2bbc44c0afad2c52fc59e87afd34f1a717..53c38dca535e6cb0d8cb28cbf2c236ae2e184b39 100755
--- a/cron.sh
+++ b/cron.sh
@@ -19,7 +19,7 @@ sleep $offset_secs
 
 while true; do
     echo "scanning..."
-    ionice -c 3 freewvs --all --xml $scan_dir \
+    nice ionice -c 3 freewvs --all --xml $scan_dir \
         | /upload.py $UPLOAD_ARGS
     if [ $? -gt 0 ]; then
         echo "ERROR: freewvs exited with status $?"
diff --git a/sample.xml b/sample.xml
new file mode 100644
index 0000000000000000000000000000000000000000..bb57b684bd631572a7cf57fd313f74f3273b3619
--- /dev/null
+++ b/sample.xml
@@ -0,0 +1,91 @@
+<?xml version="1.0" ?>
+<freewvs>
+  <app state="ok">
+    <appname>Wordpress-Akismet</appname>
+    <version>3.3.2</version>
+    <directory>/home/ale/src/ai/noblogs-docker-2/noblogs/wp-content/plugins/akismet</directory>
+  </app>
+  <app state="vulnerable">
+    <appname>WP Super Cache</appname>
+    <version>1.2</version>
+    <directory>/home/ale/src/ai/noblogs-docker-2/noblogs/wp-content/plugins/wp-super-memcache</directory>
+    <safeversion>1.6.9</safeversion>
+    <vulninfo>https://odd.blog/2019/07/25/wp-super-cache-1-6-9-security-update/</vulninfo>
+  </app>
+  <app state="vulnerable">
+    <appname>WP Super Cache</appname>
+    <version>1.4.9</version>
+    <directory>/home/ale/src/ai/noblogs-docker-2/noblogs/wp-content/plugins/wp-super-cache</directory>
+    <safeversion>1.6.9</safeversion>
+    <vulninfo>https://odd.blog/2019/07/25/wp-super-cache-1-6-9-security-update/</vulninfo>
+  </app>
+  <app state="vulnerable">
+    <appname>Wordpress-NextGEN</appname>
+    <version>2.2.3</version>
+    <directory>/home/ale/src/ai/noblogs-docker-2/noblogs/wp-content/plugins/nextgen-gallery</directory>
+    <safeversion>3.2.11</safeversion>
+    <vulninfo>CVE-2019-14314</vulninfo>
+  </app>
+  <app state="ok">
+    <appname>Wordpress-Theme-Twentyfifteen</appname>
+    <version>1.8</version>
+    <directory>/home/ale/src/ai/noblogs-docker-2/noblogs/wp-content/themes/twentyfifteen</directory>
+  </app>
+  <app state="vulnerable">
+    <appname>Wordpress</appname>
+    <version>4.8</version>
+    <directory>/home/ale/src/ai/noblogs-docker-2/noblogs</directory>
+    <safeversion>4.8.11</safeversion>
+    <vulninfo>CVE-2019-17672</vulninfo>
+  </app>
+  <app state="vulnerable">
+    <appname>WP Super Cache</appname>
+    <version>1.6.4</version>
+    <directory>/home/ale/src/ai/noblogs-wp/wp-content/plugins/wp-super-cache</directory>
+    <safeversion>1.6.9</safeversion>
+    <vulninfo>https://odd.blog/2019/07/25/wp-super-cache-1-6-9-security-update/</vulninfo>
+  </app>
+  <app state="vulnerable">
+    <appname>Wordpress-NextGEN</appname>
+    <version>3.1.6</version>
+    <directory>/home/ale/src/ai/noblogs-wp/wp-content/plugins/nextgen-gallery</directory>
+    <safeversion>3.2.11</safeversion>
+    <vulninfo>CVE-2019-14314</vulninfo>
+  </app>
+  <app state="ok">
+    <appname>Wordpress-Theme-Twentyfifteen</appname>
+    <version>2.3</version>
+    <directory>/home/ale/src/ai/noblogs-wp/wp-content/themes/twentyfifteen</directory>
+  </app>
+  <app state="vulnerable">
+    <appname>Wordpress</appname>
+    <version>5.1</version>
+    <directory>/home/ale/src/ai/noblogs-wp</directory>
+    <safeversion>5.1.3</safeversion>
+    <vulninfo>CVE-2019-17672</vulninfo>
+  </app>
+  <app state="ok">
+    <appname>WP Super Cache</appname>
+    <version>1.6.9</version>
+    <directory>/home/ale/src/ai3/docker/noblogs/noblogs/wp-content/plugins/wp-super-cache</directory>
+  </app>
+  <app state="vulnerable">
+    <appname>Wordpress-NextGEN</appname>
+    <version>3.2.10</version>
+    <directory>/home/ale/src/ai3/docker/noblogs/noblogs/wp-content/plugins/nextgen-gallery</directory>
+    <safeversion>3.2.11</safeversion>
+    <vulninfo>CVE-2019-14314</vulninfo>
+  </app>
+  <app state="ok">
+    <appname>Wordpress-Theme-Twentyfifteen</appname>
+    <version>2.5</version>
+    <directory>/home/ale/src/ai3/docker/noblogs/noblogs/wp-content/themes/twentyfifteen</directory>
+  </app>
+  <app state="vulnerable">
+    <appname>Wordpress</appname>
+    <version>5.2.3</version>
+    <directory>/home/ale/src/ai3/docker/noblogs/noblogs</directory>
+    <safeversion>5.2.4</safeversion>
+    <vulninfo>CVE-2019-17672</vulninfo>
+  </app>
+</freewvs>
diff --git a/upload.py b/upload.py
index eceebf837e4b282988b3316f7e0222ecfd30e031..ca865cd706efdd35391cd133bf39a7d8b950151b 100755
--- a/upload.py
+++ b/upload.py
@@ -2,13 +2,29 @@
 
 from xml.etree import ElementTree
 import argparse
-import backoff
 import json
 import logging
+import os
 import sys
-import requests
-import urllib
 import time
+import requests
+
+
+DATA_TYPE = 'cms_info'
+DATA_TTL = 3 * 86400
+
+
+class HomedirMap():
+
+    def __init__(self, data):
+        self._data = data
+
+    def get_resource_id(self, key):
+        while key not in ('', '/'):
+            if key in self._data:
+                return self._data[key]
+            key = os.path.split(key)[0]
+        return None
 
 
 class StatusError(Exception):
@@ -18,11 +34,7 @@ class StatusError(Exception):
             resp.request.method, resp.request.url, resp.status_code, resp.text))
 
 
-class RetriableStatusError(StatusError):
-    pass
-
-
-class Backend(object):
+class Backend():
 
     def __init__(self, url, ssl_cert=None, ssl_key=None,
                  ssl_ca=None, timeout=60):
@@ -33,11 +45,6 @@ class Backend(object):
             self._session.verify = ssl_ca
         self._session.timeout = timeout
 
-    @backoff.on_exception(backoff.expo,
-                          (requests.exceptions.Timeout,
-                           requests.exceptions.ConnectionError,
-                           RetriableStatusError),
-                          max_time=1800)
     def submit(self, data):
         req = self._session.prepare_request(
             requests.Request(
@@ -46,21 +53,37 @@ class Backend(object):
                 json=data))
         resp = self._session.send(req)
         status = resp.status_code
-        if status == 429 or status > 500:
-            raise RetriableStatusError(resp)
+        if status != 200:
+            raise StatusError(resp)
         return resp
 
 
-def read_input(fd, shard):
-    timestamp = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())
+def parse_xml_app_entry(app):
+    # Make a dictionary with all the tags.
+    data = dict((x.tag, x.text) for x in app.iter())
+    # State is an attribute on the app tag.
+    data['state'] = app.attrib.get('state')
+    return data
+
+
+def read_input(fd, homedir_map):
     tree = ElementTree.parse(fd)
     entries = []
     for app in tree.iter('app'):
-        entry = dict((x.tag, x.text) for x in app.iter())
-        entry['state'] = app.attrib.get('state')
-        entry['shard'] = shard
-        entry['timestamp'] = timestamp
-        entries.append(entry)
+        entry = parse_xml_app_entry(app)
+
+        # Take the 'homedir' attr out of the entry values and make it
+        # the aux-db secondary key.
+        homedir = entry.pop('directory')
+        resource_id = homedir_map.get_resource_id(homedir)
+        if not resource_id:
+            continue
+
+        entries.append({
+            'resource_id': resource_id,
+            'app_key': homedir,
+            'value_json': json.dumps(entry),
+        })
     return entries
 
 
@@ -70,15 +93,23 @@ def main():
     parser.add_argument('--ssl-cert', metavar='file')
     parser.add_argument('--ssl-key', metavar='file')
     parser.add_argument('--ssl-ca', metavar='file')
-    parser.add_argument('--shard', metavar='id')
+    parser.add_argument('--homedir-map', metavar='file')
     opts = parser.parse_args()
 
     logging.basicConfig(level=logging.INFO)
 
+    with open(opts.homedir_map, 'rb') as fd:
+        homedir_map = HomedirMap(json.load(fd))
     be = Backend(opts.report_url, opts.ssl_cert, opts.ssl_key, opts.ssl_ca)
-    entries = read_input(sys.stdin, opts.shard)
+    entries = read_input(sys.stdin, homedir_map)
     logging.info('submitting %d entries...', len(entries))
-    resp = be.submit({'shard': opts.shard, 'entries': entries})
+    timestamp = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())
+    resp = be.submit({
+        'type': DATA_TYPE,
+        'ttl': DATA_TTL,
+        'timestamp': timestamp,
+        'entries': entries,
+    })
     if resp.status_code != 200:
         logging.error('submission failed (HTTP status %d)', resp.status_code)
         sys.exit(1)