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:
Details
Setup of Linode server
OS
Ubuntu 20.04 LTS
Docker
Docker Compose
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
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
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
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"]
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
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' *"
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"
Top comments (0)