diff --git a/Dockerfile b/Dockerfile
index 07b5add6b5eafa150720dfa713fff1b3d62f70c1..e1c50ca7440723d2f5ab7f9c7caa1f00b748528d 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -12,6 +12,11 @@ RUN apk add \
     make \
     py-pip
 
+RUN apk add \
+    --no-cache \
+    --repository https://dl-3.alpinelinux.org/alpine/edge/testing/ \
+    vips-dev
+
 # install application dependencies
 COPY package.json yarn.lock ./
 RUN JOBS=max yarn install --non-interactive --frozen-lockfile
@@ -31,7 +36,7 @@ WORKDIR /app
 RUN apk add \
     --no-cache \
     --repository https://alpine.global.ssl.fastly.net/alpine/v3.10/community \
-    fftw
+    fftw vips
 COPY --from=build-stage /app/config config
 COPY --from=build-stage /app/lib lib
 COPY --from=build-stage /app/node_modules node_modules
diff --git a/config/default.toml b/config/default.toml
index ff877a0279a515cba20a193434dc99b87f0f6413..e182686d2b09d85efef69d2759a20f6aa78951e0 100644
--- a/config/default.toml
+++ b/config/default.toml
@@ -20,6 +20,8 @@ service_url = 'http://localhost:8800'
 
 # default user avatar, should be a png minimum 512x512
 default_avatar = 'https://images.hive.blog/DQmb2HNSGKN3pakguJ4ChCRjgkVuDN9WniFRPmrxoJ4sjR4'
+# default user cover, should be a png minimum 1344x240
+default_cover = 'https://images.ecency.com/DQmdA9wjRyGmDwrTza59yLSjCWMEc9sFD5sC4ZU5kL9UMqo/cover_fallback_day.png'
 
 # log level to output at
 log_level = 'debug'
diff --git a/src/cover.ts b/src/cover.ts
new file mode 100644
index 0000000000000000000000000000000000000000..1af4b367f64ad1366ac29cf7cffbf1aec7cc8c02
--- /dev/null
+++ b/src/cover.ts
@@ -0,0 +1,64 @@
+/** Serve user cover images. */
+
+import * as config from 'config'
+import { base58Enc } from './utils'
+
+import { Account } from '@hiveio/dhive'
+import {KoaContext, rpcClient} from './common'
+import {APIError} from './error'
+
+const DefaultCover = config.get('default_cover') as string
+const sizeW = 1344
+const sizeH = 240
+
+export async function coverHandler(ctx: KoaContext) {
+    ctx.tag({handler: 'cover'})
+
+    APIError.assert(ctx.method === 'GET', APIError.Code.InvalidMethod)
+    APIError.assertParams(ctx.params, ['username'])
+
+    const username = ctx.params['username']
+
+    interface ExtendedAccount extends Account {
+        posting_json_metadata?: string
+    }
+
+    const [account]: ExtendedAccount[] = await rpcClient.database.getAccounts([username])
+
+    APIError.assert(account, APIError.Code.NoSuchAccount)
+
+    let metadata
+
+    // read from `posting_json_metadata` if version flag is set
+    if (account.posting_json_metadata) {
+        try {
+            metadata = JSON.parse(account.posting_json_metadata)
+            if (!metadata.profile || !metadata.profile.version) {
+                metadata = {}
+            }
+        } catch (error) {
+            ctx.log.debug(error, 'unable to parse json_metadata for %s', account.name)
+            metadata = {}
+        }
+    }
+
+    // otherwise, fall back to reading from `json_metadata`
+    if (!metadata || !metadata.profile) {
+        try {
+            metadata = JSON.parse(account.json_metadata)
+        } catch (error) {
+            ctx.log.debug(error, 'unable to parse json_metadata for %s', account.name)
+            metadata = {}
+        }
+    }
+
+    let coverUrl: string = DefaultCover
+    if (metadata.profile &&
+        metadata.profile.cover_image &&
+        metadata.profile.cover_image.match(/^https?:\/\//)) {
+        coverUrl = metadata.profile.cover_image
+    }
+
+    ctx.set('Cache-Control', 'public,max-age=600')
+    ctx.redirect(`/p/${ base58Enc(coverUrl) }?width=${ sizeW }&height=${ sizeH }`)
+}
diff --git a/src/proxy.ts b/src/proxy.ts
index 19beba45091ecfb57d1a41ddb3070ed67a0566fe..e3922174525caad064597a2c8d88ec2e5fed1f21 100644
--- a/src/proxy.ts
+++ b/src/proxy.ts
@@ -242,7 +242,21 @@ export async function proxyHandler(ctx: KoaContext) {
                 user_agent: 'HiveProxy/1.0 (+https://gitlab.syncad.com/hive/imagehoster)',
             } as any)
         } catch (cause) {
-            throw new APIError({cause, code: APIError.Code.UpstreamError})
+            // old or non existing images, try to get from steemitimages server
+            try {
+                ctx.log.debug({url: url.toString()}, 'fetching from steemit server')
+                res = await fetchUrl(`https://steemitimages.com/0x0/${url.toString()}`, {
+                    open_timeout: 5 * 1000,
+                    response_timeout: 5 * 1000,
+                    read_timeout: 60 * 1000,
+                    compressed: true,
+                    parse_response: false,
+                    follow_max: 5,
+                    user_agent: 'SteemitProxy/1.0 (+https://github.com/steemit/imagehoster)',
+                } as any)
+            } catch (cause) {
+                throw new APIError({cause, code: APIError.Code.UpstreamError})
+            }
         }
 
         APIError.assert(res.bytes <= MAX_IMAGE_SIZE, APIError.Code.PayloadTooLarge)
diff --git a/src/routes.ts b/src/routes.ts
index fa5853a80541e5a40e5568abdf4f84be841d0ee9..48d940ede2dfe2ad67b6f3445c84bded3626fb37 100644
--- a/src/routes.ts
+++ b/src/routes.ts
@@ -3,6 +3,7 @@
 import * as Router from 'koa-router'
 
 import {avatarHandler} from './avatar'
+import {coverHandler} from './cover'
 import {KoaContext} from './common'
 import {legacyProxyHandler} from './legacy-proxy'
 import {proxyHandler} from './proxy'
@@ -22,6 +23,7 @@ async function healthcheck(ctx: KoaContext) {
 router.get('/', healthcheck as any)
 router.get('/.well-known/healthcheck.json', healthcheck as any)
 router.get('/u/:username/avatar/:size?', avatarHandler as any)
+router.get('/u/:username/cover', coverHandler as any)
 router.post('/hs/:accesstoken', uploadHsHandler as any)
 router.post('/:username/:signature', uploadHandler as any)
 router.post('/cs/:username/:signature', uploadCsHandler as any)