In this tutorial, we are going to create a SPA (single page app) using VueJS inside a container with docker.

Installing VueJS/CLI

Assuming you have already installed Docker into your machine, let’s start off by installing the Vue-cli to create what so-called a boilerplate project.

Vue CLI is a full system for rapid Vue.js development, providing:

  • Interactive project scaffolding via @vue/cli.
  • Zero config rapid prototyping via @vue/cli + @vue/cli-service-global.
  • A runtime dependency (@vue/cli-service) that is:
    • Upgradeable;
    • Built on top of webpack, with sensible defaults;
    • Configurable via in-project config file;
    • Extensible via plugins
  • A rich collection of official plugins integrating the best tools in the frontend ecosystem.
  • A full graphical user interface to create and manage Vue.js projects.

Installing the Vue CLI is just one single line of code in your PowerShell or Terminal:

npm install -g @vue/cli

This will install the vue-cli inside npm package globally, providing the vue command in your terminal.

Now we will create a project with vue create <projectName>, I’ll choose “vue-docker-spa” as my project name and pick the Default select features configuration option with babel and eslint.

After installation, you should see output that looks like:

Now we can change to the directory that our app has been set up by typing: cd vue-docker-spa.

To verify it works there are two ways;

1- The conventional way: by simply using run serve command as follow:

npm run serve

You should see now the following screenshot below in your browser at: http://localhost:8080/

2- The GUI way: Vue also comes with an easy to use graphical user interface to create and manage projects, that GUI can be accessed by using vue ui command as follow:

vue ui

the above command will open a browser window automatically.

And because we created the project using the command line interface CLI, not the GUI then we need to import the project folder into our dashboard by clicking on Import button in Vue Project Manager screen as below:

Now you can go to Tasks panel and select the Serve then Run Task button to run Vue server then Open app button to open the browser window:

You should see again the same screen as before in your browser at: http://localhost:8080/

Note that the GUI manager is under different port than the app itself, here the manager is on http://localhost:8000 while the app is on: http://localhost:8080/

Now as we have everything sorted and in order to understand how Vue works let’s dive a little bit deeper inside Vue and build something useful, let’s try to make a simple ToDo app to test some of Vue functionality.

Vue.JS App Anatomy

Like any other web application, the default and first page or screen a user will face is index.html, likewise, in Vue.JS this page is located inside the public folder: root/public/index.html

It contains all the necessary HTML elements such as <html> <head> <body> in addition there is the key element for Vue; a <div> with id=”app”, inside this element Vue compiling engine will inject the app when building for development or deployment.

index.html

Components

Vue mainly consists of component files which have the .vue file extension. One main component called App.vue should be there in any Vue.JS application then we can add other components and sub-component to that main component.

Any component file will have the following elements or tags inside it:

A Typical Component {file}.vue

<template>

This is where the HTML located; it should only have ONE HTML tag inside it then we can write our vue components code inside this tag along with data binding and other properties, methods and actions.

<script>

This is where to write our JS. It has to have export default classes, and usually, it contains name, components, data and methods. The scripts tag may also contain import to import classes from other components or packages.

<style>

If the style tag also contains the operative word scoped then the CSS inside the tag is for the current file only or local. If not then the styling is global, will apply to the whole app.

The main component App.vue

The App.vue is the main component which should contain all the other components (if we don’t use routing as we do here in this tutorial), or in other words, you can think of it as your landing page. It also contains the main CSS of the app and the main code.

This is the page which will be rendered then injected into index.html upon compiling, note below the keyword id=”app” inside its <template> at the top.

App.vue

Nested Components

We can add many components inside each other, these sometimes also called sub-components. One of the reasons for doing so is (like in our tutorial here) to have one item of a long list of many items injected into another element and then all to be injected inside a different component or file.

In our app, we have a list of todos contains many of todo tasks, so we used a single-todo-item component file (Todoitem.vue) imported inside another component file (Todos.vue) which is imported inside (App.vue).

The code inside Todos.vue will iterate through the data to get the titles and ids values and then inject them inside its template using the Todoitem.vue template which consists of 3 HTML elements; <p> <input checkbox> <button>. Then after all the items been injected; the Todos.vue template with all its contents will be injected as well inside the main app template inside App.vue.

The Code

Main App Component

Inside your src/App.vue remove everything is already there and add the following code:

App.vue

<template>
  <div id="app">
    <Header />
    <AddTodo v-on:add-todo="addTodo"/>
    <Todos v-bind:todos="todos" v-on:del-todo="deleteTodo"/>
  </div>
</template>

<script>
import Header from './components/layout/Header';
import AddTodo from './components/AddTodo';
import Todos from './components/Todos';

