Development Environment for Web App with Containers (Docker, Vue.js, Go) — Part 1

Karol Filipczuk
8 min readMay 21, 2021
Photo by Emile Perron on Unsplash

We will create bare minimum environment for developing web app. At the end of the series you will have containerized backend (Go), containerized frontend (Vue.js) which can communicate with each other and supports live reloading. All of it will start using single command — docker compose up.

Prerequisite

  • Docker
  • Command line (bash, zsh etc.)

Scope

Part 1 will focus on Go backend and project structure:

  1. Project structure
  2. Simple Go app
  3. Build Go docker image from Dockerfile and run container
  4. Use docker compose to run container and build image
  5. Update the solution to include live reloading on code change
  6. Update the backend to act as web server

Project structure

We firstly have to create project structure. Let’s start with 3 basic directories. My project is called football-coach. Create inside following directories: backend, frontend, docker.

Backend directory will store all the Go code written for backend.
Frontend directory will store all the Vue.js, HTML, CSS for frontend.
Docker directory will store all Dockerfiles necessary for building custom Go and Vue.js docker images.

You can store Dockerfiles in backend/frontend directories as well. Personally, I prefer to keep docker related files away from app code files.

Create a main.go file in backend directory and copy&paste following code.

It is a simple code which will only print to standard out message: “Hello from container!”

Then create Dockerfile-backend in docker directory and copy&paste following code.

It’s Dockerfile we will use for building our backend. Builds on golang:1.16 image. We then switch working directory to /go/src/appand copy all files from current directory to working directory in container. Then we run command go build main.go which will build executable binary named main in the working directory. As final step CMD will run executable. The result will be visible in container logs.

Create also docker-compose.yml in project’s root directory and leave it empty for now.

Alright! Your project structure should look now similar to mine:

Directory structure v1

Build v0.1

Let’s build image for backend and try to run it. Note: currently we are trying to run anything as a proof that we are going in a correct direction.

Build and run docker image

When you have all code and Dockerfile in the same directory and on top of that you are currently in the same directory then building docker image is as simple as running docker build .
We are keeping Dockerfiles in separate directory and we want to run build command from project’s root, therefore we need to use a slightly different command.

//Docker build command 
docker build <path-to-backend-dir> -t <image-name>:<tag> -f <path-to-Dockerfile>
//Docker build command specific to our example
docker build backend -t mygo:latest -f docker/Dockerfile-backend

Running above command from project’s root will successfully build our first, initial Go image including our code.

Initial Go image build

Now run the docker image and check logs for “Hello from container!” message.

docker run mygo
Run of initial Go image

There you go! We successfully containerized our simple application and run it in container.

Excuse me? What this is suppose to be?!

Right? Above setup is cumbersome to use for development. You would need to build image and run new container every time any change in the code is made.
…and that is where we are heading now :-)

Next steps:
- define docker-compose.yml to handle image build and container run
- install Reflex tool which will be used to observe code changes and reload

Build v0.2

Define initial docker-compose.yml

Intial docker-compose.yml will simply build docker image and run container using single command docker compose up.
Add following code to docker-compose.yml:

Let’s explain docker-compose.yml line by line.
Line 1 define version of docker compose.
Line 2 indicates that below services will be defined.
Line 3 it’s a name of our first service. You can name it differently, if you want.
Line 4–6 build section is optional. We use it to specify context and dockerfile. Context tells docker to act as if build was done in ./backend directory. Dockerfile specify which dockerfile use for building image.
Line 7 is name for the image.

Run docker compose up from project’s root directory.

Docker compose first run

Docker compose build new image named football-coach-backend:latest as stated in config, then attached default network and run a container with new image. As a result message “Hello from container!” was printed and container exited.
You can now run docker compose down to remove container and network.

Live reloading with Reflex

Let’s implement live reloading on code change. We will need to update docker-compose.yml and Dockerfile-backend.

Live reloading is based on observing files and performing an action when anything has been changed inside these files. To enable live code reloading we need to change how currently the code is deployed into container and how we are running our Go code.

