Proxy Buffalo App with Traefik and Let’s Encrypt on DigitalOcean

You have completed your Buffalo application and are ready to deploy. You can just start your app and let the app listen for requests or you can proxy requests to your app through NGINX or Apache. A proxy server reroutes requests and responses for resources from clients to other servers

Traefik is another modern HTTP reverse proxy and load balancer that offers automatic support for SSL with Let’s Encrypt. Traefik also monitors the Docker daemon and any time a new container is started and the container configuration contains traefik.* labels, Traefik adds the container to applications that Traefik needs to proxy.

This post assumes you have an account on DigitalOcean with a droplet provisioned using CoreOS container linux. If you are using a different server or provider, most of the steps remain the same except for configuration of the generation of Let’s Encrypt certificates.

SSH Keys, IP to Domain Mapping and Auth Token

Login to your server will be via SSH keys ( ssh core@server-ip-address -i name-of-your-ssh-file); you therefore need to generate SSH keys ( ssh-keygen -t rsa) on your computer and copy the contents of the public key to your DigitalOcean server.

Your domain should be setup to use DigitalOcean‘s DNS nameservers. A mapping of your domain name to your Droplet’s IP address will be necessary and ideally setup a wildcard subdomain ( *.your-domain ) to point to your Droplet as well. You do this by either creating and A-Record or using a CNAME.

Be sure to create and store your DigitalOcean personal access token which will be used to verify that you own and control the domain. While creating access token, ensure that Write(optional) is selected. Selecting this option will enable Traefik write a TXT DNS record for your domain on the droplet. This TXT record will be used while performing ACME DNS challenge for the domain.

Installing Docker Compose

Docker Compose tool defines and manages multi-container docker services. CoreOS on DigitalOcean has Docker installed but not Docker Compose. Before installing Docker Compose, make sure you have created /opt/bin folder if it doesn’t exist. This will be your installation folder (instead of /usr/local/bin which is readonly on CoreOS) for docker compose.

sudo mkdir /opt/bin

After creating the folder, install latest Docker Compose using Alternative Install Options, Install as a container option.

sudo curl -L --fail https://github.com/docker/compose/releases/download/1.22.0/run.sh -o /opt/bin/docker-compose
sudo chmod +x /opt/bin/docker-compose

You can also use the command to update older versions of Docker Compose but remember to remove any existing images from earlier versions.

Create Docker Network

It is prudent to list available docker networks ( docker network ls) before creating a network. If the docker network is not among listed networks, create a Docker network ( docker network create external).external is the name of the of the network. You can use any name that fits your scenario. You define the network outside of docker-compose file so that you can use it for multiple docker-compose files.

Create docker network image

Prepare Traefik Setup

Create a folder ( sudo mkdir /opt/traefik ) on the Droplet/Server which will hold Traefik configurations. Within the folder, create the following three files: docker-compose.yml, acme.json and traefik.toml. Change permission for acme.json to read and write.

sudo touch /opt/traefik/docker-compose.yml 
sudo touch /opt/traefik/acme.json
sudo chmod 600 /opt/traefik/acme.json
sudo touch /opt/traefik/traefik.toml

You will require a username and an encrypted password in order to access the Traefik monitoring dashboard. Use the following command htpasswd -nb username-to-use password-for-username to generate. If the httpasswd is not installed on your system, use Htpasswd Generator to create the username and password. Try to refrain from using the simple admin/admin for the username and password. The username and generated password values will be used in Traefik configuration (traefik.toml ) file.

Traefik Configuration File

Using an editor of your choice, update /opt/traefik/traefik.tomlfile. CoreOS on DigitalOcean has Vim text editor pre-installed, but you have to open the file as a super user (sudo /opt/traefik/traefik.toml).

