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.
NoteStandalone OpenWhisk runs the whole platform in one container and keeps actions and activations in memory, so they are lost when the container restarts. It is designed for development, testing and small single-server setups. For production clusters, the OpenWhisk project provides a Helm chart for Kubernetes.
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
sudoprivileges. - Docker Engine installed from Docker's official repository, with your user in the
dockergroup. - For Step 7 only: a domain name with an
Arecord pointing to your server, and Nginx installed withsudo 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'
WarningThis
guestkey is the same in every OpenWhisk standalone installation and is published in the project documentation. Anyone who can reach port 3233 can run code on your server, so never expose the API port directly. Step 7 shows how to publish only web actions.
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.
