Deploy a Golang Web Application and Couchbase as Docker Containers
Learn how to deploy a Golang web application as a Docker container alongside Couchbase using microservices and Docker images.
Join the DZone community and get the full member experience.
Join For FreeIn my development series regarding Docker containers for the web application developer, I went over deploying Java and Node.js applications as containers alongside Couchbase Server Containers. This time around I thought it would be cool to deploy a Golang web application as a Docker container alongside Couchbase.
The process is very similar to what I have already demonstrated in previous tutorials, but it is worth exploring. We’re going to explore containerizing an application that I had explained in a previous tutorial on the topic of URL shortening services.
If you’re interested, you can check out the articles Use Docker to Deploy a Containerized Java With Couchbase Web Application and Deploy a Node.js With Couchbase Web Application as Docker Containers, depending on your language preferences.
The Requirements
There is only one dependency that must be met to be successful with this guide. You need Docker installed on a host machine. You don’t need Golang installed or Couchbase Server — that is the beauty of the Docker Engine.
While you don’t have to, I would recommend becoming familiar with the application in question. To learn about the code behind the application, check out the tutorial I wrote called Create a URL Shortener With Golang and Couchbase NoSQL.
Establishing a Docker Project for the Golang Application
The source code to the application, and what we’ll be bundling, can be seen below. You should save it as a Go source code file such as main.go
.
package main
import (
"encoding/json"
"log"
"net/http"
"os"
"time"
"github.com/couchbase/gocb"
"github.com/gorilla/mux"
"github.com/speps/go-hashids"
)
type MyUrl struct {
ID string `json:"id,omitempty"`
LongUrl string `json:"longUrl,omitempty"`
ShortUrl string `json:"shortUrl,omitempty"`
}
var bucket *gocb.Bucket
var bucketName string
func ExpandEndpoint(w http.ResponseWriter, req *http.Request) {
var n1qlParams []interface{}
query := gocb.NewN1qlQuery("SELECT `" + bucketName + "`.* FROM `" + bucketName + "` WHERE shortUrl = $1")
params := req.URL.Query()
n1qlParams = append(n1qlParams, params.Get("shortUrl"))
rows, _ := bucket.ExecuteN1qlQuery(query, n1qlParams)
var row MyUrl
rows.One(&row)
json.NewEncoder(w).Encode(row)
}
func CreateEndpoint(w http.ResponseWriter, req *http.Request) {
var url MyUrl
_ = json.NewDecoder(req.Body).Decode(&url)
var n1qlParams []interface{}
n1qlParams = append(n1qlParams, url.LongUrl)
query := gocb.NewN1qlQuery("SELECT `" + bucketName + "`.* FROM `" + bucketName + "` WHERE longUrl = $1")
rows, err := bucket.ExecuteN1qlQuery(query, n1qlParams)
if err != nil {
w.WriteHeader(401)
w.Write([]byte(err.Error()))
return
}
var row MyUrl
rows.One(&row)
if row == (MyUrl{}) {
hd := hashids.NewData()
h := hashids.NewWithData(hd)
now := time.Now()
url.ID, _ = h.Encode([]int{int(now.Unix())})
url.ShortUrl = "http://localhost:12345/" + url.ID
bucket.Insert(url.ID, url, 0)
} else {
url = row
}
json.NewEncoder(w).Encode(url)
}
func RootEndpoint(w http.ResponseWriter, req *http.Request) {
params := mux.Vars(req)
var url MyUrl
bucket.Get(params["id"], &url)
http.Redirect(w, req, url.LongUrl, 301)
}
func main() {
router := mux.NewRouter()
cluster, _ := gocb.Connect("couchbase://" + os.Getenv("COUCHBASE_HOST"))
bucketName = os.Getenv("COUCHBASE_BUCKET")
bucket, _ = cluster.OpenBucket(bucketName, "")
router.HandleFunc("/{id}", RootEndpoint).Methods("GET")
router.HandleFunc("/expand/", ExpandEndpoint).Methods("GET")
router.HandleFunc("/create", CreateEndpoint).Methods("PUT")
log.Fatal(http.ListenAndServe(":12345", router))
}
The only difference between the code above and the previous tutorial on the subject is the use of the os.Getenv("COUCHBASE_HOST")
and os.Getenv("COUCHBASE_BUCKET")
commands. This will allow us to define the Couchbase connection information at container runtime via environment variables rather than hardcoding it into the application.
If you had Couchbase Server running, you could launch the web application by doing:
env COUCHBASE_HOST=localhost COUCHBASE_BUCKET=default go run *.go
Of course, the project would need to be in your $GOPATH
, but it would be accessible from http://localhost:12345
as defined in the code.
The goal here is not to run this project locally but via a Docker container. Create a Dockerfile file next to the main.go
file or whatever you called it. The Dockerfile file should contain the following:
FROM golang:alpine
RUN apk update && apk add git
COPY . /go/src/app/
WORKDIR /go/src/app
RUN go get -d -v
RUN go install -v
CMD ["app"]
As a base, we’ll be using the official Golang Alpine Linux image for Docker, but we’ll be customizing it. During build time, we are installing Git, copying the main.go
file to the image, installing the project dependencies found in the import
section, and installing the application. At runtime, we are executing the installed application.
To build an image from our custom Docker blueprint, we would execute the following:
docker build -t golang-project .
After all the compile-time steps complete, we’ll be left with a golang-project
Docker image. Before we try to run this image, we need to worry about Couchbase. While we could run Couchbase outside a container, where is the fun in that?
Creating a Custom Couchbase Server Docker Image
Just like with Golang, an official Docker image exists for Couchbase Server. While it is a perfectly acceptable solution, it is not an automated solution. This means that you’ll have to manually configure the database after the container starts. We probably want to avoid that.
Instead, create a directory somewhere on your computer with a Dockerfile file and configure.sh
file in it. The plan is to create a Docker blueprint that will execute the configuration script for us.
Open the Dockerfile file and include the following:
FROM couchbase
COPY configure.sh /opt/couchbase
CMD ["/opt/couchbase/configure.sh"]
We’re basically just copying the script into the base image and running it. The real magic happens inside of the script.
Open the configure.sh
file and include the following:
set -m
/entrypoint.sh couchbase-server &
sleep 15
curl -v -X POST http://127.0.0.1:8091/pools/default -d memoryQuota=512 -d indexMemoryQuota=512
curl -v http://127.0.0.1:8091/node/controller/setupServices -d services=kv%2cn1ql%2Cindex
curl -v http://127.0.0.1:8091/settings/web -d port=8091 -d username=$COUCHBASE_ADMINISTRATOR_USERNAME -d password=$COUCHBASE_ADMINISTRATOR_PASSWORD
curl -i -u $COUCHBASE_ADMINISTRATOR_USERNAME:$COUCHBASE_ADMINISTRATOR_PASSWORD -X POST http://127.0.0.1:8091/settings/indexes -d 'storageMode=memory_optimized'
curl -v -u $COUCHBASE_ADMINISTRATOR_USERNAME:$COUCHBASE_ADMINISTRATOR_PASSWORD -X POST http://127.0.0.1:8091/pools/default/buckets -d name=$COUCHBASE_BUCKET -d bucketType=couchbase -d ramQuotaMB=128 -d authType=sasl -d saslPassword=$COUCHBASE_BUCKET_PASSWORD
sleep 15
curl -v http://127.0.0.1:8093/query/service -d "statement=CREATE PRIMARY INDEX ON `$COUCHBASE_BUCKET`"
fg 1
Couchbase Server has a RESTful interface that will allow us to complete the configuration wizard through a script. The above commands will define instance specs and services, administrative credentials, and a Bucket.
You’ll notice things like $COUCHBASE_ADMINISTRATOR_USERNAME
within the script. Just like in the Golang application we are leveraging environment variables to prevent having to hardcode values into our image. These values can be defined during the container deployment.
More information on provisioning a Couchbase Server container can be found in a previous article that I wrote on the subject.
Finally, we can build this custom Couchbase Server image. From the Docker Shell, execute the following:
docker build -t couchbase-custom .
The above command will leave us with a couchbase-custom
image.
Deploying the Containerized Microservice
There are a few ways to deploy the images that we built. We’ll explore two of them here.
Given what we know about our images, we can deploy our containers with the following commands:
docker run -d \
-p 8091-8093:8091-8093 \
-e COUCHBASE_ADMINISTRATOR_USERNAME=Administrator \
-e COUCHBASE_ADMINISTRATOR_PASSWORD=password \
-e COUCHBASE_BUCKET=default \
-e COUCHBASE_BUCKET_PASSWORD= \
--network="docker_default" \
--name couchbase \
couchbase-custom
The above command will deploy Couchbase Server while mapping the necessary ports to the host operating system. We’re passing each environment variable in with the command and defining an operating network. The network is important because you want both your database and application to be on the same container network.
docker run -d \
-p 12345:12345 \
-e COUCHBASE_HOST=couchbase \
-e COUCHBASE_BUCKET=default \
--network="docker_default" \
--name golang
golang-project
The above command will deploy our Golang image while mapping the correct ports again. For the host, you’ll notice that we’re using couchbase
, which is actually the name of our other container. The container names also act as host names.
The commands you just saw are a very manual way to do things in my opinion. Instead, you can create a Docker Compose file to handle this for you.
Create a docker-compose.yml
file somewhere on your computer with the following:
version: '2'
services:
couchbase:
image: couchbase-custom
ports:
- 8091:8091
- 8092:8092
- 8093:8093
environment:
- COUCHBASE_ADMINISTRATOR_USERNAME=Administrator
- COUCHBASE_ADMINISTRATOR_PASSWORD=password
- COUCHBASE_BUCKET=default
- COUCHBASE_BUCKET_PASSWORD=
golang:
image: golang-project
ports:
- 12345:12345
environment:
- COUCHBASE_HOST=couchbase
- COUCHBASE_BUCKET=default
restart: always
Everything in a Compose file will be on the same network. To deploy each service, execute the following:
docker-compose run -d --service-ports couchbase
docker-compose run -d --service-ports golang
If you’re familiar with Docker Compose, you’ll be familiar with docker-compose up
, but we can’t use that here. Couchbase Server doesn’t have a mechanism to tell us it is ready, so we don’t want the Golang application to try to connect before it is configured. Definitely not a big deal when it comes to container deployment.
Conclusion
You just saw how to create a Golang microservice as a Docker container that communicates to the Couchbase Server, also running within a Docker container. By turning your application into a Docker image it becomes very easy to distribute because you won’t have to worry about the environment you’re trying to deploy to. As long as the Docker Engine is available you can deploy your application. A useful example of this would be in the continuous deployment of containers using Jenkins.
Want to see how to containerize your Java application? Check out this tutorial I wrote. I also have a tutorial for containerizing a Node.js application here.
Published at DZone with permission of Nic Raboy, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments