DEV Community

Gunstein Vatnar
Gunstein Vatnar

Posted on • Edited on

How I put my demo into production with Docker compose and Traefik

Overview

In my previous article 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

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

Details

Setup of Linode server

OS

Ubuntu 20.04 LTS

Docker

Followed these instructions

Docker Compose

Followed these instructions

Client Dockerfile

The Dockerfile for the React client is pretty common, I think.

Multi-stage build to make the build as small as possible.

#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
Enter fullscreen mode Exit fullscreen mode

This is how I build my Dockerfile and push it to DockerHub:

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
Enter fullscreen mode Exit fullscreen mode

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:

RUN go build -a -ldflags "-linkmode external -extldflags '-static' -s -w" -o server_stellar_gallery main.go
Enter fullscreen mode Exit fullscreen mode

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 which is using the 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.

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"]
Enter fullscreen mode Exit fullscreen mode

This is how I build my Dockerfile and push it to DockerHub:

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
Enter fullscreen mode Exit fullscreen mode

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 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 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:

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' *"
Enter fullscreen mode Exit fullscreen mode

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:

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"

Enter fullscreen mode Exit fullscreen mode

Top comments (0)