debug = false
loglevel = "ERROR" 
defaultEntryPoints = ["https", "http"]
[entryPoints]
[entryPoints.http]
address = ":80"
[entryPoints.http.redirect]
entryPoint = "https"
[entryPoints.https]
address = ":443"
[entryPoints.https.tls]
[entryPoints.traefik]
address = ":8080"
[entryPoints.traefik.auth.basic]
users = ["username:hashed-password",]
[api]
entryPoint = "traefik"
[retry]
[docker]
endpoint = "unix:///var/run/docker.sock"
domain = "your-domain"
watch = true
exposedByDefault = false
[acme]
email = "email-where-to-send-letencrypt-notifications"
storage = "acme.json"
onHostRule = true
entryPoint = "https"
   [acme.dnsChallenge]
provider = "digitalocean"
delayBeforeCheck = 0
   [[acme.domains]]
main = "*.your-domain"
sans = ["your-domain"]

watch = true tells Traefik to constantly watch for new (or no longer available) containers.

exposedByDefault = false makes all containers on the same network as Traefik unreachable from the outside world.

onHostRule = true tells Traefik to automatically generate certificates if backend has a valid host

Traefik Container

Update your /opt/traefik/docker-compose.yml using your favourite text editor. Like you did with traefik.toml, replace the placeholder values with actual values.

version: '3'
services:   
reverse-proxy:
image: traefik:latest
restart: always
container_name: traefik
ports:
- 80:80
- 443:443
expose:
- 8080
networks:
- external
- internal
environment:
- DO_AUTH_TOKEN=your-digitalocean-generated-api-token
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- /opt/traefik/traefik.toml:/traefik.toml
- /opt/traefik/acme.json:/acme.json
labels:
- "traefik.backend=traefik"
- "traefik.docker.network=external"
- "traefik.enable=true"
- "traefik.frontend.rule=Host:Host:traefik.your-domain"
- "traefik.port=8080"
- "traefik.frontend.headers.forceSTSHeader=true"
- "traefik.frontend.headers.STSSeconds=315360000"
- "traefik.frontend.headers.STSIncludeSubdomains=true"
- "traefik.frontend.headers.STSPreload=true"
networks:
external:
external: true
internal:

traefik.enable=true tells Traefik to expose this container to the outside world.

Launch the Traefik container

After the container complete starting up, run docker ps -a to observe the status of the Traefik container. This command should display the all running containers with their container ids. Run docker logs first-4-characters-of-container-id to see if the container encountered any errors while starting up. For example if the container id is 14c0f3f86348, the the command would be docker logs 14c0

If no errors were encountered, open your browser and navigate to http://traefik.your-domain. You will be prompted for the username and password. Use the values that you generated for username and password. This will display Traefik dashboard. You will also note that the http request has been redirected to https.

If you click on the HEALTH menu, you should observe average response times and status code counts for the running containers and other metrics.

Now that you know Traefik is working smoothly, you can deploy your Buffalo application together with any databases used by the application. Make sure your application builds without errors.

Create App Folder On Server

In order to use the default Docker Compose file (docker-compose.yml) while provisioning your application container, create a folder on the server ( mkdir /home/core/your-app-name). This will be the folder to hold files relating to your application including the docker compose file. Creating a folder for the app will also allow you to run the docker-compose up without specifying the docker configuration file.

Create Buffalo App Docker File

In your Buffalo project root, create an application docker file (app.dockerfile) that will be used to build the application docker image. The docker file will also install bash and postgresql-client packages which are not installed on DigitalOcean CoreOS droplets by default.

FROM alpine
RUN apk add --no-cache bash && apk add --no-cache ca-certificates && apk add --no-cache postgresql-client
WORKDIR /bin/
COPY your-app-img .

ENV GO_ENV=production
ENV ADDR=0.0.0.0
EXPOSE 3000
CMD /bin/wait-for-postgres.sh; /bin/your-app migrate; /bin/your-app

Buffalo App and Database Docker Compose File

Next you will create a docker compose file (docker-compose.yml) for provisioning Postgresql and your Buffalo application containers.

version: '3'
services:   
your-app-db:
image: postgres:11-alpine
container_name: your-app-db
environment:
POSTGRES_USER: postgres
POSTGRES_DB: your-app-db
POSTGRES_PASSWORD:db-pwd
expose:
- 5432
restart: always
networks:
- internal
volumes:
- postgres_data:/var/lib/postgresql/data
labels:
- "traefik.enable.false"