docker-compose.yml

Right now we copy all the code inside backend directory into container directory /go/src/app when we build image. The actual code which would need to change is inside container and we don’t want that. We want to change the code in backend directory and reload on that event.

To make sure container is running on actual code in backend directory instead of copy of code, we need to use volumes. Volume will mount a directory from our project to a container. This way any change made in mounted directory will be immediately reflected in container. Mount backend directory to /go/src/app directory in docker-compose.yml:

volumes:      - ./backend:/go/src/app

Then we need to change a command used to run our Go code. Currently we are building executable binary and running it. Now we need to use Reflex to run our Go code on every change.

command: reflex -r '\.go$$' -s go run main.go

Let’s dive into it:
reflex -r '\.go$$' is stating that reflex will watch for any change in all files which name ends with .go . If you want to reload on change of any file, remove run reflexcommand without -r option.
Option -s is stating that we are running service and reflex should restart it every time code change.
go run main.go is a command we want to be run with every code change.

At the end docker-compose.yml should look like that:

Updated docker-compose.yml

Dockerfile-backend

Since we are now defining command to run and volumes in docker compose, we can get rid of equivalent functionality in Dockerfile-backend.
The one thing we need to add is installing Reflex tool, which is not available by default in golang:1.16 image.

Remove COPY, RUN and CMD command. Then add RUN command to install Reflex.

Updated Dockerfile-backned

Give it a try!

We should be now able to use live reloading with our Go app. Make sure you previously executed docker compose down and you might want also to remove docker image created by docker compose (docker image ls -> docker rim <IMAGE_ID>).

Now we are ready to test it. Run docker compose up .

First start with live reloading

Image was built successfully and container is running.
Reflex started a service, which for us is simply command go run main.go .
But…the container didn't exited after code execution, it is good sign.
Container is still running, because Reflex is observing files, waits for any changes.

Go ahead and change in main.go message from “Hello from container!” to “This time live from container!”. Remember to save the file after update.

Live reload works!

After the change in main.go, Reflex killed the service and started it again. This time the logs says “This time live from container!” and we didn't have to build image again or restart container. It works!

Build v0.3

We have now working docker compose with Go backend and live reloading on code change. Great, but we need to remember that backend will act as web server. Let’s make changes to have a simple web server returning “Hello World!” messages.

Web server

We will need to make two changes:

  • change main.go code to act as web server on port 8080
  • change docker-compose.yml to expose port 8080

Change main.go

We will use net/http to set up web server. Add net/http and log to import section.
Then create new function above main() which will be our response to GET on port 8080. Add following code

func helloWorld(w http.ResponseWriter, r *http.Request) {  fmt.Fprintf(w, "Hello World!")}

Update main() which will now become web server listening on port 8080.

func main() {  http.HandleFunc("/", helloWorld)  fmt.Println("Starting web server on 8080...")  if err := http.ListenAndServe(":8080", nil); err != nil {    log.Fatal(err)  }}

http.HandleFunc is defining what should be done upon receiving GET.
http.ListenAndServe is starting our web server on port 8080. If starting server will end with error, then it will be logged by log.Fatal().
Your complete code should look similar to mine.

main.go acting as web server

Expose port 8080 in container

After change made in main.go our web server is ready to run on local machine. It will also start when running in the container, but it will server on port 8080 inside container and currently we don’t have access to from outside.

We need now to expose port 8080 from container to our local machine. We will do it by adding ports to our docker-compose.yml. After the change we will have access from our local machine to the web server inside container.

Running our web server with docker compose

Go ahead and run docker compose up.

Running web server with docker compose

Open web browser and naviagte to localhost:8080.

Hello World from web browser

Check if live reloading still works. Change “Hello World!” to “Hello World with live reloading!” in main.go.
Logs looks ok.

Web server live reloading

Refresh page in web browser.

Hello World with live reloading!

Great! Looks that we now have simple web server with live reloading.

We will finish here Part 1 of the series. In the next part we will add Vue.js to frontend, containerize it and enable live reloading as well.

--

--