How Vue SSR Can Cut Development Time 20% – Full Docker & Kubernetes Deployment Guide
This article walks through the benefits, trade‑offs, and step‑by‑step implementation of Vue server‑side rendering using Koa, then shows how to containerize the service with Docker and automate deployment with Kubernetes, covering code, configuration, and common pitfalls.
Project Benefits
Overall development efficiency improves by 20%.
First‑screen render speed increases, reducing white‑screen time by 40% on weak networks.
Trade‑offs
Before adopting SSR you must consider:
SSR requires a Node.js‑capable server, raising learning cost.
Servers must handle higher load than static‑file serving and need caching strategies.
Two execution environments mean lifecycle hooks (beforeCreate, created) run on both server and client; side‑effect code or platform‑specific APIs can cause issues.
Read the official Vue SSR documentation first to get a solid understanding.
Build a Simple SSR Service
Install dependencies
yarn add vue vue-server-renderer koa vue-server-rendereris the core module for Vue SSR; we use Koa to build the HTTP server.
const Koa = require('koa');
const server = new Koa();
const Vue = require('vue');
const renderer = require('vue-server-renderer').createRenderer();
const router = require('koa-router')();
const app = new Vue({
data: { msg: 'vue ssr' },
template: '<div>{{msg}}</div>'
});
router.get('*', (ctx) => {
renderer.renderToString(app, (err, html) => {
if (err) { throw err; }
ctx.body = html;
});
});
server.use(router.routes()).use(router.allowedMethods());
module.exports = server;This minimal server renders a Vue instance to HTML.
SSR Implementation Details
Directory structure for a real‑world project
app
├── src
│ ├── components
│ ├── router
│ ├── store
│ ├── index.js
│ ├── App.vue
│ ├── index.html
│ ├── entry-server.js // runs on the server
│ └── entry-client.js // runs in the browser
└── server
├── app.js
└── ssr.jsBecause the server and client differ, we need two entry files.
Server entry ( entry-server.js)
import cookieUtils from 'cookie-parse';
import createApp from './index.js';
import createRouter from './router/router';
import createStore from'./store/store';
export default context => {
return new Promise((resolve, reject) => {
const router = createRouter();
const app = createApp({ router });
const store = createStore({ context });
const cookies = cookieUtils.parse(context.cookie || '');
router.push(context.url);
router.onReady(() => {
const matchedComponents = router.getMatchedComponents();
if (!matchedComponents.length) { return reject(new Error('404')); }
Promise.all(
matchedComponents.map(({ asyncData }) => asyncData && asyncData({
store,
route: router.currentRoute,
cookies,
context: { ...context }
}))
)
.then(() => {
context.meta = app.$meta;
context.state = store.state;
resolve(app);
})
.catch(reject);
}, () => { reject(new Error('500 Server Error')); });
});
};Client entry ( entry-client.js)
import createApp from './index.js';
import createRouter from './router/router';
export const initClient = () => {
const router = createRouter();
const app = createApp({ router });
const cookies = cookieUtils.parse(document.cookie);
router.onReady(() => {
if (window.__INITIAL_STATE__) { store.replaceState(window.__INITIAL_STATE__); }
router.beforeResolve((to, from, next) => {
const matched = router.getMatchedComponents(to);
const prevMatched = router.getMatchedComponents(from);
let diffed = false;
const activated = matched.filter((c, i) => diffed || (diffed = (prevMatched[i] !== c)));
if (!activated.length) { return next(); }
Promise.all(activated.map(c => c.asyncData && c.asyncData({
store,
route: to,
cookies,
context: {}
})))
.then(() => next())
.catch(next);
});
app.$mount('#app');
});
};To avoid a singleton Vue instance (which would share state across requests), we expose a factory function:
import Vue from 'vue';
import App from './App.vue';
export default function createApp({ router }) {
const app = new Vue({
router,
render: h => h(App),
});
return app;
};Automatic loading of router and store modules using require.context:
// store implementation (simplified)
const storeContext = require.context('../module/', true, /\.(\/.+)\/js\/store(\/.+){1,}\.js/);
const getStore = (context) => {
storeContext.keys().filter(key => {
const filePath = key.replace(/^(.\/)|(js\/store\/)|(.js)$/g, '');
let moduleData = storeContext(key).default || storeContext(key);
const namespaces = filePath.split('/');
moduleData = normalizeModule(moduleData, filePath);
store.modules = store.modules || {};
const storeModule = getStoreModule(store, namespaces);
VUEX_PROPERTIES.forEach(property => {
mergeProperty(storeModule, moduleData[property], property);
});
return true;
});
};
export default ({ context }) => {
getStore(context);
return new Vuex.Store({ modules: { ...store.modules } });
};Webpack configuration splits common, client, and server builds.
// webpack.base.conf.js – common settings (omitted for brevity) // webpack.server.conf.js
const merge = require('webpack-merge');
const nodeExternals = require('webpack-node-externals');
const VueSSRServerPlugin = require('vue-server-renderer/server-plugin');
const path = require('path');
const baseConfig = require('./webpack.base.conf.js');
const resolve = src => path.resolve(__dirname, './', src);
module.exports = merge(baseConfig, {
entry: { app: ['./src/entry-server.js'] },
target: 'node',
devtool: 'source-map',
output: {
filename: '[name].js',
publicPath: '',
path: resolve('./dist'),
libraryTarget: 'commonjs2'
},
externals: nodeExternals({}),
plugins: [new VueSSRServerPlugin()]
}); // webpack.client.conf.js
const VueSSRClientPlugin = require('vue-server-renderer/client-plugin');
const merge = require('webpack-merge');
const webpack = require('webpack');
const baseConfig = require('./webpack.base.conf');
const UploadPlugin = require('@q/hj-webpack-upload'); // custom CDN upload plugin
const path = require('path');
const resolve = src => path.resolve(__dirname, './', src);
module.exports = merge(baseConfig, {
entry: { app: ['./src/entry-client.js'] },
target: 'web',
output: { filename: '[name].js', path: resolve('./dist'), publicPath: '', libraryTarget: 'var' },
plugins: [
new VueSSRClientPlugin(),
new webpack.HotModuleReplacementPlugin(),
new UploadPlugin(cdn, {
enableCache: true,
logLocal: false,
src: path.resolve(__dirname, '..', Source.output),
dist: path.resolve(__dirname, '..', Source.output),
beforeUpload: (content, location) => {
if (path.extname(location) === '.js') {
return UglifyJs.minify(content, { compress: true, toplevel: true }).code;
}
return content;
},
compilerHooks: 'done',
onError(e) { console.log(e); }
})
]
});SSR server middleware (simplified):
// ssr.js – middleware that renders the bundle
async render(context) {
const renderer = await this.getRenderer();
return new Promise((resolve, reject) => {
renderer.renderToString(context, (err, html) => {
if (err) { reject(err); } else { resolve(html); }
});
});
}
getRenderer() {
return new Promise((resolve, reject) => {
const htmlPath = `${this.base}/index.html`;
const bundlePath = `${this.base}/vue-ssr-server-bundle.json`;
const clientPath = `${this.base}/vue-ssr-client-manifest.json`;
fs.stat(htmlPath, statErr => {
if (!statErr) {
fs.readFile(htmlPath, 'utf-8', (err, template) => {
const bundle = require(bundlePath);
const clientManifest = require(clientPath);
const renderer = createBundleRenderer(bundle, {
template,
clientManifest,
runInNewContext: false,
shouldPrefetch: () => false,
shouldPreload: () => false,
});
resolve(renderer);
});
} else { reject(statErr); }
});
});
}
// app.js – Koa server wiring
const Koa = require('koa');
const server = new Koa();
const router = require('koa-router')();
const ssr = require('./ssr');
server.use(router.routes()).use(router.allowedMethods());
server.use(ssr(server));
server.on('error', (err, ctx) => { console.error('server error', err, ctx); });
module.exports = server;Common Pitfalls
Only beforeCreate and created run during SSR; placing side‑effect code (e.g., setTimeout, setInterval) there can cause memory leaks because the server process persists.
Async data fetching on the server does not automatically forward client cookies; you must manually attach cookies to request headers.
To manipulate <head> for SEO, use vue-meta and assign context.meta = app.$meta() in the server entry.
// src/index.js
Vue.use(Meta);
// entry-server.js
context.meta = app.$meta();Deployment Strategy
Previous manual PM2 deployment suffered from:
Manual ops: each server needed Node and PM2 installed and synchronized.
Environment drift between local dev and production.
Rollback required publishing a new version and re‑triggering CI, making quick rollbacks hard.
Solution: containerize everything with Docker and orchestrate with Kubernetes.
Docker provides lightweight, fast virtualization and guarantees identical environments across stages.
Kubernetes handles automated deployment, scaling, self‑healing, and service discovery.
Overall workflow (image omitted for brevity).
Local Development with Docker
Dependency download (node_modules as a data volume)
# dependency download
docker run -it \
-v $(pwd)/package.json:/opt/work/package.json \
-v $(pwd)/yarn.lock:/opt/work/yarn.lock \
-v $(pwd)/.yarnrc:/opt/work/.yarnrc \
-v mobile_node_modules:/opt/work/node_modules \
--workdir /opt/work \
--rm node:13-alpine \
yarnDevelopment mode (mount project and expose ports)
# dev mode
docker run -it \
-v $(pwd)/:/opt/work/ \
-v mobile_node_modules:/opt/work/node_modules \
--expose 8081 -p 8081:8081 \
--expose 9229 -p 9229:9229 \
--expose 3003 -p 3003:3003 \
--workdir /opt/work \
node:13-alpine \
./node_modules/.bin/nodemon --inspect=0.0.0.0:9229 --watch server server/bin/wwwCI Pipeline
Dockerfile builds an image per commit, enabling precise rollbacks.
FROM node:13-alpine
COPY package.json /opt/dependencies/package.json
COPY yarn.lock /opt/dependencies/yarn.lock
COPY .yarnrc /opt/dependencies/.yarnrc
RUN cd /opt/dependencies \
&& yarn install --frozen-lockfile \
&& yarn cache clean \
&& mkdir /opt/work \
&& ln -s /opt/dependencies/node_modules /opt/work/node_modules
COPY ci/docker/docker-entrypoint.sh /usr/bin/docker-entrypoint.sh
COPY ./ /opt/work/
RUN cd /opt/work \
&& yarn build
WORKDIR /opt/work
EXPOSE 3003
ENV NODE_ENV production
ENTRYPOINT ["docker-entrypoint.sh"]
CMD ["node", "server/bin/www"]CD with Kubernetes
Key objects: Deployment, Service, Ingress.
apiVersion: apps/v1
kind: Deployment
metadata:
name: frontend-mobile
namespace: mobile
labels:
app: frontend-mobile
spec:
selector:
matchLabels:
app: frontend-mobile
replicas: 8
template:
metadata:
name: frontend-mobile
labels:
app: frontend-mobile
spec:
containers:
- name: frontend-mobile
image: nginx:latest
ports:
- containerPort: 3003
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
livenessProbe:
httpGet:
path: /api/serverCheck
port: 3003
httpHeaders:
- name: X-Kubernetes-Health
value: health
initialDelaySeconds: 15
timeoutSeconds: 1
---
apiVersion: v1
kind: Service
metadata:
name: frontend-mobile
namespace: mobile
labels:
app: frontend-mobile
spec:
selector:
app: frontend-mobile
ports:
- protocol: TCP
port: 8081
targetPort: 3003
---
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: frontend-mobile
namespace: mobile
labels:
app: frontend-mobile
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
rules:
- host: local-deploy.com
http:
paths:
- path: /
backend:
serviceName: frontend-mobile
servicePort: 8081Choosing a small resource quota per pod (256 Mi memory, 250 m CPU) allows rapid pod restarts when memory leaks occur, keeping the service available while the root cause is investigated.
Further Work
GitLab integration with Kubernetes for automated rollouts.
Centralized log collection.
AliNode integration for edge deployment.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Huajiao Technology
The Huajiao Technology channel shares the latest Huajiao app tech on an irregular basis, offering a learning and exchange platform for tech enthusiasts.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
