From dataapp-developer
Guides deploying web apps to Keboola Data Apps: keboola-config setup, Docker Nginx/Supervisord config, UV for Python deps via pyproject.toml, SSE/WebSocket proxying, secrets to env vars, debug POST/500 errors.
How this skill is triggered — by the user, by Claude, or both
Slash command
/dataapp-developer:dataapp-deploymentThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Guide for deploying web apps (Node.js, Python, or any language) to Keboola Data Apps using the `keboola/data-app-python-js` Docker base image.
Guide for deploying web apps (Node.js, Python, or any language) to Keboola Data Apps using the keboola/data-app-python-js Docker base image.
Keboola Data Apps run in Docker containers:
Internet → Keboola proxy → Docker container
├── Nginx (port 8888, public-facing)
│ └── reverse proxy → localhost:<app-port>
├── Supervisord (process manager)
│ └── manages your app process(es)
└── Your app (any internal port)
Key facts:
keboola/data-app-python-js (Debian Bookworm slim with Python, Node.js, Nginx, Supervisord)/app/keboola-config/setup.sh runs on container startup before your appdataApp.secrets are exported as env vars/ on startup — your app must handle this (not just GET)The container startup sequence is:
/app/dataApp.secrets as environment variablespip_repositories is set.conf in keboola-config/nginx/sites/.conf in keboola-config/supervisord//app/keboola-config/setup.sh (install deps)run.sh if it exists)The base image uses uv to manage Python. Bare pip is blocked (PEP 668).
These will ALL fail:
# WRONG — PEP 668 blocks this
pip install -r requirements.txt
# WRONG — no virtual environment found
uv pip install -r requirements.txt
# WRONG — still fails in this environment
uv pip install --system -r requirements.txt
The correct approach:
# CORRECT — uses pyproject.toml, creates venv, installs everything
cd /app && uv sync
This means your Python app must have a pyproject.toml with dependencies listed in the [project.dependencies] array. A requirements.txt alone is not sufficient.
Similarly, all Python commands in Supervisord must be prefixed with uv run to execute within the uv-managed environment.
your-repo/
├── keboola-config/
│ ├── nginx/
│ │ └── sites/
│ │ └── default.conf # Nginx reverse proxy config
│ ├── supervisord/
│ │ └── services/
│ │ └── app.conf # Process manager config
│ └── setup.sh # Startup script (install deps)
├── pyproject.toml # Python deps (required for Python apps)
├── <your app files> # Any language/framework
└── <dependency file> # package.json for Node.js, etc.
Basic reverse proxy (works for any backend):
server {
listen 8888;
server_name _;
location / {
proxy_pass http://127.0.0.1:8050;
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;
}
}
Change 8050 to whatever port your app listens on.
For WebSocket apps (Streamlit, etc.), add upgrade headers:
server {
listen 8888;
server_name _;
location / {
proxy_pass http://127.0.0.1:8050;
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;
# WebSocket support
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 86400;
}
}
For streaming endpoints (SSE, long-polling), add a separate location block with buffering disabled:
location /api/stream {
proxy_pass http://127.0.0.1:3000;
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_buffering off;
proxy_cache off;
proxy_request_buffering off;
client_max_body_size 5m;
proxy_read_timeout 120s;
proxy_http_version 1.1;
proxy_set_header Connection '';
}
Without proxy_buffering off, Nginx buffers the entire response before forwarding — the client sees nothing until the stream ends.
Important: Nginx is managed by the base image automatically — do NOT add
[program:nginx]in your configs. Only define your own app processes.
Python (Streamlit):
[program:app]
command=uv run streamlit run /app/streamlit_app.py --server.port 8050 --server.headless true
directory=/app
autostart=true
autorestart=true
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
Python (Flask/FastAPI with uvicorn):
[program:app]
command=uv run uvicorn app:app --host 127.0.0.1 --port 8050
directory=/app
autostart=true
autorestart=true
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
Python (Gunicorn):
[program:app]
command=uv run gunicorn --bind 0.0.0.0:5000 app:app
directory=/app
autostart=true
autorestart=true
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
Node.js:
[program:app]
command=node /app/server.js
directory=/app
autostart=true
autorestart=true
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
Use absolute paths (/app/...). Relative paths cause startup failures.
Python apps:
#!/bin/bash
set -Eeuo pipefail
cd /app && uv sync
Node.js apps:
#!/bin/bash
set -Eeuo pipefail
cd /app && npm install
Multi-server (Python + Node.js):
#!/bin/bash
set -Eeuo pipefail
cd /app && uv sync &
cd /app/frontend && npm install &
wait
Must be executable (chmod +x). Runs once on container startup before Supervisord starts your app.
Python apps must define dependencies in pyproject.toml, not just requirements.txt:
[project]
name = "my-data-app"
version = "0.1.0"
requires-python = ">=3.11"
dependencies = [
"streamlit~=1.45.1",
"pandas~=2.2.3",
"plotly~=6.0.1",
"requests>=2.31.0",
]
[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta"
If migrating from requirements.txt, move all dependencies into the dependencies array with their version specifiers.
Keboola dataApp.secrets entries are exported as environment variables:
# is stripped (Keboola secret marker)_| dataApp.secrets key | Env var in container |
|---|---|
#KBC_TOKEN | KBC_TOKEN |
#KBC_URL | KBC_URL |
#KBC_DATABASE_NAME | KBC_DATABASE_NAME |
#ANTHROPIC_API_KEY | ANTHROPIC_API_KEY |
#my-custom-var | MY_CUSTOM_VAR |
Access them in your code as normal environment variables:
import os
token = os.environ.get("KBC_TOKEN")
const token = process.env.KBC_TOKEN;
If your app already reads env vars locally, it works in Keboola with no code changes — just add the matching secrets in the data app configuration.
Secrets are available to both setup.sh and the application runtime.
Streamlit is the simplest to deploy — it handles POST to / natively and needs minimal config.
Nginx: Must include WebSocket upgrade headers (see above). Streamlit uses WebSockets for /_stcore/stream.
Supervisord:
command=uv run streamlit run /app/streamlit_app.py --server.port 8050 --server.headless true
setup.sh:
cd /app && uv sync
from flask import Flask, send_from_directory
import os
app = Flask(__name__, static_folder="static")
PORT = int(os.environ.get("PORT", 5000))
@app.route("/api/data", methods=["GET", "POST"])
def data():
return {"status": "ok"}
@app.route("/", methods=["GET", "POST"]) # Handle POST too
def index():
return send_from_directory(".", "index.html")
if __name__ == "__main__":
app.run(host="0.0.0.0", port=PORT)
import express from 'express';
import { dirname, join } from 'path';
import { fileURLToPath } from 'url';
const __dirname = dirname(fileURLToPath(import.meta.url));
const app = express();
const PORT = process.env.PORT || 3000;
app.use(express.json());
// API routes
import myHandler from './api/my-route.js';
app.all('/api/my-route', myHandler);
// Serve frontend — use app.all(), NOT app.get()
// Keboola POSTs to / on startup
app.all('/', (req, res) => res.sendFile(join(__dirname, 'index.html')));
app.use(express.static(__dirname, { index: false }));
app.listen(PORT, '0.0.0.0');
Vercel dual-deployment tip: Vercel serverless handlers (export default function(req, res)) are directly compatible with Express route handlers. Create an Express server.js that imports and mounts the same handler files — no code changes to the handlers themselves.
Cause: Using pip install directly. The base image manages Python via uv.
Fix: Use uv sync in setup.sh and prefix all Python commands with uv run in Supervisord. Ensure your project has a pyproject.toml with dependencies listed.
Cause: Using uv pip install without --system, or with --system which also fails in this image.
Fix: Use uv sync — it reads pyproject.toml, creates a venv, and installs deps automatically.
Cause: Keboola platform POSTs to / on startup. Your app only handles GET.
Fix: Handle all HTTP methods on the root route. In Express: app.all('/'). In Flask: methods=["GET", "POST"]. Streamlit handles this natively.
Cause: Missing environment variable not configured in dataApp.secrets.
Fix: Add all required env vars as secrets. Check server logs via Keboola UI to identify which variable is missing.
Cause: Nginx buffers the response by default.
Fix: Add proxy_buffering off; proxy_cache off; to the Nginx location block for streaming endpoints.
Cause: setup.sh failed (dependency install error), wrong path in Supervisord config, or missing uv run prefix.
Fix: Ensure setup.sh is executable (chmod +x), paths in Supervisord are absolute (/app/...), uv run prefixes all Python commands, and uv sync succeeds.
Cause: Nginx not configured for WebSocket upgrade.
Fix: Add proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; to the Nginx location block.
Cause: Usually a missing env var, a port mismatch between Nginx and your app, a dependency that fails to install, or missing uv run prefix.
Fix: Check that Nginx proxies to the same port your app listens on, all env vars are in dataApp.secrets, uv sync installs everything, and Supervisord commands use uv run.
pyproject.toml has all Python dependencies listed (not just requirements.txt)keboola-config/setup.sh — Executable, uses uv sync for Python / npm install for Node.jskeboola-config/nginx/sites/default.conf — Listens on 8888, proxies to your app's portkeboola-config/supervisord/services/*.conf — Absolute paths, correct start command, uv run prefix for Python[program:nginx] in your Supervisord configs (base image manages Nginx)dataApp.secrets in Keboolaproxy_buffering off in Nginxkeboola/data-app-python-js has both Python and Node.js runtimes. Use whichever fits your app.command locally to catch issues before deploying.keboola-config/ directory and all config files.npx claudepluginhub keboola/ai-kit --plugin dataapp-developerDevelops Streamlit data apps for Keboola deployment: validates schemas with Keboola MCP, builds SQL-first implementations, tests with Playwright. For dashboards, filters, pages, debugging.
Manages Keboola Connection projects via kbagent CLI: explores configs, jobs, and lineage; syncs configs as local files (GitOps); manages branches, buckets, and data apps; encrypts secrets; debugs SQL in workspaces; and handles bulk onboarding.
Guides deploying Deno apps to Deno Deploy using `deno deploy` CLI, covering workflows, environment variables, KV database, custom domains, and --tunnel flag.