Apache OpenWhisk is an open-source, event-driven serverless platform. You upload small units of code called actions, and OpenWhisk runs each invocation in a Docker container, wiring actions to events through triggers and rules. In this tutorial you will run OpenWhisk in standalone mode with Docker on Ubuntu 24.04, configure the wsk CLI, create Node.js and Python actions, group them in a package, connect them to a trigger, and publish a web action through Nginx.

Prerequisites

To follow this tutorial, you need:

  • A server running Ubuntu 24.04 LTS on amd64, for example a CubePath VPS, with at least 2 vCPUs and 4 GB of RAM.
  • A non-root user with sudo privileges.
  • Docker Engine installed from Docker's official repository, with your user in the docker group.
  • For Step 7 only: a domain name with an A record pointing to your server, and Nginx installed with sudo apt install nginx.

Confirm that Docker works without sudo:

docker run --rm hello-world

Step 1 - Starting OpenWhisk standalone

The openwhisk/standalone image contains the controller, an in-memory database and the Playground UI. It starts action containers through the host's Docker socket. Publish the API (port 3233) and the Playground (port 3232) on the loopback interface only:

docker run -d \
  --name openwhisk \
  --hostname openwhisk \
  -p 127.0.0.1:3233:3233 \
  -p 127.0.0.1:3232:3232 \
  -v /var/run/docker.sock:/var/run/docker.sock \
  openwhisk/standalone:nightly

The image includes a waitready helper that blocks until the controller accepts requests. Run it and wait for it to return:

docker exec openwhisk waitready

Standalone mode creates a guest namespace with a default key. The startup log prints the exact wsk command to use it:

docker logs openwhisk 2>&1 | grep 'wsk property'
wsk property set --apihost 'http://localhost:3233' --auth '23bc46b1-71f6-4ed5-8c54-816aa4f8c502:123zO3xZCLrMN6v2BKK1dXYFpXlPkccOFqm12CdAsMgRU4VrNZ9lyGVCGuMDGIwP'

Step 2 - Installing and configuring the wsk CLI

wsk is the OpenWhisk command-line client. Download the latest release, 1.2.0, and install the binary:

curl -fsSLO https://github.com/apache/openwhisk-cli/releases/download/1.2.0/OpenWhisk_CLI-1.2.0-linux-amd64.tgz
tar -xzf OpenWhisk_CLI-1.2.0-linux-amd64.tgz wsk
sudo install -m 0755 wsk /usr/local/bin/wsk
rm wsk OpenWhisk_CLI-1.2.0-linux-amd64.tgz

Point the CLI at your server with the command printed in the logs. It saves the API host and key in ~/.wskprops:

wsk property set \
  --apihost 'http://localhost:3233' \
  --auth '23bc46b1-71f6-4ed5-8c54-816aa4f8c502:123zO3xZCLrMN6v2BKK1dXYFpXlPkccOFqm12CdAsMgRU4VrNZ9lyGVCGuMDGIwP'

Verify the connection:

wsk namespace list
namespaces
guest

Step 3 - Creating and invoking actions

An action is a stateless function that receives a dictionary of parameters and returns a dictionary. Create a Node.js action:

mkdir -p ~/openwhisk && cd ~/openwhisk
nano hello.js
function main(params) {
  const name = params.name || "World";
  console.log(`Greeting ${name}`);
  return { greeting: `Hello, ${name}!` };
}

Upload it. OpenWhisk infers the Node.js runtime from the .js extension:

wsk action create hello hello.js
ok: created action hello

Invoke it and wait for the result. The first call is slower because OpenWhisk pulls the runtime image and starts a container; later calls reuse a warm container:

wsk action invoke hello --param name OpenWhisk --result
{
    "greeting": "Hello, OpenWhisk!"
}

Without --result (or --blocking), the invocation is asynchronous and returns an activation ID immediately. Every invocation is recorded as an activation that holds the result and the logs:

wsk action invoke hello --param name async
wsk activation list --limit 3
wsk activation logs --last
2026-09-25T10:20:14.512Z  stdout: Greeting async

Actions can use other runtimes by passing --kind. Create a Python action:

nano process.py
from datetime import datetime, timezone


def main(args):
    return {
        "message": args.get("msg", "no message"),
        "processed_at": datetime.now(timezone.utc).isoformat(),
    }
wsk action create process process.py --kind python:3.11
wsk action invoke process --param msg "hello from Python" --result
{
    "message": "hello from Python",
    "processed_at": "2026-09-25T10:21:03.418226+00:00"
}

To change an action later, edit the file and run wsk action update with the same arguments. update also creates the action if it does not exist, which makes it convenient for deployment scripts.

Step 4 - Grouping actions in packages

