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.

Huajiao Technology
Huajiao Technology
Huajiao Technology
How Vue SSR Can Cut Development Time 20% – Full Docker & Kubernetes Deployment Guide

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-renderer

is 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.js

Because 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).

Deployment workflow
Deployment workflow

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 \
    yarn

Development 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/www

CI 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: 8081

Choosing 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.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

BackendDockerKubernetesNode.jsSSRVue
Huajiao Technology
Written by

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.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.