export default {
  name: 'app',
  components: {
    Header, Todos, AddTodo
  },
  data(){
    return{
      todos: [
        {
          id: 1,
        title: "Finishing the tutorial",
        completed: false
        },
        {
          id: 2,
        title: "Building the app",
        completed: false
        },
        {
          id: 3,
        title: "Deploying the app",
        completed: false
        },
        {
          id: 4,
        title: "Testing the app",
        completed: false
        }
      ]
    }
  },
  methods:{
    deleteTodo(id) {
      this.todos = this.todos.filter(todo => todo.id !== id);
    },
    addTodo(newTodo){
      this.todos = [...this.todos, newTodo];
    }
  }
}
</script>

<style>
*{
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}
body{
  font-family: Arial, Helvetica, sans-serif;
  line-height: 1.4;
}
.btn{
  display: inline-block;
  border: none;
  background: #555;
  color: #fff;
  padding: 7px 20px;
  cursor: pointer;
}
.btn:hover{
  background: #666;
}
</style>

Adding the components

We will add three main components to the app and one sub-component, as follow:

  • Header: to use as application name for now and later on as a navigation bar.
  • AddTodo: This is a small form with one input field to add new ToDo tasks.
  • Todos: This is to act like the main component which will hold a smaller or sub-component of each Todo task, which will be called Todoitem.

Inside /component directory, create three files: AddTodo.vue, Todos.vue and Todoitem.vue. The fourth one we’ll create is called Header.vue inside a subfolder called layout that is only to make our file structure cleaner, but you can locate that anywhere.

AddTodo.vue

This component also requires uuid library to generate random unique IDs for us on the fly, you can install it from npm by the following command:

npm i uuid

Now add the following code and save the file:

AddTodo.vue

<template>
  <div>
    <form @submit="addTodo">
      <input type="text" v-model="title" required name="title" placeholder="Add Todo..">
      <input type="submit" value="Submit" class="btn">
    </form>
  </div>
</template>

<script>
import uuid from 'uuid';
export default {
  name: "AddTodo",
  data(){
    return {
      title: ''
    }
  },
  methods: {
    addTodo(e) {
      e.preventDefault();
      const newTodo = {
        id: uuid.v4(),
        title: this.title,
        completed: false
      }
      // Send up to parent
      this.$emit('add-todo', newTodo);

      this.title = '';
    }
  }
}
</script>

<style scoped>
  form {
    display: flex;
  }
  input[type="text"]{
    flex: 10;
    padding: 5px;
  }
</style>

Todos.vue

<template>
  <div>
    <div v-bind:key="todo.id" v-for="todo in todos">
      <TodoItem v-bind:todo="todo" v-on:del-todo="$emit('del-todo', todo.id)"/>
    </div>
  </div>
</template>

<script>
import TodoItem from './Todoitem.vue'

export default {
  name: "Todos",
  components: {
    TodoItem
  },
  props: ["todos"]
}
</script>

<style scoped>
</style>

Todoitem.vue

<template>
  <div class="todo-item" v-bind:class="{'is-complete':todo.completed}">
    <p>
      <input type="checkbox" v-on:change="markComplete">
      {{todo.title}}
      <button class="del" @click="$emit('del-todo', todo.id)">x</button>
      </p>
  </div>
</template>

<script>
export default {
  name: "TodoItem",
  props: ["todo"],
  methods: {
    markComplete() {
      this.todo.completed = !this.todo.completed;
    }
  }
}
</script>

<style scoped>
  .todo-item{
    background: #f4f4f4;
    padding: 10px;
    border-bottom: 1px #ccc dotted;
  }

  .is-complete{
    text-decoration: line-through;
  }

  .del{
    background: #ff0000;
    color: #fff;
    border: none;
    padding: 5px 10px;
    border-radius: 50%;
    cursor: pointer;
    float: right;
  }
</style>

Header.vue

<template>
  <header class="header">
    <h1>My ToDo List<br >Internet Technologies, UWS</h1>
  </header>
</template>

<script>
export default {
  name: 'Header'
}
</script>

<style scoped>
  .header{
    background-color: #333;
    color: #fff;
    font-size: 24px;
    text-align: center;
    padding: 10px;
  }

  .header a{
    color: #fff;
    padding-right: 5px;
  }
</style>

If you save and check the app, you should be able to see the following screen:

You can do some tests by adding new tasks, deleting tasks or marking some as completed. If everything is OK, then it’s time to deployment.

We can either deploy the app using one of the cloud application web services such as Heroku, AWS or Firebase. Or, deploy it with Docker container.

Deploying with Docker Container

To create a docker container, we’ll need to create a Dockerfile that runs nginx, a high-performance web server.

