OpenFaaS lets you package small pieces of code as functions, deploy them as containers and call them over HTTP, without managing a web server for each one. faasd is the single-host edition of OpenFaaS: it runs the OpenFaaS gateway, a NATS queue for asynchronous calls and Prometheus on top of containerd, with no Kubernetes involved, which makes it a good fit for one VPS. In this tutorial you will install faasd on Ubuntu 24.04 with a Let's Encrypt certificate, deploy a ready-made function, then write, build and deploy your own Python function and call it synchronously and asynchronously.

Prerequisites

To follow this tutorial, you will need:

  • A dedicated server running Ubuntu 24.04 LTS, for example a CubePath VPS, with at least 2 vCPUs, 2 GB of RAM and 20 GB of disk. faasd must be the only container runtime on the host: do not install it on a server that runs Docker, because both manage containerd, CNI and iptables and they conflict.
  • A non-root user with sudo privileges on the server.
  • A domain name with an A record pointing to the server, used for HTTPS. This guide uses faas.your_domain.
  • Ports 22, 80 and 443 reachable from the Internet.
  • A separate workstation or CI runner with Docker (including Buildx) to build function images, and an account on a container registry such as Docker Hub. OpenFaaS recommends building images on another host, never on the faasd server itself.

Step 1 - Installing faasd

faasd provides an installation script that installs runc, the CNI plugins, containerd, faas-cli, faasd itself and, when you pass a domain, the Caddy web server with an automatic Let's Encrypt certificate. Install git and clone the repository on the server:

sudo apt update
sudo apt install -y git
git clone https://github.com/openfaas/faasd --depth=1
cd faasd

The script runs commands with sudo, so read it before executing it:

less hack/install.sh

If UFW is enabled on the server, allow SSH and the ports Caddy needs to obtain the certificate and serve HTTPS:

sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp

Run the script as your regular user, passing your domain and the email address Let's Encrypt should use for expiry notices. Replace both values:

FAASD_DOMAIN=faas.your_domain LETSENCRYPT_EMAIL=you@your_domain ./hack/install.sh

The script prints every command it runs and takes a few minutes. When it finishes, faasd runs as two systemd services: faasd-provider, which creates function containers, and faasd, which runs the gateway, NATS, the queue worker and Prometheus. Check both:

systemctl is-active faasd faasd-provider caddy
active
active
active

The gateway listens on port 8080 and answers a health check:

curl -s http://127.0.0.1:8080/healthz; echo
OK

Caddy terminates TLS on port 443 and forwards to the gateway, so the same check works over HTTPS once the certificate has been issued:

curl -s https://faas.your_domain/healthz; echo

If this call fails right after the installation, give Caddy a minute to complete the certificate request and check its log with sudo journalctl -u caddy -n 50.

Step 2 - Logging in with faas-cli

The gateway protects its management API (deploying, listing and removing functions) with basic authentication. The installer generated a password for the admin user and stored it in /var/lib/faasd/secrets/. Log in to the local gateway with it:

sudo cat /var/lib/faasd/secrets/basic-auth-password | faas-cli login --password-stdin
Calling the OpenFaaS server to validate the credentials...
credentials saved for admin http://127.0.0.1:8080

faas-cli uses http://127.0.0.1:8080 by default. Confirm that it can talk to the gateway:

faas-cli list
Function                      	Invocations    	Replicas

The list is empty because no functions are deployed yet.

Step 3 - Deploying a function from the store

The OpenFaaS function store contains ready-made functions, which is the fastest way to check that the whole stack works. Deploy figlet, which turns text into ASCII art:

faas-cli store deploy figlet

faasd pulls the image and starts the container. Invoke the function with faas-cli, which sends standard input as the request body:

echo "Hi" | faas-cli invoke figlet
 _   _ _ 
| | | (_)
| |_| | |
|  _  | |
|_| |_|_|

Every function is also available over HTTPS at /function/<name>, without authentication, which is how applications and webhooks call it:

curl -s -d "Hi" https://faas.your_domain/function/figlet

The response is the same ASCII art. Remove the test function when you are done:

faas-cli remove figlet

Step 4 - Setting up faas-cli on your build machine

Function images are built and pushed from your workstation or CI runner, then faasd pulls them from the registry. On that machine, install faas-cli with the official script, which downloads the right binary for your OS and architecture:

curl -sSL https://cli.openfaas.com | sudo sh
faas-cli version

Point faas-cli at your server and log in. Copy the password from the server (sudo cat /var/lib/faasd/secrets/basic-auth-password), save it in a file only your user can read, and pass it on standard input so it does not end up in your shell history:

export OPENFAAS_URL=https://faas.your_domain
nano ~/.openfaas-password
chmod 600 ~/.openfaas-password
faas-cli login --password-stdin < ~/.openfaas-password
Calling the OpenFaaS server to validate the credentials...
credentials saved for admin https://faas.your_domain

Add the export OPENFAAS_URL=... line to your ~/.bashrc or ~/.zshrc so later sessions use it too. Finally, log in to your registry with docker login so that faas-cli can push images.

Step 5 - Creating a Python function

Functions are created from templates that contain the language runtime and a small HTTP server called the watchdog. python3-http is the recommended Python template: your handler receives the full HTTP request and returns a status code, headers and a body. Download it from the template store:

mkdir -p ~/functions && cd ~/functions
faas-cli template store pull python3-http

Create a function called hello. The --prefix value is your registry user or organization and becomes the first part of the image name:

