Skip to content

Tutorials

Real-world recipes. Copy-paste and adapt.

StackJump toTime
β–² Next.jsNext.js3 min
🟒 Express / FastifyExpress / Fastify (Node.js)2 min
πŸ₯Ÿ BunBun1 min
🐍 FastAPI + UvicornPython β€” FastAPI + Uvicorn2 min
πŸ¦„ Django + GunicornPython β€” Django + Gunicorn2 min
🐹 Go web serverGo web server2 min
πŸ¦€ Rust (Actix/Axum)Rust (Actix / Axum)2 min
πŸ“„ Static siteStatic site server (Caddy / Nginx)1 min
⏰ Cron / scheduledCron / scheduled tasks1 min
πŸ”’ Production hardeningSecure isolation (production)3 min
πŸš€ Full deploy walkthroughFull production deploy (step by step)10 min
πŸ“œ Lynxfile (declarative)Lynxfile.yml β€” declarative multi-app deploy5 min
πŸ“Š Monitor & debugMonitoring and debugging1 min
πŸ’‘ Daily-use tipsTips-

πŸ’‘ Tip: all examples work identically in user mode (lynxd &) and system mode (sudo systemctl start lynxd). The only difference in prod: swap --isolation self for --isolation dynamic.


Terminal window
# Inside your Next.js project directory
lynxpm start "npm run dev" --name nextjs-dev --cwd /srv/myapp --shell
lynxpm logs nextjs-dev --follow

What you see:

Started nextjs-dev
ID: 019d93ab-... PID: 12345 Status: running
[STDOUT] β–² Next.js 15.0.0
[STDOUT] - Local: http://localhost:3000
[STDOUT] βœ“ Ready in 2.1s
Terminal window
# 1. Build first
cd /srv/myapp && npm run build
# 2. Start the standalone server
lynxpm start "node .next/standalone/server.js" \
--name nextjs-prod \
--cwd /srv/myapp \
--restart always \
--env-file .env.production \
--memory-max 512M
# 3. Verify
lynxpm show nextjs-prod

Next.js standalone doesn’t support Node cluster natively. Use --scale instead β€” each instance listens on a different port:

Terminal window
# Start 3 instances; each reads LYNX_INSTANCE to pick a port
lynxpm start "node .next/standalone/server.js" \
--name nextjs \
--cwd /srv/myapp \
--scale 3 \
--restart always \
--env-file .env.production
# In your server.js or next.config.js:
# const port = 3000 + Number(process.env.LYNX_INSTANCE || 0);

Then put Nginx or Caddy in front:

