The author selected the Free and Open Source Fund to receive a donation as part of the Write for DOnations program.
Docker is the most common containerization software used today. It enables developers to easily package apps along with their environments, which allows for quicker iteration cycles and better resource efficiency, while providing the same desired environment on each run. Docker Compose is a container orchestration tool that facilitates modern app requirements. It allows you to run multiple interconnected containers at the same time. Instead of manually running containers, orchestration tools give developers the ability to control, scale, and extend a container simultaneously.
The benefits of using Nginx as a front-end web server are its performance, configurability, and TLS termination, which frees the app from completing these tasks. The nginx-proxy
is an automated system for Docker containers that greatly simplifies the process of configuring Nginx to serve as a reverse proxy. Its Let’s Encrypt add-on can accompany the nginx-proxy
to automate the generation and renewal of certificates for proxied containers.
In this tutorial, you will deploy an example Go web application with gorilla/mux as the request router and Nginx as the web server, all inside Docker containers, orchestrated by Docker Compose. You’ll use nginx-proxy
with the Let’s Encrypt add-on as the reverse proxy. At the end of this tutorial, you will have deployed a Go web app accessible at your domain with multiple routes, using Docker and secured with Let’s Encrypt certificates.
sammy
.your_domain
throughout. You can get one for free on Freenom, or use the domain registrar of your choice.your_domain
pointing to your server’s public IP address. You can follow this introduction to DigitalOcean DNS for details on how to add them.In this step, you will set up your workspace and create a simple Go web app, which you’ll later containerize. The Go app will use the powerful gorilla/mux request router, chosen for its flexibility and speed.
For this tutorial, you’ll store all data under ~/go-docker
. Run the following command to do this:
- mkdir ~/go-docker
Navigate to it:
- cd ~/go-docker
You’ll store your example Go web app in a file named main.go
. Create it using your text editor:
- nano main.go
Add the following lines:
package main
import (
"fmt"
"net/http"
"github.com/gorilla/mux"
)
func main() {
r := mux.NewRouter()
r.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "<h1>This is the homepage. Try /hello and /hello/Sammy\n</h1>")
})
r.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "<h1>Hello from Docker!\n</h1>")
})
r.HandleFunc("/hello/{name}", func(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
title := vars["name"]
fmt.Fprintf(w, "<h1>Hello, %s!\n</h1>", title)
})
http.ListenAndServe(":80", r)
}
You first import net/http
and gorilla/mux
packages, which provide HTTP server functionality and routing.
The gorilla/mux
package implements an easier and more powerful request router and dispatcher, while at the same time maintaining interface compatibility with the standard router. Here, you instantiate a new mux
router and store it in variable r
. Then, you define three routes: /
, /hello
, and /hello/{name}
. The first (/
) serves as the homepage and you include a message for the page. The second (/hello
) returns a greeting to the visitor. For the third route (/hello/{name}
) you specify that it should take a name as a parameter and show a greeting message with the name inserted.
At the end of your file, you start the HTTP server with http.ListenAndServe
and instruct it to listen on port 80
, using the router you configured.
Save and close the file.
Before running your Go app, you first need to compile and pack it for execution inside a Docker container. Go is a compiled language, so before a program can run, the compiler translates the programming code into executable machine code.
You’ve set up your workspace and created an example Go web app. Next, you will deploy nginx-proxy
with an automated Let’s Encrypt certificate provision.
It’s important that you secure your app with HTTPS. To accomplish this, you’ll deploy nginx-proxy
via Docker Compose, along with its Let’s Encrypt add-on. This secures Docker containers proxied using nginx-proxy
, and takes care of securing your app through HTTPS by automatically handling TLS certificate creation and renewal.
You’ll be storing the Docker Compose configuration for nginx-proxy
in a file named nginx-proxy-compose.yaml
. Create it by running:
- nano nginx-proxy-compose.yaml
Add the following lines to the file:
version: '2'
services:
nginx-proxy:
restart: always
image: jwilder/nginx-proxy
ports:
- "80:80"
- "443:443"
volumes:
- "/etc/nginx/vhost.d"
- "/usr/share/nginx/html"
- "/var/run/docker.sock:/tmp/docker.sock:ro"
- "/etc/nginx/certs"
letsencrypt-nginx-proxy-companion:
restart: always
image: jrcs/letsencrypt-nginx-proxy-companion
volumes:
- "/var/run/docker.sock:/var/run/docker.sock:ro"
volumes_from:
- "nginx-proxy"
Here you’re defining two containers: one for nginx-proxy
and one for its Let’s Encrypt add-on (letsencrypt-nginx-proxy-companion
). For the proxy, you specify the image jwilder/nginx-proxy
, expose and map HTTP and HTTPS ports, and finally define volumes that will be accessible to the container for persisting Nginx-related data.
In the second block, you name the image for the Let’s Encrypt add-on configuration. Then, you configure access to Docker’s socket by defining a volume and then the existing volumes from the proxy container to inherit. Both containers have the restart
property set to always
, which instructs Docker to always keep them up (in the case of a crash or a system reboot).
Save and close the file.
Deploy the nginx-proxy
by running:
- docker-compose -f nginx-proxy-compose.yaml up -d
Docker Compose accepts a custom named file via the -f
flag. The up
command runs the containers, and the -d
flag, detached mode, instructs it to run the containers in the background.
Your final output will look like this:
OutputCreating network "go-docker_default" with the default driver
Pulling nginx-proxy (jwilder/nginx-proxy:)...
latest: Pulling from jwilder/nginx-proxy
a5a6f2f73cd8: Pull complete
2343eb083a4e: Pull complete
...
Digest: sha256:619f390f49c62ece1f21dfa162fa5748e6ada15742e034fb86127e6f443b40bd
Status: Downloaded newer image for jwilder/nginx-proxy:latest
Pulling letsencrypt-nginx-proxy-companion (jrcs/letsencrypt-nginx-proxy-companion:)...
latest: Pulling from jrcs/letsencrypt-nginx-proxy-companion
...
Creating go-docker_nginx-proxy_1 ... done
Creating go-docker_letsencrypt-nginx-proxy-companion_1 ... done
You’ve deployed nginx-proxy
and its Let’s Encrypt companion using Docker Compose. Next, you’ll create a Dockerfile for your Go web app.
In this section, you will create a Dockerfile containing instructions on how Docker will create an immutable image for your Go web app. Docker builds an immutable app image—similar to a snapshot of the container—using the instructions found in the Dockerfile. The image’s immutability guarantees the same environment each time a container, based on the particular image, is run.
Create the Dockerfile
with your text editor:
- nano Dockerfile
Add the following lines:
FROM golang:alpine AS build
RUN apk --no-cache add gcc g++ make git
WORKDIR /go/src/app
COPY . .
RUN go mod init webserver
RUN go mod tidy
RUN GOOS=linux go build -ldflags="-s -w" -o ./bin/web-app ./main.go
FROM alpine:3.13
RUN apk --no-cache add ca-certificates
WORKDIR /usr/bin
COPY --from=build /go/src/app/bin /go/bin
EXPOSE 80
ENTRYPOINT /go/bin/web-app --port 80
This Dockerfile has two stages. The first stage uses the golang:alpine
base, which contains pre-installed Go on Alpine Linux.
Then you install gcc
, g++
, make
, and git
as the necessary compilation tools for your Go app. You set the working directory to /go/src/app
, which is under the default GOPATH. You also copy the content of the current directory into the container. The first stage concludes with recursively fetching the packages used from the code and compiling the main.go
file for release without symbol and debug info (by passing -ldflags="-s -w"
). When you compile a Go program it keeps a separate part of the binary that would be used for debugging, however, this extra information uses memory, and is not necessary to preserve when deploying to a production environment.
The second stage bases itself on alpine:3.13
(Alpine Linux 3.13). It installs trusted CA certificates, copies the compiled app binaries from the first stage to the current image, exposes port 80
, and sets the app binary as the image entry point.
Save and close the file.
You’ve created a Dockerfile for your Go app that will fetch its packages, compile it for release, and run it upon container creation. In the next step, you will create the Docker Compose yaml
file and test the app by running it in Docker.
Now, you’ll create the Docker Compose config file and write the necessary configuration for running the Docker image you created in the previous step. Then, you will run it and check if it works correctly. In general, the Docker Compose config file specifies the containers, their settings, networks, and volumes that the app requires. You can also specify that these elements can start and stop as one at the same time.
You will be storing the Docker Compose configuration for the Go web app in a file named go-app-compose.yaml
. Create it by running:
- nano go-app-compose.yaml
Add the following lines to this file:
version: '2'
services:
go-web-app:
restart: always
build:
dockerfile: Dockerfile
context: .
environment:
- VIRTUAL_HOST=your_domain
- LETSENCRYPT_HOST=your_domain
Remember to replace your_domain
both times with your domain name. Save and close the file.
This Docker Compose configuration contains one container (go-web-app
), which will be your Go web app. It builds the app using the Dockerfile you’ve created in the previous step, and takes the current directory, which contains the source code, as the context for building. Furthermore, it sets two environment variables: VIRTUAL_HOST
and LETSENCRYPT_HOST
. nginx-proxy
uses VIRTUAL_HOST
to know from which domain to accept the requests. LETSENCRYPT_HOST
specifies the domain name for generating TLS certificates, and must be the same as VIRTUAL_HOST
, unless you specify a wildcard domain.
Now, you’ll run your Go web app in the background via Docker Compose with the following command:
- docker-compose -f go-app-compose.yaml up -d
Your final output will look like the following:
OutputCreating network "go-docker_default" with the default driver
Building go-web-app
Step 1/12 : FROM golang:alpine AS build
---> b97a72b8e97d
...
Successfully tagged go-docker_go-web-app:latest
WARNING: Image for service go-web-app was built because it did not already exist. To rebuild this image you must use `docker-compose build` or `docker-compose up --build`.
Creating go-docker_go-web-app_1 ... done
If you review the output presented after running the command, Docker logged every step of building the app image according to the configuration in your Dockerfile.
You can now navigate to https://your_domain/
to see your homepage. At your web app’s home address, you’re seeing the page as a result of the /
route you defined in the first step.
Now navigate to https://your_domain/hello
. You will see the message you defined in your code for the /hello
route from Step 1.
Finally, try appending a name to your web app’s address to test the other route, like: https://your_domain/hello/Sammy
.
Note: In the case that you receive an error about invalid TLS certificates, wait a few minutes for the Let’s Encrypt add-on to provision the certificates. If you are still getting errors after a short time, double check what you’ve entered against the commands and configuration shown in this step.
You’ve created the Docker Compose file and written configuration for running your Go app inside a container. To finish, you navigated to your domain to check that the gorilla/mux
router setup is serving requests to your Dockerized Go web app correctly.
You have now successfully deployed your Go web app with Docker and Nginx on Ubuntu 18.04. With Docker, maintaining applications becomes less of a hassle, because the environment the app is executed in is guaranteed to be the same each time it’s run. The gorilla/mux package has excellent documentation and offers more sophisticated features, such as naming routes and serving static files. For more control over the Go HTTP server module, such as defining custom timeouts, visit the official docs.
Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.
This textbox defaults to using Markdown to format your answer.
You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!
From a snyk scan it seems the latest version of jwilder/nginx-proxy is riddled with security bugs. There are 5 critical and 5 high severity vulnerabilities related to the bullseye dependency. Not sure if any/all of these are false positives, but might be worth considering setting up your own image.
How can i deploy on an ip instead of domain ?
If you are having a problem where the command line doesn’t give you errors, but your website still isn’t secure, you might have checked out too many Let’s Encrypt duplicate certificates for the week. ( https://letsencrypt.org/docs/rate-limits/ )
You can check how many certificates you’ve gotten using this website: https://crt.sh/
If you have checked out too many certificates you can retry next week or add/remove domain names in your go-app-compose.yaml file. For example: environment: - VIRTUAL_HOST= www.example1.com, www.example2.com - LETSENCRYPT_HOST= www.example1.com, www.example2.com
Also, after you’ve had this problem, you might get a 502 bad gateway error on your computer when you visit your website. If you do, try using another device to go to your website or clear all your cookies on your computer: https://www.keycdn.com/support/502-bad-gateway
Hello, how can I make Docker recompile my Programm? When i run “docker-compose -f go-app-compose.yaml up -d” my Website doesnt change…
This tutorial is broken once you try to run the go dockerfile.
I’m not familiar with Go at all, but this SO post said something about go-wrapper being deprecated.
A working fix is to replace the first line in the Dockerfile with:
However, someone more experienced with GO should probably suggest a more up-to-date solution.
Hello,
Thank you for this clear and simple documentation. I followed instructions and it worked!
I just wonder about something. For example I have 2 applications. One of them is production second of them development.
I want to serve first one in www.example.com and second one in dev.example.com.
I tried but console returns ‘Bind for 0.0.0.0:443 failed: port is already allocated’.
I don’t want to serve another port I want to serve both of them in 80 and 443.
Can you help me with that? Maybe you can give me some keyword for search it on google or something.
Thank you very muck again this simple and clear documentation.
Thanks for the article. I was able to make it work without much complication. It was helpful. I do have a question though:
Presumably the Let’s Encrypt container that’s already running needs to see that LETSENCRYPT_HOST=example.com that you specify only later in the process. How does it obtain that? (I’m just asking for a bit more context on what is going on here.)
I believe the answer is that the code in the two containers that are already running are Docker-aware and use docker.sock to monitor the addition of additional containers on the host. When they see a new container added, they check if the new container has “env” variables set. If so, they read them and if those variable suggest that the new container would like LETSENCRYPT proxying, those containers start to do that.
Did I get that right?
This is not a very well made tutorial, at least add how to remove the stuff u brought up ;(
All other things being equal, I want to serve files from a public directory I placed in the same level with the main.go file. Then in the main.go, this is my new main function:
But I now get the 404 page after running docker-compose. Please help. I thought the
line in the Dockerfile copies the project file listing as is. And too, there is an index.html in the public folder
I’m getting a “500 Internal Server Error - nginx/1.17.3” when I try to access the url via HTTPS. I’ve exposed port 443 (i’m on AWS). Any idea why it wouldn’t work?