your-app:
depends_on:
- your-app-db
image: your-app-img
container_name: your-app
restart: always
environment:
- SESSION_SECRET=B4268...-D189754C8A9F
- DATABASE_URL="postgres://postgres:db-pwd@your-app-db:5432/your-app-db?sslmode=disable"
expose:
- 3000
networks:
- external
- internal
labels:
- "traefik.enabled=true"
- "traefik.backend=your-app"
- "traefik.frontend.rule=Host:your-app.your-domain"
- "traefik.network=external"
- "traefik.port=3000"
- "traefik.frontend.headers.forceSTSHeader=true"
- "traefik.frontend.headers.STSSeconds=315360000"
- "traefik.frontend.headers.STSIncludeSubdomains=true"
- "traefik.frontend.headers.STSPreload=true"
volumes:
postgres_data:
networks:
external:
external: true
internal:

POSTGRES_DB: your-app-db is the environment variable for the default database to be created if it does not exist.

traefik.frontend.headers.* labels will ensure that our application domain or sub-domain scores a grade A+ when tested on SSL Labs.

Wait For PostgreSQL Startup Script

When the containers are provisioned, the Buffalo application will need to connect to the database. It may be possible that by the time a connection is being requested, the database container has not completed starting up. You can force the application to wait until the database is ready by creating your own wait-for-PostgresSQL startup script (wait-for-postgres.sh) or use the one below. After creating the script file, assign execute permissions (chmod +x wait-for-postgres.sh ) to the script file.

#!/bin/sh
# wait-for-postgres.sh
# Using Docker Compose Entrypoint To Check if Postgres is Running
# https://bit.ly/2KCdFxh
# Author: Kelly Andrews
set -e
cmd="$@"
# pg_isready is a postgreSQL client tool for checking the connection
# status of PostgreSQL server
while ! pg_isready -h your
-app-db -p 5432 -U postgres > /dev/null 2> /dev/null; do
echo "Connecting to postgres Failed"
sleep 1
done
>&2 echo "Postgres is up - executing command"
exec $cmd

Build and Upload Docker Images Script

All you need now is to build your Buffalo app, create a Docker image for your app, upload the image to your DigitalOcean droplet and then provision your app using the docker compose file. You can automate these steps by creating a script file (build-and-upload.sh) to perform these tasks and then run the script from the command terminal. After creating the script file, remember to assign execute permissions (chmod +x build-and-upload.sh) to the script. In the script, replace placeholder values with actual values pertaining to your application.

#!/bin/bash
# build-and-upload.sh
clear
echo "Building Buffalo app image ..."
docker rmi -f your-app-img 2>/dev/null
env GOOS=linux GOARCH=386 buffalo build -o your-app
docker build --no-cache -t your-app-img -f app.Dockerfile .
rm your-app
echo "saving and compressing docker image ..."
docker save your-app-img | bzip2 > your-app-latest.tar.bz2

echo "uploading image and docker compose file to the server ..."
scp -i your-ssh-key your-app-latest.tar.bz2 docker-compose.yml core@your-droplet-ip:/home/core/your-app
ssh -i your-ssh-key core@your-droplet-ip << ENDSSH
cd /home/core/your-app
bunzip2 --stdout your-app-latest.tar.bz2 | docker load
rm your-app-latest.tar.bz2

docker-compose down && docker-compose up -d --force-recreate;
ENDSSH

Run ./build-and-upload.sh to build and upload your Buffalo app image to your server.

If you navigate to the Traefik dashboard site, you will note that Traefik has already added your app to the dashboard.

Congratulations! Your site your-app.your-domain is now running on HTTPS and scores a A+ grade on SSL Labs. The beauty is Traefik will automatically renew your SSL certificates when they are about to expire.

The End

That’s it. You have completed the process of proxying your Buffalo app using Traefik and Let’s Encrypt.

Resources