faas-cli new hello --lang python3-http --prefix your_dockerhub_user

This creates a hello/ directory with handler.py and requirements.txt, and a stack.yaml file that describes the function (older releases of faas-cli name it hello.yml; use that name in the commands below if so). Open the handler:

nano hello/handler.py

Replace its contents with a function that reads a JSON body and returns a greeting:

import json


def handle(event, context):
    try:
        data = json.loads(event.body) if event.body else {}
    except ValueError:
        return {
            "statusCode": 400,
            "headers": {"Content-Type": "application/json"},
            "body": json.dumps({"detail": "Request body must be JSON"}),
        }

    name = data.get("name", "world")
    return {
        "statusCode": 200,
        "headers": {"Content-Type": "application/json"},
        "body": json.dumps({"message": f"Hello, {name}!"}),
    }

event.body contains the raw request body; event.headers, event.method, event.query and event.path are also available. Add any Python dependencies to hello/requirements.txt, one per line, and they are installed when the image is built.

Open stack.yaml to review the function definition:

nano stack.yaml
version: 1.0
provider:
  name: openfaas
  gateway: http://127.0.0.1:8080
functions:
  hello:
    lang: python3-http
    handler: ./hello
    image: your_dockerhub_user/hello:latest

The gateway line is ignored while OPENFAAS_URL is set, so you can leave it as it is. For real deployments, replace the latest tag with a version such as 0.1.0 and bump it on every release, so faasd always pulls the image you just built.

Step 6 - Building, pushing and deploying the function

faas-cli publish builds the image with Docker Buildx and pushes it to the registry. Pass the platform of your server explicitly: this matters when you build on an Apple Silicon Mac (arm64) for an x86_64 VPS, or the other way round. Use linux/arm64 if your VPS runs on Arm:

faas-cli publish -f stack.yaml --platforms linux/amd64

Deploy it to faasd:

faas-cli deploy -f stack.yaml
Deploying: hello.

Deployed. 200 OK.
URL: https://faas.your_domain/function/hello

Call the function through the public URL:

curl -s -H "Content-Type: application/json" -d '{"name": "CubePath"}' https://faas.your_domain/function/hello
{"message": "Hello, CubePath!"}

Check that invalid input is rejected with the status code set in the handler:

curl -s -o /dev/null -w "%{http_code}\n" -d 'not json' https://faas.your_domain/function/hello
400

To release a change, edit the handler, bump the image tag in stack.yaml, and run publish and deploy again.

Step 7 - Invoking the function asynchronously

For work that takes longer than an HTTP client wants to wait, such as sending emails or processing files, call the function through /async-function/ instead. The gateway stores the request in NATS, answers immediately with 202 Accepted, and the queue worker runs the function in the background:

curl -si -d '{"name": "async"}' https://faas.your_domain/async-function/hello | head -n 1
HTTP/1.1 202 Accepted

To receive the result, add an X-Callback-Url header with a URL that accepts POST requests. When the function finishes, the queue worker posts its response there, along with an X-Call-Id header that matches the one returned in the 202 response:

curl -si -d '{"name": "async"}' \
  -H "X-Callback-Url: https://your_app_domain/openfaas-callback" \
  https://faas.your_domain/async-function/hello

Step 8 - Monitoring and managing functions

faas-cli shows invocation counts, replicas and logs for each function. List the deployed functions:

faas-cli list
Function                      	Invocations    	Replicas
hello                         	4              	1

Show details such as the image, the URLs and the number of invocations:

faas-cli describe hello

Read the function's logs, which include anything the handler prints and errors from the watchdog:

faas-cli logs hello

On the server, the platform itself logs to the journal:

sudo journalctl -u faasd -n 50
sudo journalctl -u faasd-provider -n 50

faasd runs one replica per function and does not autoscale or scale to zero. That is enough for webhooks, scheduled jobs and internal tools on a single VPS. If you need several replicas that scale with load, OpenFaaS on Kubernetes is the next step.

Troubleshooting

faas-cli deploy succeeds but calls return 500 or the function never becomes ready: the image could not be pulled or crashes on start. Check sudo journalctl -u faasd-provider -n 50 on the server. exec format error means the image was built for the wrong CPU architecture: rebuild with the correct --platforms value. pull access denied means the image name is wrong or the repository is private; make it public or configure registry credentials for faasd.

Requests to a slow function are cut off: the watchdog inside the function enforces timeouts. Add the following under the function in stack.yaml and deploy again. The gateway has its own timeouts in /var/lib/faasd/docker-compose.yaml, which must be at least as long:

    environment:
      exec_timeout: 60s
      read_timeout: 60s
      write_timeout: 60s

https://faas.your_domain does not respond: check that the A record points to the server, that ports 80 and 443 are open, and read Caddy's log with sudo journalctl -u caddy -n 50. Caddy cannot obtain a certificate until the domain resolves to the server.

unauthorized from faas-cli: the saved credentials do not match the gateway, for example after a reinstallation. Repeat the login from Step 2 or Step 4.

Conclusion

You installed OpenFaaS faasd on Ubuntu 24.04 behind HTTPS, deployed a function from the store, then built, deployed and invoked your own Python function both synchronously and asynchronously. From here you can store API keys for your functions with faas-cli secret create, connect a function to a database by adding its driver to requirements.txt, and automate faas-cli publish and faas-cli deploy in a CI pipeline so every commit ships a new version.