A package groups related actions and can hold default parameters shared by all of them. Create a package with a greeting parameter:

wsk package create demo --param greeting Hola

Create an action that uses that parameter:

nano greet.js
function main(params) {
  const name = params.name || "World";
  return { text: `${params.greeting}, ${name}!` };
}
wsk action create demo/greet greet.js
wsk action invoke demo/greet --param name Alice --result
{
    "text": "Hola, Alice!"
}

Parameters passed at invocation time override package defaults, so --param greeting Hi would return Hi, Alice!. List the contents of the package with:

wsk package get demo --summary

Step 5 - Connecting actions to events with triggers and rules

A trigger is a named channel for a class of events, and a rule connects a trigger to an action. When the trigger fires, OpenWhisk invokes every action connected to it by an active rule, passing the event parameters.

Create a trigger and a rule that runs demo/greet for each event:

wsk trigger create orderCreated
wsk rule create greetOnOrder orderCreated demo/greet

Fire the trigger with a parameter, as an event producer would:

wsk trigger fire orderCreated --param name Bob
ok: triggered /_/orderCreated with id 6b0a2f6c9e8d4f1b8a2f6c9e8d4f1b8a

Check that the rule invoked the action:

wsk activation list --limit 2
wsk activation result --last
{
    "text": "Hola, Bob!"
}

You can disable a rule without deleting it with wsk rule disable greetOnOrder, and enable it again with wsk rule enable greetOnOrder. External systems fire triggers by sending a POST request to the OpenWhisk API, for example from a webhook receiver running on the same server.

Step 6 - Creating a web action

Web actions can be called over plain HTTP without an OpenWhisk key, which makes them suitable for public endpoints. A web action can control the status code, headers and body of the response:

nano api.js
function main(params) {
  return {
    statusCode: 200,
    headers: { "Content-Type": "application/json" },
    body: { message: `Hello, ${params.name || "web"}!` }
  };
}

Create it with --web true and print its URL:

wsk action create api api.js --web true
wsk action get api --url
ok: got action api
http://localhost:3233/api/v1/web/guest/default/api

Query string parameters are passed to the action as parameters:

curl "http://localhost:3233/api/v1/web/guest/default/api?name=Alice"
{
  "message": "Hello, Alice!"
}

Step 7 - Publishing web actions with Nginx

To make web actions reachable from the internet without exposing the authenticated API, put Nginx in front of OpenWhisk and forward only the /api/v1/web/ path. Create a server block, replacing your_domain with your domain:

sudo nano /etc/nginx/sites-available/openwhisk
server {
    listen 80;
    listen [::]:80;
    server_name your_domain;

    location /api/v1/web/ {
        proxy_pass http://127.0.0.1:3233;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_read_timeout 70s;
    }

    location / {
        return 404;
    }
}

Enable the site, test the configuration and reload Nginx:

sudo ln -s /etc/nginx/sites-available/openwhisk /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

Allow HTTP and HTTPS through UFW:

sudo ufw allow 'Nginx Full'

From your workstation, the web action now answers, while the rest of the API returns 404:

curl "http://your_domain/api/v1/web/guest/default/api?name=Internet"
curl -s -o /dev/null -w "%{http_code}\n" http://your_domain/api/v1/namespaces
{
  "message": "Hello, Internet!"
}
404

Add HTTPS with Certbot (sudo apt install certbot python3-certbot-nginx and then sudo certbot --nginx -d your_domain) before sending real traffic to it.

Troubleshooting

wsk returns connection refused. The container is not running or not ready yet. Check it with docker ps --filter name=openwhisk, then run docker exec openwhisk waitready and docker logs --tail 50 openwhisk.

All actions disappeared. Standalone mode stores everything in memory, so a container restart or a server reboot deletes actions, packages, triggers and rules. Keep your action code and wsk action update commands in Git so you can recreate them in seconds.

The first invocation times out. OpenWhisk pulls each runtime image, such as the Node.js or Python action image, the first time it is used. Invoke the action again after a minute, or pre-pull the images by invoking each action once after startup.

An action fails with The action did not produce a valid response. The action must return a JSON object (a dictionary in Python). Returning a string, a list or nothing produces this error. Check the details with wsk activation get --last.

Conclusion

You ran Apache OpenWhisk standalone in Docker, configured the wsk CLI, created Node.js and Python actions, shared defaults through a package, connected an action to events with a trigger and a rule, and exposed a web action through Nginx without exposing the authenticated API. As next steps, add sequences with wsk action create name --sequence a,b to chain actions, secure the Nginx site with a Let's Encrypt certificate, and move to the OpenWhisk Helm chart on Kubernetes when you need persistent storage and several invoker nodes.