Tutorials
Real-world recipes. Copy-paste and adapt.
π― Pick your stack
Section titled βπ― Pick your stackβ| Stack | Jump to | Time |
|---|---|---|
| β² Next.js | Next.js | 3 min |
| π’ Express / Fastify | Express / Fastify (Node.js) | 2 min |
| π₯ Bun | Bun | 1 min |
| π FastAPI + Uvicorn | Python β FastAPI + Uvicorn | 2 min |
| π¦ Django + Gunicorn | Python β Django + Gunicorn | 2 min |
| πΉ Go web server | Go web server | 2 min |
| π¦ Rust (Actix/Axum) | Rust (Actix / Axum) | 2 min |
| π Static site | Static site server (Caddy / Nginx) | 1 min |
| β° Cron / scheduled | Cron / scheduled tasks | 1 min |
| π Production hardening | Secure isolation (production) | 3 min |
| π Full deploy walkthrough | Full production deploy (step by step) | 10 min |
| π Lynxfile (declarative) | Lynxfile.yml β declarative multi-app deploy | 5 min |
| π Monitor & debug | Monitoring and debugging | 1 min |
| π‘ Daily-use tips | Tips | - |
π‘ Tip: all examples work identically in user mode (
lynxd &) and system mode (sudo systemctl start lynxd). The only difference in prod: swap--isolation selffor--isolation dynamic.
β² Next.js
Section titled ββ² Next.jsβDevelopment
Section titled βDevelopmentβ# Inside your Next.js project directorylynxpm start "npm run dev" --name nextjs-dev --cwd /srv/myapp --shelllynxpm logs nextjs-dev --followWhat 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.1sProduction (standalone build)
Section titled βProduction (standalone build)β# 1. Build firstcd /srv/myapp && npm run build
# 2. Start the standalone serverlynxpm start "node .next/standalone/server.js" \ --name nextjs-prod \ --cwd /srv/myapp \ --restart always \ --env-file .env.production \ --memory-max 512M
# 3. Verifylynxpm show nextjs-prodProduction + multiple instances (cluster-like)
Section titled βProduction + multiple instances (cluster-like)βNext.js standalone doesnβt support Node cluster natively. Use --scale
instead β each instance listens on a different port:
# Start 3 instances; each reads LYNX_INSTANCE to pick a portlynxpm 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; }}Scale up / down on the fly
Section titled βScale up / down on the flyβlynxpm scale nextjs 5 # add 2 more instanceslynxpm scale nextjs 2 # drop back to 2Output:
Scaled nextjs: 3 β 5 + nextjs-4 + nextjs-5β οΈ Warning: Each instance must bind a unique port. Read
LYNX_INSTANCE(0-based) and computeport = 3000 + LYNX_INSTANCE.
π’ Express / Fastify (Node.js)
Section titled βπ’ Express / Fastify (Node.js)β# Simplelynxpm start "node server.js" --name api --cwd /srv/api --restart always
# With env filelynxpm 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+NGraceful shutdown (Express)
Section titled βGraceful shutdown (Express)βExpress needs SIGINT to close connections cleanly:
lynxpm start "node server.js" \ --name api \ --stop-signal SIGINT \ --stop-timeout 30000 \ --restart alwaysIn your Express app:
process.on('SIGINT', () => { server.close(() => process.exit(0));});π₯ Bun
Section titled βπ₯ Bunβ# Devlynxpm start "bun run dev" --name bun-dev --cwd /srv/app
# Productionlynxpm 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π Python β FastAPI + Uvicorn
Section titled βπ Python β FastAPI + Uvicornβ# 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π¦ Python β Django + Gunicorn
Section titled βπ¦ Python β Django + Gunicornβ# Via uvlynxpm 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 venvlynxpm start "/srv/django/.venv/bin/gunicorn myproject.wsgi:application -b 0.0.0.0:8000" \ --name django \ --cwd /srv/django \ --restart alwaysπΉ Go web server
Section titled βπΉ Go web serverβ# Compiled binary (recommended for production)cd /srv/api && go build -o bin/api ./cmd/apilynxpm 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/apiGo servers typically handle SIGINT for graceful shutdown:
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)defer stop()srv.Shutdown(ctx)π¦ Rust (Actix / Axum)
Section titled βπ¦ Rust (Actix / Axum)β# Build and runcd /srv/api && cargo build --releaselynxpm start ./target/release/api \ --name rust-api \ --cwd /srv/api \ --restart always \ --memory-max 64Mπ Static site server (Caddy / Nginx)
Section titled βπ Static site server (Caddy / Nginx)β# 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β° Cron / scheduled tasks
Section titled ββ° Cron / scheduled tasksβ# Run a backup script every 6 hourslynxpm 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π Secure isolation (production)
Section titled βπ Secure isolation (production)βDynamicUser (system mode, strongest)
Section titled βDynamicUser (system mode, strongest)βEach process runs as a unique synthetic user. Secrets never appear in
/proc/<pid>/environ.
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 15000Sandbox (user mode, no sudo)
Section titled βSandbox (user mode, no sudo)βRuns inside user namespace + landlock. Canβt write to /home, /etc,
/usr. Can write to cwd + /tmp.
lynxpm start "node server.js" \ --name api \ --cwd /srv/api \ --isolation sandbox \ --restart alwaysπ Full production deploy (step by step)
Section titled βπ Full production deploy (step by step)βA complete workflow for deploying a Node.js API:
# 1. Install Lynxsudo apt install ./lynxpm_*_amd64.debsudo usermod -aG lynxadm $USER && newgrp lynxadm
# 2. Make dev tools visible to the daemonlynxpm install-tools
# 3. Prepare app directorysudo mkdir -p /srv/api && sudo chown $USER:$USER /srv/apicd /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 <<EOFDATABASE_URL=postgres://user:pass@db:5432/appPORT=3000NODE_ENV=productionEOF
# 5. Start with all hardeninglynxpm 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 workerslynxpm scale prod:api 3
# 7. Verifylynxpm list --namespace prodlynxpm logs prod:api --follow
# 8. Enable boot persistencesudo lynxpm startupWhat 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 startupwires thelynxd.serviceinto 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:
version: "1"namespace: prodapps: - 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: alwayslynxpm apply Lynxfile.ymllynxpm list --namespace prodUpdate later:
# Edit Lynxfile.yml, then:lynxpm delete --namespace prod # wipe the whole namespace in one shotlynxpm apply Lynxfile.ymlπ Monitoring and debugging
Section titled βπ Monitoring and debuggingβ# Live dashboard (refreshes every 2s, Ctrl+C to exit)lynxpm monit
# JSON output for scriptinglynxpm list --json | jq '.[] | select(.state == "running") | {name, pid, memory}'
# Check restart historylynxpm show api
# Reset counter after fixing a buglynxpm reset api
# View logslynxpm logs api --follow # both stdout+stderrlynxpm logs api --stdout --lines 50 # only stdout, last 50 lines
# Flush old logslynxpm flush apiπ‘ Tips
Section titled βπ‘ Tipsβ- Name your processes.
--name apiis easier to type than a UUID. - Use namespaces.
--namespace prod+--namespace stagingkeeps things clean. Filter withlynxpm list --namespace prod. - Use
namespace:namesyntax.lynxpm show prod:api,lynxpm stop staging:worker. - 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 tierlynxpm flush 'staging:*' # truncate logs across staginglynxpm delete --namespace prod --purge # wipe + drop logs - Always set
--restart alwaysin production. Defaulton-failuredoesnβt restart on clean exit. - Set
--memory-maxin production. Prevents a single leak from killing the host. The daemon auto-restarts when the OOM kills the process. - Use
--stop-signal SIGINTfor Node.js/Python. These runtimes handle SIGINT more gracefully than SIGTERM by default. - Use
--dry-runwhen unsure.lynxpm start "complex command" --dry-runprints the resolved spec without touching the daemon. - Use
--quietin scripts.lynxpm start ... -q && echo okkeeps CI output clean. - Export + apply for backups.
lynxpm export --namespace prod > backup.ymlsaves your running config. Restore withlynxpm apply backup.yml. - Shell completion saves keystrokes.
lynxpm completion bash > ~/.local/share/bash-completion/completions/lynxpm