Functions as a Service With SSL Using OpenFaaS, Docker Swarm, Traefik 2 and a Socket Proxy
Tags: docker, go, serverless
Published: 2020-12-09 18:14:31

---
title: Functions as a Service With SSL Using OpenFaaS, Docker Swarm, Traefik 2 and a Socket Proxy
published: true
date: 2020-09-30 20:00:00 UTC
tags: docker, go, serverless
cover_image: https://images.unsplash.com/photo-1518260991123-9a5cfe81eecf?crop=entropy&cs=tinysrgb&fit=crop&fm=jpg&h=420&ixlib=rb-1.2.1&q=80&w=1000
canonical_url: https://www.rockyourcode.com/functions-as-a-service-with-ssl-using-openfaas-docker-swarm-and-traefik-2-and-a-socket-proxy/
---


> Self-deployed FaaS with Docker Swarm

Serverless is all the rage right now. Instead of maintaining a server and its infrastructure, you can create self-contained functions that do the job.

This can be a boon for front-end developers. A bespoke front-end client written in React.js or another framework can easily be enhanced with back-end code.

It's now trivial to add a secure integration with a payment provider like Stripe.  
You don't have to write, deploy and maintain separate back-end code (e.g., a Node.js and Express.js server).

One of the often-touted advantages of FaaS (Functions as a service) is the price. You only pay for what you're using.

Some providers, like Netlify, even offer a generous free tier.

But what happens when you want to avoid lock-in?

That's where **[OpenFaaS][openfaas]** comes in. OpenFaaS is an open-source framework for functions-as-a-service, using Docker Swarm, Kubernetes or OpenShift.

We can deploy OpenFaaS to a VPS (Virtual Private Server) provider like [Upcloud][upcloud][^1] or [DigitalOcean][doreferral][^1]. A small instance, which costs around $3 to $5 US dollars, is enough for a self-hosted FaaS setup.

In this post I will show you how to use Docker Swarm to deploy an OpenFaaS cluster. We'll also use **[Traefik 2][traefik]** to provision SSL certificates for our functions _for free_.  

## Prerequisites

You will need a running cloud server.

I particularly like Hetzner and Upcloud, but you can use what you like.

[Upcloud][upcloud][^1] has top-notch performance for the 5-dollar-tier and a friendly customer support.

[Hetzner][hetzner] has very cheap offerings under USD \$3. Their smallest offering is not very performant, but sufficient for our use case. Keep in mind that Hetnzer's servers are located in Germany and Finland.

