diff --git a/Dockerfile-production b/Dockerfile-production
new file mode 100644
index 0000000000000000000000000000000000000000..57dc80537a12d810753aa045b6310e32475ec4d4
--- /dev/null
+++ b/Dockerfile-production
@@ -0,0 +1,52 @@
+# IMAGE FOR BUILDING
+FROM node:12.20-alpine as build
+
+RUN apk add --no-cache git python make g++
+
+WORKDIR /home/node/app
+
+COPY package.json .
+COPY yarn.lock .
+
+# Install production node modules for server use
+RUN yarn install --frozen-lockfile --production=true
+# Copy to another folder for later use
+RUN mv node_modules production_node_modules
+
+# Install development node modules for building webpack bundle
+RUN yarn install --frozen-lockfile --production=false
+
+COPY . .
+
+ARG node_env
+ARG server_protocol
+ARG server_host
+ARG server_port
+
+ENV NODE_ENV=$node_env
+ENV SERVER_PROTOCOL=$server_protocol
+ENV SERVER_HOST=$server_host
+ENV SERVER_PORT=$server_port
+
+RUN yarn pubsweet build
+
+# IMAGE FOR RUNNING
+FROM node:12.20-alpine as server
+
+WORKDIR /home/node/app
+
+RUN chown -R node:node .
+USER node
+
+COPY --chown=node:node ./config ./config
+COPY --chown=node:node ./public ./public
+COPY --chown=node:node ./scripts ./scripts
+COPY --chown=node:node ./server ./server
+COPY --chown=node:node ./startServer.js .
+
+COPY --from=build /home/node/app/_build/assets ./_build
+COPY --from=build /home/node/app/production_node_modules ./node_modules
+
+ENTRYPOINT ["sh", "./scripts/setupProdServer.sh"]
+
+CMD ["node", "./startServer.js"]
diff --git a/config/custom-environment-variables.js b/config/custom-environment-variables.js
index 60227e35a0ee96c00a02a407c8ff7c5a681c760d..92f0c19e7f8342e19be8d69243359ad3805e50ad 100644
--- a/config/custom-environment-variables.js
+++ b/config/custom-environment-variables.js
@@ -8,11 +8,11 @@ module.exports = {
     port: 'SERVER_PORT',
     secret: 'PUBSWEET_SECRET',
     db: {
-      user: 'POSTGRES_USER',
-      password: 'POSTGRES_PASSWORD',
       host: 'POSTGRES_HOST',
-      database: 'POSTGRES_DB',
       port: 'POSTGRES_PORT',
+      database: 'POSTGRES_DB',
+      user: 'POSTGRES_USER',
+      password: 'POSTGRES_PASSWORD',
     },
   },
 }
diff --git a/config/default.js b/config/default.js
index 62ecdfb4fd11a9339695310697314f4c6bbb452a..b39e7eb1c468864d552f60b242031bf4e2135c9d 100644
--- a/config/default.js
+++ b/config/default.js
@@ -1,6 +1,7 @@
 const path = require('path')
 const components = require('./components.json')
 const logger = require('winston')
+const { deferConfig } = require('config/defer')
 
 module.exports = {
   teams: {
@@ -40,6 +41,10 @@ module.exports = {
     port: 3000,
     logger,
     uploads: 'uploads',
+    baseUrl: deferConfig(
+      cfg =>
+        `['pubsweet-server'].protocol:://['pubsweet-server'].host:${cfg['pubsweet-server'].port}`,
+    ),
     typeDefs: `
       extend type User {
         name: String
diff --git a/config/production.js b/config/production.js
new file mode 100644
index 0000000000000000000000000000000000000000..4ba52ba2c8df6758685c8f65f490306b5c44eb76
--- /dev/null
+++ b/config/production.js
@@ -0,0 +1 @@
+module.exports = {}
diff --git a/docker-compose.production.yml b/docker-compose.production.yml
new file mode 100644
index 0000000000000000000000000000000000000000..fdb0dc1ab66568d5547a9a5bc1fac1ac789b9ea5
--- /dev/null
+++ b/docker-compose.production.yml
@@ -0,0 +1,46 @@
+version: '3'
+
+services:
+  server:
+    build:
+      context: .
+      dockerfile: ./Dockerfile-production
+      target: server
+      args:
+        - node_env=${NODE_ENV:-production}
+        - server_protocol=${SERVER_PROTOCOL}
+        - server_host=${SERVER_HOST}
+        - server_port=${SERVER_PORT}
+    ports:
+      - ${SERVER_PORT:-3000}:${SERVER_PORT:-3000}
+    environment:
+      - NODE_ENV=${NODE_ENV:-production}
+      - POSTGRES_HOST=${POSTGRES_HOST}
+      - POSTGRES_PORT=${POSTGRES_PORT}
+      - POSTGRES_DB=${POSTGRES_DB}
+      - POSTGRES_USER=${POSTGRES_USER}
+      - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
+      - PUBSWEET_SECRET=${PUBSWEET_SECRET}
+      - SERVER_PROTOCOL=${SERVER_PROTOCOL}
+      - SERVER_HOST=${SERVER_HOST}
+      - SERVER_PORT=${SERVER_PORT}
+      - ORCID_CLIENT_ID=${ORCID_CLIENT_ID}
+      - ORCID_CLIENT_SECRET=${ORCID_CLIENT_SECRET}
+
+  job-xsweet:
+    image: pubsweet/job-xsweet
+    depends_on:
+      - server
+    command:
+      [
+        'bash',
+        './scripts/wait-for-it.sh',
+        'server:${SERVER_PORT}',
+        --,
+        'node',
+        'src/xsweet.js',
+      ]
+    environment:
+      - DATABASE_URL=postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}
+    volumes:
+      - ./scripts/wait-for-it.sh:/home/node/scripts/wait-for-it.sh
diff --git a/scripts/setupProdServer.sh b/scripts/setupProdServer.sh
new file mode 100644
index 0000000000000000000000000000000000000000..1fec3a5a8de2b4143424223254a06c5e12507a72
--- /dev/null
+++ b/scripts/setupProdServer.sh
@@ -0,0 +1,7 @@
+#!/bin/sh
+set -x
+
+# This is run through docker. Its CWD will be the root folder.
+node_modules/.bin/pubsweet migrate
+
+exec "$@"
diff --git a/server/app.js b/server/app.js
index 22ada082520d1dc728d3effe9d194c0bad60343f..157e621c656ae7683ef5906a95d44fd1e79a99bb 100644
--- a/server/app.js
+++ b/server/app.js
@@ -90,6 +90,10 @@ const configureApp = app => {
   // app.use('/', index)
   app.use('/healthcheck', (req, res) => res.send('All good!'))
 
+  app.get('*', (req, res) => {
+    res.sendFile(path.join(__dirname, '..', '_build', 'index.html'))
+  })
+
   app.use((err, req, res, next) => {
     // development error handler, will print stacktrace
     if (app.get('env') === 'development' || app.get('env') === 'test') {