Installation
Stand up a self-hosted Nosdesk instance from the official Docker image, from zero to first admin login.
Nosdesk ships as a single application container that serves both the HTTP API and the built frontend, alongside PostgreSQL and Redis. The supported install path is Docker Compose pulling the official image from the GitHub Container Registry (ghcr.io/nosdesk/nosdesk). This guide takes you from a clean host to a running instance with an admin account.
There is nothing to compile and no repository to clone. You create two files, a compose.yaml and a .env, and pull a prebuilt image.
Prerequisites
- A Linux host (or any machine running Docker). 2 vCPU and 2 GB RAM is enough to start.
- Docker Engine 24+ and the Docker Compose plugin.
- A domain name pointing at the host, and a way to terminate TLS in front of it (see step 6). Nosdesk runs in production mode with
Securecookies, so the browser login must be reached over HTTPS.
1. Create the project directory
mkdir nosdesk && cd nosdesk Everything below lives in this one directory.
2. Create compose.yaml
Save the following as compose.yaml. It pulls the published Nosdesk image and runs Postgres and Redis alongside it. Secrets are read from .env (created in the next step); the database and cache expand their passwords from that file at runtime, so you never have to repeat them in this file.
name: nosdesk
services:
nosdesk:
image: ghcr.io/nosdesk/nosdesk:latest
restart: unless-stopped
env_file: .env
volumes:
- backend_uploads:/app/uploads
# Optional: drop signed plugin zips into ./plugins to provision
# them on startup. Uncomment and create the directory if you use it.
# - ./plugins:/app/plugins:ro
ports:
- "127.0.0.1:8080:8080"
networks:
- nosdesk-network
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
interval: 30s
timeout: 10s
retries: 3
postgres:
image: postgres:17-bookworm
restart: unless-stopped
env_file: .env
volumes:
- postgres_data:/var/lib/postgresql/data
networks:
- nosdesk-network
healthcheck:
test: ["CMD-SHELL", 'pg_isready -U "$POSTGRES_USER" -d "$POSTGRES_DB"']
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
redis:
image: redis:8-alpine
restart: unless-stopped
env_file: .env
command: sh -c 'exec redis-server --appendonly yes --requirepass "$REDIS_PASSWORD"'
volumes:
- redis_data:/data
networks:
- nosdesk-network
healthcheck:
test: ["CMD-SHELL", 'redis-cli -a "$REDIS_PASSWORD" --no-auth-warning ping | grep -q PONG']
interval: 10s
timeout: 3s
retries: 5
volumes:
postgres_data:
redis_data:
backend_uploads:
networks:
nosdesk-network:
driver: bridge The published image carries the official plugin-registry trust root, so official-tier plugins and registry sync work out of the box with no extra build step.
3. Create .env
Save the following as .env in the same directory, then fill in real values. The four secrets are mandatory: the instance refuses to boot in production with the example placeholders.
# --- Required secrets -------------------------------------------------
# Signing key for session tokens
JWT_SECRET=
# 32-byte (64 hex char) key for at-rest encryption of MFA secrets,
# channel credentials, and plugin secrets
MFA_KEK_V1=
# Database and cache passwords
POSTGRES_PASSWORD=
REDIS_PASSWORD=
# --- Connection strings -----------------------------------------------
# DATABASE_URL and REDIS_URL embed the credentials. Keep the user and
# password in DATABASE_URL matching POSTGRES_USER / POSTGRES_PASSWORD, and
# the password in REDIS_URL matching REDIS_PASSWORD. If you change the user
# or database name, change them in both places.
DATABASE_URL=postgres://nosdesk:CHANGE_ME@postgres:5432/nosdesk
REDIS_URL=redis://:CHANGE_ME@redis:6379
POSTGRES_USER=nosdesk
POSTGRES_DB=nosdesk
# --- Deployment -------------------------------------------------------
ENVIRONMENT=production
# Inside the container the backend must listen on all interfaces so the
# published port can reach it. Host exposure is still restricted to
# loopback by the "127.0.0.1:8080:8080" port mapping in compose.yaml.
HOST=0.0.0.0
# Your public HTTPS URL. Drives CORS and cookie scope.
FRONTEND_URL=https://help.example.com Generate the secrets and paste them in:
openssl rand -base64 32 # JWT_SECRET
openssl rand -hex 32 # MFA_KEK_V1
openssl rand -base64 24 # POSTGRES_PASSWORD (also put in DATABASE_URL)
openssl rand -base64 24 # REDIS_PASSWORD (also put in REDIS_URL) DATABASE_URL and REDIS_URL embed the passwords, so update them to match the values you generated. See the Configuration reference for every available setting; for a first boot the values above are enough.
The
ALLOW_INSECURE_DEFAULT_SECRETSflag exists only to let throwaway test environments boot with placeholder passwords. Never set it in production.
4. Start the stack
docker compose up -d Compose pulls three images and starts them:
- nosdesk (
ghcr.io/nosdesk/nosdesk:latest): the application, serving the API and frontend on port 8080. - postgres (
postgres:17-bookworm): the database, on a persistentpostgres_datavolume. Migrations run automatically on startup. - redis (
redis:8-alpine): distributed rate limiting and ephemeral state.
The nosdesk service waits for Postgres and Redis to report healthy before it starts, so the first up may sit for a few seconds on a cold start while the database initialises.
5. Verify it’s running
The backend exposes two health endpoints:
# Liveness: returns 200 as soon as the process is up (no I/O)
curl http://localhost:8080/health
# Readiness: returns 200 only when the database and Redis are reachable
curl http://localhost:8080/readiness A readiness check that returns 503 with a Retry-After header means the backend is up but still waiting on a dependency. Give it a few seconds on a cold start.
6. Put it behind TLS
The published port binds to 127.0.0.1:8080, so the instance is reachable only from the host. Serve it to the world through a reverse proxy that terminates TLS and forwards to 127.0.0.1:8080. Point the proxy at the domain you set in FRONTEND_URL.
If you use passkeys, also set WEBAUTHN_RP_ORIGIN (and WEBAUTHN_RP_ID) to that same HTTPS origin so the WebAuthn checks line up. See the Configuration reference.
Because the instance runs in production mode, session cookies are flagged Secure and only sent over HTTPS. Reach the next step through your HTTPS URL, not plain http://localhost.
For example nginx and Caddy configurations — including the WebSocket and X-Forwarded-For details that matter — see the Reverse proxy + TLS guide.
7. Create the first admin
Open your HTTPS URL in a browser. Nosdesk detects that no users exist and shows the first-run setup screen. Fill in the admin name, email, and password. That account is created with workspace-admin rights and seeds a few starter categories so the app is not empty on first login.
First-run setup is gated by a one-time bootstrap token. The server prints a ready-to-click setup link (token embedded) to its startup logs; you can also print it on demand:
docker compose exec nosdesk nosdesk-cli setup-token The token is valid for a limited window — 30 minutes by default, configurable via BOOTSTRAP_TOKEN_TTL_SECONDS — and is cleared once setup completes. If the link expires before you finish, the setup screen tells you it is no longer valid. Because no admin account exists yet, restarting the server mints a fresh token and prints a new link:
docker compose restart nosdesk Then grab the new link from the logs (or nosdesk-cli setup-token) and continue. Note that setup-token only prints an existing, still-valid token; it cannot mint one once the previous token has lapsed, which is why a restart is the way to recover.
For headless or automated provisioning, pre-seed the admin instead with the INITIAL_ADMIN_NAME, INITIAL_ADMIN_EMAIL, and INITIAL_ADMIN_PASSWORD_HASH variables. See the Configuration reference.
Everyday operations
# Tail logs
docker compose logs -f nosdesk
# Update to the latest image (see the upgrade guide for the full procedure)
docker compose pull && docker compose up -d
# Stop the stack (data volumes are preserved)
docker compose down To back up and restore your data, see the Backup & restore guide. Configuration questions are answered in the Configuration reference.