upstream nextjs {
server 127.0.0.1:3000;
server 127.0.0.1:3001;
server 127.0.0.1:3002;
}
server {
listen 80;
location / { proxy_pass http://nextjs; }
}
Terminal window
lynxpm scale nextjs 5 # add 2 more instances
lynxpm scale nextjs 2 # drop back to 2

Output:

Scaled nextjs: 3 β†’ 5
+ nextjs-4
+ nextjs-5

⚠️ Warning: Each instance must bind a unique port. Read LYNX_INSTANCE (0-based) and compute port = 3000 + LYNX_INSTANCE.


Terminal window
# Simple
lynxpm start "node server.js" --name api --cwd /srv/api --restart always
# With env file
lynxpm start "node server.js" \
--name api \
--cwd /srv/api \
--env-file .env \
--restart always \
--memory-max 256M
# Cluster (4 workers)
lynxpm start "node server.js" --name api --scale 4 --cwd /srv/api
# Your app reads process.env.LYNX_INSTANCE to bind to port 3000+N

Express needs SIGINT to close connections cleanly:

Terminal window
lynxpm start "node server.js" \
--name api \
--stop-signal SIGINT \
--stop-timeout 30000 \
--restart always

In your Express app:

process.on('SIGINT', () => {
server.close(() => process.exit(0));
});

Terminal window
# Dev
lynxpm start "bun run dev" --name bun-dev --cwd /srv/app
# Production
lynxpm start "bun run src/index.ts" \
--name bun-prod \
--cwd /srv/app \
--restart always \
--memory-max 256M
# Hot reload: Bun already watches files by default in dev

Terminal window
# Development (with reload)
lynxpm start "uvicorn main:app --reload --host 0.0.0.0 --port 8000" \
--name fastapi-dev \
--cwd /srv/api \
--shell
# Production (with uv)
lynxpm start "uv run uvicorn main:app --host 0.0.0.0 --port 8000 --workers 4" \
--name fastapi-prod \
--cwd /srv/api \
--restart always \
--memory-max 1G \
--env-file .env
# Production with venv (direct path)
lynxpm start "/srv/api/.venv/bin/uvicorn main:app --host 0.0.0.0 --port 8000" \
--name fastapi-prod \
--cwd /srv/api \
--restart always

Terminal window
# Via uv
lynxpm start "uv run gunicorn myproject.wsgi:application --bind 0.0.0.0:8000 --workers 4" \
--name django \
--cwd /srv/django \
--restart always \
--env-file .env \
--stop-signal SIGINT \
--stop-timeout 30000
# Via venv
lynxpm start "/srv/django/.venv/bin/gunicorn myproject.wsgi:application -b 0.0.0.0:8000" \
--name django \
--cwd /srv/django \
--restart always

Terminal window
# Compiled binary (recommended for production)
cd /srv/api && go build -o bin/api ./cmd/api
lynxpm start ./bin/api \
--name go-api \
--cwd /srv/api \
--restart always \
--memory-max 128M \
--stop-signal SIGINT \
--stop-timeout 15000
# Development (go run)
lynxpm start "go run ./cmd/api" --name go-dev --cwd /srv/api

Go servers typically handle SIGINT for graceful shutdown:

ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
defer stop()
srv.Shutdown(ctx)

Terminal window
# Build and run
cd /srv/api && cargo build --release
lynxpm start ./target/release/api \
--name rust-api \
--cwd /srv/api \
--restart always \
--memory-max 64M

Terminal window
# Caddy (auto-HTTPS)
lynxpm start "caddy run --config /srv/site/Caddyfile" \
--name caddy \
--restart always \
--stop-signal SIGINT
# Python simple server (quick sharing)
lynxpm start "python3 -m http.server 8080" \
--name static \
--cwd /srv/site

Terminal window
# Run a backup script every 6 hours
lynxpm start "/srv/scripts/backup.sh" \
--name backup \
--schedule "0 */6 * * *" \
--restart never
# Run a health probe every 10 seconds (sidecar pattern)
lynxpm start "curl -sSf http://localhost:3000/healthz || exit 1" \
--name probe \
--schedule "@every 10s" \
--restart on-failure \
--shell

Each process runs as a unique synthetic user. Secrets never appear in /proc/<pid>/environ.

Terminal window
lynxpm start "node server.js" \
--name api \
--cwd /srv/api \
--isolation dynamic \
--env-file .env.production \
--restart always \
--memory-max 512M \
--stop-signal SIGINT \
--stop-timeout 15000

Runs inside user namespace + landlock. Can’t write to /home, /etc, /usr. Can write to cwd + /tmp.

Terminal window
lynxpm start "node server.js" \
--name api \
--cwd /srv/api \
--isolation sandbox \
--restart always

A complete workflow for deploying a Node.js API:

Terminal window
# 1. Install Lynx
sudo apt install ./lynxpm_*_amd64.deb
sudo usermod -aG lynxadm $USER && newgrp lynxadm
# 2. Make dev tools visible to the daemon
lynxpm install-tools
# 3. Prepare app directory
sudo mkdir -p /srv/api && sudo chown $USER:$USER /srv/api
cd /srv/api && git clone https://github.com/you/api.git .
npm install && npm run build
# 4. Create env file (secrets stay on disk, not in ps)
cat > .env.production <<EOF
DATABASE_URL=postgres://user:pass@db:5432/app
PORT=3000
NODE_ENV=production
EOF
# 5. Start with all hardening
lynxpm start "node dist/server.js" \
--name api \
--namespace prod \
--cwd /srv/api \
--env-file .env.production \
--isolation dynamic \
--restart always \
--memory-max 512M \
--stop-signal SIGINT \
--stop-timeout 30000
# 6. Scale to 3 workers
lynxpm scale prod:api 3
# 7. Verify
lynxpm list --namespace prod
lynxpm logs prod:api --follow
# 8. Enable boot persistence
sudo lynxpm startup

What lynxpm list --namespace prod shows after step 6:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ id β”‚ name β”‚ namespace β”‚ version β”‚ mode β”‚ pid β”‚ status β”‚ cpu β”‚ mem β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ 019d9... β”‚ api-1 β”‚ prod β”‚ 0.0.1 β”‚ forked β”‚ 12340 β”‚ running β”‚ 0% β”‚ 52 MB β”‚
β”‚ 019d9... β”‚ api-2 β”‚ prod β”‚ 0.0.1 β”‚ forked β”‚ 12341 β”‚ running β”‚ 0% β”‚ 48 MB β”‚
β”‚ 019d9... β”‚ api-3 β”‚ prod β”‚ 0.0.1 β”‚ forked β”‚ 12342 β”‚ running β”‚ 0% β”‚ 50 MB β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”˜

πŸ’‘ Tip: sudo lynxpm startup wires the lynxd.service into systemd so apps restart after reboot. All specs in ~/.config/lynx/apps/ are restored automatically at boot.


πŸ“œ Lynxfile.yml β€” declarative multi-app deploy

Section titled β€œπŸ“œ Lynxfile.yml β€” declarative multi-app deploy”

Instead of individual start commands, declare everything in a file:

Lynxfile.yml
version: "1"
namespace: prod
apps:
- name: api
command: node dist/server.js
cwd: /srv/api
env_file: .env.production
restart:
policy: always
max_restarts: 10
backoff: expo
- name: worker
command: node dist/worker.js
cwd: /srv/api
env_file: .env.production
restart:
policy: always
- name: scheduler
command: node dist/scheduler.js
cwd: /srv/api
restart:
policy: always
Terminal window
lynxpm apply Lynxfile.yml
lynxpm list --namespace prod

Update later:

Terminal window
# Edit Lynxfile.yml, then:
lynxpm delete --namespace prod # wipe the whole namespace in one shot
lynxpm apply Lynxfile.yml

Terminal window
# Live dashboard (refreshes every 2s, Ctrl+C to exit)
lynxpm monit
# JSON output for scripting
lynxpm list --json | jq '.[] | select(.state == "running") | {name, pid, memory}'
# Check restart history
lynxpm show api
# Reset counter after fixing a bug
lynxpm reset api
# View logs
lynxpm logs api --follow # both stdout+stderr
lynxpm logs api --stdout --lines 50 # only stdout, last 50 lines
# Flush old logs
lynxpm flush api

  1. Name your processes. --name api is easier to type than a UUID.
  2. Use namespaces. --namespace prod + --namespace staging keeps things clean. Filter with lynxpm list --namespace prod.
  3. Use namespace:name syntax. lynxpm show prod:api, lynxpm stop staging:worker.
  4. Bulk lifecycle ops by namespace. Every lifecycle command (stop, restart, reload, reset, delete, flush) accepts --namespace <ns> or the <ns>:* selector to target a whole namespace at once. Use '*' (quoted) to hit every managed process. Examples:
    Terminal window
    lynxpm restart --namespace prod # roll the prod tier
    lynxpm flush 'staging:*' # truncate logs across staging
    lynxpm delete --namespace prod --purge # wipe + drop logs
  5. Always set --restart always in production. Default on-failure doesn’t restart on clean exit.
  6. Set --memory-max in production. Prevents a single leak from killing the host. The daemon auto-restarts when the OOM kills the process.
  7. Use --stop-signal SIGINT for Node.js/Python. These runtimes handle SIGINT more gracefully than SIGTERM by default.
  8. Use --dry-run when unsure. lynxpm start "complex command" --dry-run prints the resolved spec without touching the daemon.
  9. Use --quiet in scripts. lynxpm start ... -q && echo ok keeps CI output clean.
  10. Export + apply for backups. lynxpm export --namespace prod > backup.yml saves your running config. Restore with lynxpm apply backup.yml.
  11. Shell completion saves keystrokes. lynxpm completion bash > ~/.local/share/bash-completion/completions/lynxpm