How I put my demo into production with Docker compose and Traefik
Published: 2021-02-08 06:35:49

## Overview
In my previous [article](https://dev.to/gunstein/send-and-receive-payments-with-the-stellar-cryptocurrency-4ne8) I described my simple demo-application, StellarGallery. A simple frontend for an art gallery where users can buy art using stellar cryptocurrency. The backend keeps track of orders and let customers download a high resolution image when payment is received.  
This article is about what my production architecture looks like and how I build my docker containers. Even though the domain is a little bit uncommon, the production architecture should be common. A React SPA and a Golang Server.

Keywords: Docker, Docker Compose, Traefik( https, Letsencrypt, redirect port 80->443, gzip compression), substitution of api url in client image at container startup.

## Links
<!-- * [The demo](https://gallery.vatnar.no)
* [Client CodeSandbox](https://codesandbox.io/s/great-chebyshev-8fplu) -->
* [Previous article](https://dev.to/gunstein/send-and-receive-payments-with-the-stellar-cryptocurrency-4ne8)
* [Client on Github](https://github.com/gunstein/stellar_testing/tree/main/stellar_art_gallery/client/art_gallery)
* [Server on Github](https://github.com/gunstein/stellar_testing/tree/main/stellar_art_gallery/server_stellar_gallery)

## Concept
In earlier projects I would have Dockerhub do the builds automatically after github updates, but this is now a paid option at Dockerhub so here I use a more manual approach.
I build docker images on my laptop and push them to DockerHub. I'm not planning on frequent updates and it's just a demo so I think its okay for now.
My production server is a Linode machine running Ubuntu. I will describe the setup details in the Details section below.

Figure showing the container building process:
![drawing](https://raw.githubusercontent.com/gunstein/stellar_testing/main/stellar_gallery_docker_build.jpg)


## Details
### Setup of Linode server
#### OS
Ubuntu 20.04 LTS
#### Docker
[Followed these instructions](https://docs.docker.com/engine/install/ubuntu/)
#### Docker Compose
[Followed these instructions](https://docs.docker.com/compose/install/)

### Client Dockerfile
The Dockerfile for the React client is pretty common, I think.  
[Multi-stage build](https://docs.docker.com/develop/develop-images/multistage-build/) to make the build as small as possible.

```Text
#Build Stage Start

#Specify a base image
FROM node:alpine as builder

#Specify a working directory
WORKDIR '/app'

#Copy the dependencies file
COPY package.json .

#Install dependencies
RUN npm install

#Copy remaining files
COPY . .

#Build the project for production
RUN npm run build

#Run Stage Start
FROM nginx

#Copy production build files from builder phase to nginx
COPY --from=builder /app/build /usr/share/nginx/html

EXPOSE 80
```
This is how I build my Dockerfile and push it to DockerHub:
```Bash
docker build -t client_stellar_gallery .
docker tag client_stellar_gallery:latest gunstein/client_stellar_art_gallery:latest
docker push gunstein/client_stellar_art_gallery:latest
```

### Server Dockerfile
Pretty plain Dockerfile for Go, I guess, but I want to mention some hassle I had.  
Originally I wrote the server to use SQLite as database. It worked well when run on my laptop, but gave me plenty of trouble when run in a Docker container on my Linode server. To build a container with SQLite I had to make a static build. My understanding is that this is because SQLite needs glibc. I used this build command in my Dockerfile:  
```Text
RUN go build -a -ldflags "-linkmode external -extldflags '-static' -s -w" -o server_stellar_gallery main.go
```
The build command worked ok, but my server crashed at the strangest places when run in the container. I think the crashes were caused by some glibc conflicts.  
In the end I gave up and converted to PostgreSQL. The ORM I'm using is [Gorm](https://gorm.io/index.html) which is using the [pgx](https://github.com/jackc/pgx), a pure Go driver for PostgreSQL. This worked like a charm. To me, it seems like a good rule to avoid static builds if possible.

I also want to explain the last line in my Dockerfile, CMD ["--account_public_key"]. The string is just a placeholder and is supposed to be overridden in the Docker-compose.yml file.  

```Text
FROM golang:latest as builder

WORKDIR /app
COPY . .

RUN go get -d -v ./...
RUN go install -v ./...

RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o server_stellar_gallery .

#FROM scratch
FROM centos:latest
COPY --from=builder /app/server_stellar_gallery /

# Copy CA certificates to prevent x509: certificate signed by unknown authority errors
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt

EXPOSE 5000
ENTRYPOINT ["/server_stellar_gallery"]
CMD ["--account_public_key"]
```
This is how I build my Dockerfile and push it to DockerHub:
```Bash
docker build -t server_stellar_gallery . 
docker tag server_stellar_gallery:latest gunstein/server_stellar_art_gallery:latest
docker push gunstein/server_stellar_art_gallery:latest
```

### Docker-compose
I would have thought my docker-compose.yml file to be an ordinary one, but I must admit that I had some trouble creating it and especially the stuff involving [Traefik](https://traefik.io/) was a challenge. It seems to me that configuring Traefik has evolved a bit and this must be considered when looking at examples.
I chose to set up redirection from port 80->443 and connecting middleware for compression to the frontend. This seems to be considered best practice for frontends. At least [Lighthouse](https://developers.google.com/web/tools/lighthouse) gives you credit for implementing these things.

I really like Traefik. They seem to have achieved their vision of simplifying networking. From a programmers point of view it's pleasant to be released of all the burden setting up for example https, letsencrypt, port redirection and response-compression.

One last thing to mention:
After docker-compose up -d is run I have to substitute the placeholder __API_URL__ in the client build. (The placeholder exists in the .env file in the client project.)
I use this command:
```bash
docker-compose exec -w /usr/share/nginx/html/static/js client_stellar_art_gallery bash -c "sed -i 's,__API_URL__,https://galleryapi.vatnar.no,g' *"
```
I never found a way to run this command in the docker.compose.yml file so instead I made a script first running docker-compose up -d and then the command above. It's working, but it feels like the last command could be set up in the docker-compose.yml file.

Here's my docker-compose.yml file:
```yaml
version: "3.8"

services:
  traefik:
    image: "traefik:v2.4"
    container_name: "traefik"
    command:
      # - "--log.level=DEBUG"
      # - "--api.insecure=true"
      - "--providers.docker=true"
      - "--providers.docker.exposedbydefault=false"
      - "--entrypoints.websecure.address=:443"
      - "--certificatesresolvers.myresolver.acme.tlschallenge=true"
      - "--certificatesresolvers.myresolver.acme.email=my_email@email.com"
      - "--certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json"
      # redirect port 80 -> 443
      - "--entrypoints.web.address=:80"
      - "--entrypoints.web.http.redirections.entryPoint.to=websecure"
      - "--entrypoints.web.http.redirections.entryPoint.scheme=https"
      - "--entrypoints.web.http.redirections.entrypoint.permanent=true"
    ports:
      - "443:443"
      - "80:80"
      # - "8080:8080"
    volumes:
      - "./letsencrypt:/letsencrypt"
      - "/var/run/docker.sock:/var/run/docker.sock:ro"

  client_stellar_art_gallery:
    image: gunstein/client_stellar_art_gallery:latest
    container_name: "client_stellar_art_gallery"
    restart: always
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.client_stellar_art_gallery.rule=Host(`gallery.vatnar.no`)"
      - "traefik.http.routers.client_stellar_art_gallery.entrypoints=websecure"
      - "traefik.http.routers.client_stellar_art_gallery.tls.certresolver=myresolver"
      # use compression
      - "traefik.http.routers.client_stellar_art_gallery.middlewares=test-compress"
      - "traefik.http.middlewares.test-compress.compress=true"

  postgres:
    image: postgres
    container_name: "postgres"
    restart: always
    environment:
      POSTGRES_PASSWORD: postgres
    volumes:
      - ./dbdata:/var/lib/postgresql/data

  server_stellar_art_gallery:
    image: gunstein/server_stellar_art_gallery:latest
    container_name: "server_stellar_art_gallery"
    depends_on:
      - "postgres"
    restart: always
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.server_stellar_art_gallery.rule=Host(`galleryapi.vatnar.no`)"
      - "traefik.http.routers.server_stellar_art_gallery.entrypoints=websecure"
      - "traefik.http.routers.server_stellar_art_gallery.tls.certresolver=myresolver"
    command: "-account=GBGJFGCDZHQ3LXJOUK7EOZB77OR2GMES3FVQRK4M724THUDLZLP7J6A7"

```