Inside your project folder create a new file: Dockerfile (yes without extension) and copy/paste the following code inside it:

# Create the container from the alpine linux image
FROM alpine:3.7

# Add nginx and nodejs
RUN apk add --update nginx nodejs

# Create the directories we will need
RUN mkdir -p /tmp/nginx/vue-single-page-app
RUN mkdir -p /var/log/nginx
RUN mkdir -p /var/www/html

# Copy the respective nginx configuration files
COPY nginx_config/nginx.conf /etc/nginx/nginx.conf
COPY nginx_config/default.conf /etc/nginx/conf.d/default.conf

# Set the directory we want to run the next commands for
WORKDIR /tmp/nginx/vue-single-page-app
# Copy our source code into the container
COPY . .
# Install the dependencies, can be commented out if you're running the same node version
RUN npm install

# run webpack and the vue-loader
RUN npm run build

# copy the built app to our served directory
RUN cp -r dist/* /var/www/html

# make all files belong to the nginx user
RUN chown nginx:nginx /var/www/html

# start nginx and keep the process from backgrounding and the container from quitting
CMD ["nginx", "-g", "daemon off;"]

We also need to create the nginx config files that we’re referencing, nginx.conf and default.conf in the directory nginx_config:

So, I created a sub-directory called nginx_config with the following files:

nginx.conf

user  nginx;
worker_processes  1;

error_log  /var/log/nginx/error.log warn;
pid        /var/run/nginx.pid;

events {
    worker_connections  1024;
}

http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    sendfile        off;

    keepalive_timeout  60;

    #gzip  on;
    include /etc/nginx/conf.d/*.conf;
}

default.conf

server {
  location / {
      root /var/www/html;
      try_files $uri $uri/ /index.html;
  }
}

Now let’s build our docker container, I will name it: vue-docker-container-spa

docker build -t vue-docker-container-spa .

and finally, we can run our container with: docker run command

docker run -p 8080:80 vue-docker-container-spa

Now you should be able to see your app in your browser again, but this time it is running from within Docker and you are free to deploy your container wherever you want!

let’s now stop the currently running container by using right-click > stop option over container name > dropdown menu. In order to try the other way, I will deploy the app using Firebase Hosting service by Google.

Deploying with Firebase

You can use any cloud base service to deploy your VueJS application. I chose the Firebase Hosting.

Firebase Hosting is one of many services Google is offering with their Firebase integrated cloud solution. The other services are Authentication, Database, Storage, Cloud Functions and Machine Learning Kit. Firebase Hosting is basically a static site hosting that would be perfect for any frontend application, it also supports SSL, CDN and custom domains.

Before you start:

  • Ensure you have a google account
  • Login to Firebase Console
  • Create a project; I called mine vue-todo-spa
  • Navigate to the hosting tab and click get started and follow the steps.

After that, Install firebase-tools globally:

npm install -g firebase-tools

This will install the firebase-tools inside npm package globally, providing the firebase command in your terminal.

Sign in to your Google account:

firebase login

Then from your project’s root directory, initialise firebase using the command:

firebase init

Firebase will ask some questions on how to set up your project.

  • Choose which Firebase CLI features you want to set up your project. Make sure to select hosting.
  • Select the default Firebase project for your project, in our case here; we’ll choose “Use an existing project” then select the project we created earlier vue-todo-spa
  • Set your public directory to dist (or where your build’s output is) which will be uploaded to Firebase Hosting. To do that, I answered dist to the next question: What do you want to use as your public directory? (public)
// firebase.json
{
  "hosting": {
    "public": "dist"
  }
}
  • Select yes to configure your project as a single-page app. This will create an index.html and on your dist folder and configure your hosting information.
// firebase.json
{
  "hosting": {
    "rewrites": [
      {
        "source": "**",
        "destination": "/index.html"
      }
    ]
  }
}

If everything went OK, you’ll get the following output line:

+  Firebase initialization complete!

The project file-tree should look similar to the following, note the new Firebase files and dist/index.html file:

Now let’s build our project by run npm run build command.

npm run build

Then finally, to deploy your project on Firebase Hosting, run the command:

firebase deploy --only hosting

This is a screenshot of the output that I’ve got:

You can now access your project on https://<YOUR-PROJECT-ID>.firebaseapp.com

For me, in this tutorial my app was successfully hosted under the following domain: https://vue-todo-spa.firebaseapp.com

And here is the source code on Github: https://github.com/heshamsaqqar/vue-todo-app

References:

https://cli.vuejs.org/guide/

https://jonathanmh.com/deploying-a-vue-js-single-page-app-including-router-with-docker/

https://www.taniarascia.com/getting-started-with-vue/

https://cli.vuejs.org/guide/deployment.html#firebase