For alternatives, check [VPSBenchmarks](https://www.vpsbenchmarks.com/).

Provision an **Ubuntu 20.04** server, [create and connect an SSH key](https://upcloud.com/community/tutorials/use-ssh-keys-authentication/), and don't forget [about basic security](https://upcloud.com/community/tutorials/secure-linux-cloud-server/).

You'll also want to have a domain. [Namecheap](https://www.namecheap.com/) is one of the services I use for buying domains, and so far I've been satisfied with them.

You need to find a way to connect your domain to the cloud server you just spun up. How you do it depends on your domain registrar and your VPS provider.

You'll need 3 subdomains for the following services: Traefik dashboard, OpenFaaS dashboard, Prometheus dashboard.

Let's say your domain name is `myfaas.xyz`.  
Create A records for `traefik.myfaas.xyz`, `faas.myfaas.xyz` and `prometheus.myfaas.xzy`.  
(The names for the subdomains are up to you, of course).

Here's a [guide that might help you out](https://upcloud.com/community/stories/domain-name-system/).

## Software Installation

I suggest using a [non-root user with sudo access](https://www.vultr.com/docs/setup-a-non-root-user-with-sudo-access-on-ubuntu) instead of the default root user.

SSH into your cloud server and install the necessary packages:

```bash
# Install Docker (https://docs.docker.com/engine/install/ubuntu/)
sudo DEBIAN_FRONTEND=noninteractive apt -y install \
  apt-transport-https \
  ca-certificates \
  curl \
  git \
  gnupg-agent \
  software-properties-common
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io
```

Give the current user the necessary permissions:

```bash
sudo groupadd -f docker
sudo usermod -aG docker "${USER}"
```

Log out of the session, so that the changes to can take place.

## Install OpenFaaS and `faas-cli`

We'll be using **Docker Swarm** for deployment.

The default option is Kubernetes because of its richer ecosystem. But right now, I have no clue how Kubernetes works. And this guide shows you a setup for hobby projects, so Kubernetes would be overkill anyway.  
I'm familiar with Docker, so Docker Swarm it is.

You can also use **[faasd][faasd]**, a simpler alternative. faasd uses [containerd](https://containerd.io/) instead of Docker.  
But although Docker _uses_ containerd, they [are not the same][containerd_docker].  
That means that automatic SSL certificate generation via Traefik 2 _does not work_. Traefik works with a few providers like Docker or Kubernetes, [but not with bare-bones containerd](https://doc.traefik.io/traefik/providers/overview/).

---

_Edit:_ What you _can_ do is add [Terraform](https://www.terraform.io/) (infrastructure as code) and use it to install [Caddy](https://caddyserver.com/) (a server) to provision SSL certificates. Guide [here](https://www.openfaas.com/blog/faasd-tls-terraform/).  
Thanks to [Alex Ellis](https://twitter.com/alexellisuk), creator of OpenFaaS & faasd, for pointing that out.

---

We'll now use the **[official deployment guide for Docker Swarm on the OpenFaaS docs][openfaasswarm]**.

### 1. Install `faas-cli`

```bash
curl -sL https://cli.openfaas.com | sudo sh
```

### 2. Docker Swarm Setup

In preparation for the Traefik 2 setup, we'll create the necessary environment variables. The following section is more or less a copy of the [guide from dockerswarm.rocks][dockerswarmrocks].

```bash
docker swarm init
```

We will use the **[docker socket proxy by Tecnativa][tecnativa]** to protect our Docker unix socket.  
The socket proxy is a security measure that's [encouraged by Traefik](https://doc.traefik.io/traefik/providers/docker/#docker-api-access).

Get the Swarm node ID of the current node and store it in an environment variable:

```bash
export NODE_ID=$(docker info -f '{{.Swarm.NodeID}}')
```

Now we'll add a tag to the node to ensure that we'll always deploy to the same node. Why?  
We need a way to ensure that the SSL certificates are available to the Docker node that runs Traefik (which handles the provision of the certificates).

```bash
docker node update --label-add traefik-public.traefik-certificates=true $NODE_ID
```

Now we need to find a way to create an admin username and password for both the Traefik admin dashboard and the Prometheus monitoring service.  
OpenFaaS will automatically provide basic auth for the functions dashboard.

In my example, I will use environment variables. You can also use [docker secrets](https://docs.docker.com/engine/swarm/secrets/), which is a more secure approach. But it's also trickier to set up and harder to change as Docker secrets are immutable.

```bash
export TRAEFIK_ADMINS=admin:$(openssl passwd -apr1)
```

Here, `admin` is your desired username for login. Change it to something else if you like. You will be prompted for a password. The command will hash the password for you.

We'll do the same for the Prometheus dashboard:

```bash
export PROMETHEUS_ADMINS=admin:$(openssl passwd -apr1)
```

We will use [Let's Encrypt](https://letsencrypt.org/) to generate SSL certificates. Provide an email as unique identifier:

```bash
export EMAIL=myemail@email.com
```

Next up, we'll add the domains for the services. Assuming we want to deploy the Traefik dashboard to `traefik.myfaas.xyz`, we create the environment variable like this:

```bash
export TRAEFIK_DOMAIN=traefik.myfaas.xyz
```

Don't forget the Prometheus dashboard and also the domain for the functions:

```bash
export PROMETHEUS_DOMAIN=prometheus.myfaas.xyz
export FAAS_DOMAIN=functions.myfaas.xyz
```

## 3. Download OpenFaaS

You should be in the home directory of your (non-root) user on your VPS.  
Let's use `git` to get a copy of OpenFaaS:

```bash
git clone https://github.com/openfaas/faas && cd faas
```

## 4. Prepare for Traefik and Docker Socket Proxy

The OpenFaaS stack provides you with a `docker-compose.yml`. We now need to tweak it for our needs.

This step is quite tricky, because it's easy to get indentation errors when editing yaml files.

First, open the ready-made `docker-compose.yml` file and take a look around. You will find several services for the OpenFaaS gateway, Prometheus, etc.

### 4.1. Adjust Existing Configuration

The default network is `functions`. We'll need to add another network for the services that Traefik should make public (the Traefik dashboard, the Prometheus dashboard and the OpenFaaS dashboard).

Find the `networks` section at the end of the file and add a new network. Also add a volume forThe SSL certificates:

```diff
networks:
+    traefik-public:
+        driver: overlay
+        attachable: true
    functions:
        driver: overlay
        attachable: true
        labels:
            - "openfaas=true"

+volumes:
+    traefik-public:
```

Add the `traefik-public` network to all service that we can reach via the internet: `gateway`, `prometheus`, and later `traefik`.

Also remove the external facing ports from the services `gateway` and `prometheus`. Traefik will handle that for us:

```diff
  gateway:
        image: openfaas/gateway:0.18.18
-        ports:
-            - 8080:8080
        networks:
             - functions
+            - traefik-public

...


  prometheus:
      image: prom/prometheus:v2.11.0
      ...
-      ports:
-          - 9090:9090
      networks:
           - functions
+          - traefik-public
```

Let's now setup Traefik and the Docker socket proxy services:

```diff
services:
+  socket-proxy:
+    image: 'tecnativa/docker-socket-proxy:latest'
+    environment:
+      # required permissions
+      NETWORKS: 1
+      SERVICES: 1
+      TASKS: 1
+      POST: 1
+    deploy:
+      restart_policy:
+        condition: on-failure
+      placement:
+        constraints:
+          - node.role == manager
+    networks:
+      - traefik-public
```

Let's use the security-enhanced socket in the `faas-swarm` service:

```diff
  faas-swarm:
    image: 'openfaas/faas-swarm:0.9.0'
    networks:
      - functions
+     - traefik-public
    environment:
      read_timeout: 5m5s
      write_timeout: 5m5s
      DOCKER_API_VERSION: '1.30'
+     DOCKER_HOST: 'tcp://traefik-public:2375'
      basic_auth: '${BASIC_AUTH:-false}'
```

The Traefik setup is significantly more complicated. Explaining all the moving parts is beyond the scope of this post.

I suggest these [two][guide1] [posts][guide2] and the [official Traefik documentation][traefik] to get started.

```diff
+    traefik:
+        image: traefik:v2.2
+        command:
+          - --entrypoints.web.address=:80
+          - --entrypoints.websecure.address=:443
+          - --providers.docker=true
+          - --providers.docker.swarmMode=true
+          - --providers.docker.network=faas_traefik-public
+          - --providers.docker.exposedByDefault=false
+          - --providers.docker.endpoint=tcp://socket-proxy:2375
+          - --api.dashboard=true
+          - --log.level=ERROR
+          - --certificatesresolvers.letsencrypt.acme.email=${EMAIL?Variable not defined}
+          - --certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json
+          - --certificatesresolvers.letsencrypt.acme.tlschallenge=true
+       deploy:
+         restart_policy:
+           condition: on-failure
+         placement:
+           constraints:
+             - node.labels.traefik-public.traefik-certificates == true
+             - node.role == manager
+         labels:
+             # Redirect to https
+             - traefik.http.routers.http-catchall.rule=hostregexp(`{host:.+}`)
+             - traefik.http.routers.http-catchall.entrypoints=web
+             - traefik.http.routers.http-catchall.middlewares=redirect-to-https
+             - traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https
+             - traefik.http.middlewares.redirect-to-https.redirectscheme.permanent=true
+
+             # Dashboard
+             - traefik.http.routers.api.entrypoints=websecure
+             - traefik.http.routers.api.rule=(Host(`${TRAEFIK_DOMAIN?Variable not defined}`)
+             - traefik.http.routers.api.middlewares=api-auth
+             - traefik.http.middlewares.api-auth.basicauth.users=${TRAEFIK_ADMINS?Variable not set}
+             - traefik.http.routers.api.service=api@internal
+             - traefik.http.services.gateway.loadbalancer.server.port=8080
+           ports:
+             - 80:80
+             - 443:443
+           networks:
+             - traefik-public
+           volumes:
+               - traefik-certificates:/letsencrypt
+               - /home/$USER/traefik/traefik_conf:/traefik_conf
```

Finally, we'll need to tell Traefik which containers it should make publicly available. For that feat, we must add labels to the services `gateway` and `prometheus`:

```diff
    gateway:
        image: openfaas/gateway:0.18.18
        ...
        deploy:
+           labels:
+               - traefik.enable=true
+               - traefik.constraint-label=traefik-public
+               - traefik.http.routers.gateway.entrypoints=websecure
+               - traefik.http.routers.gateway.rule=Host(`${FAAS_DOMAIN?Variable not set`)
+               - traefik.http.routers.gateway.tls.certresolver=letsencrypt
+               - traefik.http.services.gateway.loadbalancer.server.port=8080
```

The same for the Prometheus dashboard:

```diff
    prometheus:
    ...
        deploy:
+           labels:
+               - traefik.enable=true
+               - traefik.constraint-label=traefik-public
+               - traefik.http.routers.gateway.entrypoints=websecure
+               - traefik.http.routers.gateway.rule=Host(`${PROMETHEUS_DOMAIN?Variable not set`)
+               - traefik.http.routers.gateway.tls.certresolver=letsencrypt
+               - traefik.http.services.gateway.loadbalancer.server.port=9090
```

### 4.2. Final Configuration

This is the final `docker-compose.yml`.

I've updated the docker version to `3.8` and ran the file through a yaml prettifier to avoid indentation errors.

```yaml
version: '3.8'
services:
  socket-proxy:
    image: 'tecnativa/docker-socket-proxy:latest'
    environment:
      NETWORKS: 1
      SERVICES: 1
      TASKS: 1
      POST: 1
    deploy:
      restart_policy:
        condition: on-failure
      placement:
        constraints:
          - node.role == manager
    volumes:
      - '/var/run/docker.sock:/var/run/docker.sock:ro'
    networks:
      - traefik-public
  traefik:
    image: 'traefik:v2.2'
    restart: unless-stopped
    command:
      - '--entrypoints.web.address=:80'
      - '--entrypoints.websecure.address=:443'
      - '--providers.docker=true'
      - '--providers.docker.swarmMode=true'
      - '--providers.docker.endpoint=tcp://socket-proxy:2375'
      - '--providers.docker.network=faas_traefik-public'
      - '--providers.docker.exposedByDefault=false'
      - '--api.dashboard=true'
      - '--log.level=ERROR'
      - >-
        --certificatesresolvers.letsencrypt.acme.email=${EMAIL?Variable not
        defined}
      - '--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json'
      - '--certificatesresolvers.letsencrypt.acme.tlschallenge=true'
    deploy:
      restart_policy:
        condition: on-failure
      placement:
        constraints:
          - node.labels.traefik-public.traefik-certificates == true
          - node.role == manager
      labels:
        - 'traefik.http.routers.http-catchall.rule=hostregexp(`{host:.+}`)'
        - traefik.http.routers.http-catchall.entrypoints=web
        - traefik.http.routers.http-catchall.middlewares=redirect-to-https
        - traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https
        - >-
          traefik.http.middlewares.redirect-to-https.redirectscheme.permanent=true
        - traefik.http.routers.api.entrypoints=websecure
        - >-
          traefik.http.routers.api.rule=(Host(`${TRAEFIK_DOMAIN?Variable not
          defined}`)
        - traefik.http.routers.api.middlewares=api-auth
        - >-
          traefik.http.middlewares.api-auth.basicauth.users=${TRAEFIK_ADMINS?Variable
          not defined}
        - traefik.http.routers.api.service=api@internal
        - traefik.http.services.gateway.loadbalancer.server.port=8080
    ports:
      - '80:80'
      - '443:443'
    networks:
      - traefik-public
    volumes:
      - 'traefik-certificates:/letsencrypt'
      - '/home/$USER/traefik/traefik_conf:/traefik_conf'
  gateway:
    image: 'openfaas/gateway:0.18.18'
    networks:
      - functions
      - traefik-public
    environment:
      functions_provider_url: 'http://faas-swarm:8080/'
      read_timeout: 5m5s
      write_timeout: 5m5s
      upstream_timeout: 5m
      dnsrr: 'true'
      faas_nats_address: nats
      faas_nats_port: 4222
      direct_functions: 'true'
      direct_functions_suffix: ''
      basic_auth: '${BASIC_AUTH:-false}'
      secret_mount_path: /run/secrets/
      scale_from_zero: 'true'
      max_idle_conns: 1024
      max_idle_conns_per_host: 1024
      auth_proxy_url: '${AUTH_URL:-}'
      auth_proxy_pass_body: 'false'
    deploy:
      labels:
        - traefik.enable=true
        - traefik.constraint-label=traefik-public
        - traefik.http.routers.gateway.entrypoints=websecure
        - >-
          traefik.http.routers.gateway.rule=Host(`${FAAS_DOMAIN?Variable not
          defined}`)
        - traefik.http.routers.gateway.tls.certresolver=letsencrypt
        - traefik.http.services.gateway.loadbalancer.server.port=8080
      resources:
        reservations:
          memory: 100M
      restart_policy:
        condition: on-failure
        delay: 5s
        max_attempts: 20
        window: 380s
      placement:
        constraints:
          - node.platform.os == linux
    secrets:
      - basic-auth-user
      - basic-auth-password
  basic-auth-plugin:
    image: 'openfaas/basic-auth-plugin:0.18.18'
    networks:
      - functions
    environment:
      secret_mount_path: /run/secrets/
      user_filename: basic-auth-user
      pass_filename: basic-auth-password
    deploy:
      placement:
        constraints:
          - node.role == manager
          - node.platform.os == linux
      resources:
        reservations:
          memory: 50M
      restart_policy:
        condition: on-failure
        delay: 5s
        max_attempts: 20
        window: 380s
    secrets:
      - basic-auth-user
      - basic-auth-password
  faas-swarm:
    volumes:
      - '/var/run/docker.sock:/var/run/docker.sock:ro'
    image: 'openfaas/faas-swarm:0.9.0'
    networks:
      - functions
    environment:
      read_timeout: 5m5s
      write_timeout: 5m5s
      DOCKER_API_VERSION: '1.30'
      basic_auth: '${BASIC_AUTH:-false}'
      secret_mount_path: /run/secrets/
    deploy:
      placement:
        constraints:
          - node.role == manager
          - node.platform.os == linux
      resources:
        reservations:
          memory: 100M
      restart_policy:
        condition: on-failure
        delay: 5s
        max_attempts: 20
        window: 380s
    secrets:
      - basic-auth-user
      - basic-auth-password
  nats:
    image: 'nats-streaming:0.17.0'
    command: '--store memory --cluster_id faas-cluster'
    networks:
      - functions
    deploy:
      resources:
        limits:
          memory: 125M
        reservations:
          memory: 50M
      placement:
        constraints:
          - node.platform.os == linux
  queue-worker:
    image: 'openfaas/queue-worker:0.11.2'
    networks:
      - functions
    environment:
      max_inflight: '1'
      ack_wait: 5m5s
      basic_auth: '${BASIC_AUTH:-false}'
      secret_mount_path: /run/secrets/
      gateway_invoke: 'true'
      faas_gateway_address: gateway
    deploy:
      resources:
        limits:
          memory: 50M
        reservations:
          memory: 20M
      restart_policy:
        condition: on-failure
        delay: 5s
        max_attempts: 20
        window: 380s
      placement:
        constraints:
          - node.platform.os == linux
    secrets:
      - basic-auth-user
      - basic-auth-password
  prometheus:
    image: 'prom/prometheus:v2.11.0'
    environment:
      no_proxy: gateway
    configs:
      - source: prometheus_config
        target: /etc/prometheus/prometheus.yml
      - source: prometheus_rules
        target: /etc/prometheus/alert.rules.yml
    command:
      - '--config.file=/etc/prometheus/prometheus.yml'
    networks:
      - functions
      - traefik-public
    deploy:
      labels:
        - traefik.enable=true
        - traefik.constraint-label=traefik-public
        - traefik.http.routers.gateway.entrypoints=websecure
        - >-
          traefik.http.routers.gateway.rule=Host(`${PROMETHEUS_DOMAIN?Variable
          not defined}`)
        - traefik.http.routers.gateway.tls.certresolver=letsencrypt
        - traefik.http.services.gateway.loadbalancer.server.port=8080
      placement:
        constraints:
          - node.role == manager
          - node.platform.os == linux
      resources:
        limits:
          memory: 500M
        reservations:
          memory: 200M
  alertmanager:
    image: 'prom/alertmanager:v0.18.0'
    environment:
      no_proxy: gateway
    command:
      - '--config.file=/alertmanager.yml'
      - '--storage.path=/alertmanager'
    networks:
      - functions
    deploy:
      resources:
        limits:
          memory: 50M
        reservations:
          memory: 20M
      placement:
        constraints:
          - node.role == manager
          - node.platform.os == linux
    configs:
      - source: alertmanager_config
        target: /alertmanager.yml
    secrets:
      - basic-auth-password
configs:
  prometheus_config:
    file: ./prometheus/prometheus.yml
  prometheus_rules:
    file: ./prometheus/alert.rules.yml
  alertmanager_config:
    file: ./prometheus/alertmanager.yml
networks:
  traefik-public:
    driver: overlay
    attachable: true
  functions:
    driver: overlay
    attachable: true
volumes:
  traefik-public: null
secrets:
  basic-auth-user:
    external: true
  basic-auth-password:
    external: true
```

## Deploy Functions

You can deploy new functions via the dashboard. It's available under the url of the `FAAS_DOMAIN` environment variable.

![openfaas ui](https://www.openfaas.com/images/2020-cloud-native-tools-webinar/openfaas-dashboard.png)

OpenFaaS automatically created an admin user with password. You should have seen the credentials in your terminal. If don't have have them, check the [troubleshooting guide for forgetting the gateway password](https://docs.openfaas.com/deployment/troubleshooting/#i-forgot-my-gateway-password).

You can also use the `faas-cli` command-line utility. It offers pre-made templates and convenience wrappers for pushing to a container registry.

More info on the [official website](https://docs.openfaas.com/cli/templates/#get-started).

## Final Thoughts

Docker Swarm, Traefik 2 and OpenFaaS play very well together. OpenFaaS offers you an agnostic framework for serverless functions without vendor lock-in. The `faas-cli` and the dashboard are very user-friendly.

Traefik 2 shows all its power as a load-balancer and edge router with automatic provisioning of SSL certificates via [Let's Encrypt](https://letsencrypt.org/). It plays well with the OpenFaaS architecture.

You should give it a try.

## Further Reading

- [OpenFaaS: Deployment guide for Docker Swarm][openfaasswarm]
- [How To Install and Secure OpenFaaS Using Docker Swarm on Ubuntu 16.04][digitalocean]
- [Traefik Proxy with HTTPS][dockerswarmrocks]
- [How to install Traefik 2.x on a Docker Swarm][guide1]
- [How to expose Traefik 2.x dashboard securely on Docker Swarm][guide2]

[openfaas]: https://www.openfaas.com/
[traefik]: https://doc.traefik.io/traefik/
[faasd]: https://github.com/openfaas/faasd/
[containerd_docker]: https://www.inovex.de/blog/containers-docker-containerd-nabla-kata-firecracker/
[openfaasswarm]: https://docs.openfaas.com/deployment/docker-swarm/
[dockerswarmrocks]: https://dockerswarm.rocks/traefik/
[tecnativa]: https://github.com/Tecnativa/docker-socket-proxy
[digitalocean]: https://www.digitalocean.com/community/tutorials/how-to-install-and-secure-openfaas-using-docker-swarm-on-ubuntu-16-04
[guide1]: https://blog.creekorful.com/2019/10/how-to-install-traefik-2-docker-swarm/
[guide2]: https://blog.creekorful.com/2020/01/how-to-expose-traefik-2-dashboard-securely-docker-swarm/
[upcloud]: https://upcloud.com/signup/?promo=2QM5FX
[hetzner]: https://hetzner.cloud
[doreferral]: https://m.do.co/c/aff28b1fcb99

[^1]: affiliate links - you'll get free credit and I also get some - thank you!