Self-Hosted
Docker Compose and bare-metal deployment paths, PostgreSQL, Caddy reverse proxy, systemd, and first-admin setup.
Self-Hosted is the right choice for teams that need on-premises deployment, compliance, or an air-gapped environment. It runs on PostgreSQL and supports any number of users.
Two deployment shapes are supported — pick whichever fits:
| Method | Best for | Pros | Difficulty |
|---|---|---|---|
| A. Docker Compose | Standard production, on-prem servers | One command brings up the full stack (DB + server + Caddy); upgrades and rollbacks are simple | ★☆☆ |
| B. Bare-metal | Existing PostgreSQL, fine-grained system integration, air-gapped / no-Docker hosts (Kylin, UOS, …) | Lower resource footprint, integrates with existing systemd / Nginx / internal CAs | ★★☆ |
Both methods run the same application: one niuniu-server process + a PostgreSQL database + a reverse proxy. The difference is only in packaging.
Common prerequisites
- Linux server (Ubuntu 22.04+ / Debian 12+ / RHEL 9+ / Alibaba Linux 3, etc.)
- At least 2 vCPU / 4 GB RAM / 20 GB disk
- A domain name pointed at the server (for HTTPS)
- Outbound network access to the Anthropic API (or your chosen third-party endpoint)
Method A: Docker Compose deployment
A.1 Prerequisites
- Docker 24+ and Docker Compose v2
# Ubuntu / Debian one-liner
curl -fsSL https://get.docker.com | sh
sudo systemctl enable --now docker
A.2 Download the deployment template
git clone https://github.com/niuniu-dev/niuniu.git
cd niuniu/relay/deploy
cp server.yaml.example server.yaml
Edit server.yaml and replace the two placeholders:
server:
host: 0.0.0.0
port: 3000
storage:
driver: postgres
postgres:
dsn: "postgres://niuniu:your-strong-password@postgres:5432/niuniu_server?sslmode=disable"
auth:
enabled: true
token_expiry: 15m
refresh_expiry: 168h
users:
- username: admin
password: your-admin-password
display_name: Admin
role: admin
agent:
default: claude-code
claude_code:
command: claude
args: []
workspace:
base_dir: /data/.niuniu/workspaces
harness:
enabled: true
Inside the container
HOME=/data, so workspaces default to/data/.niuniu/workspaces, persisted via theserver-datavolume.
A.3 Configure environment variables
Create .env under relay/deploy/:
POSTGRES_PASSWORD=match the strong password used in server.yaml DSN
TEAM_DOMAIN=team.example.com
RELAY_DOMAIN=relay.example.com # leave blank if not deploying the relay
DEMO_DOMAIN= # usually blank
WEBSITE_DOMAIN= # usually blank
WEBSITE_APEX_DOMAIN=
A.4 Start the stack
docker compose up -d postgres niuniu-server caddy
The first start initializes PostgreSQL (creates the niuniu_server database) and the server schema automatically.
A.5 Caddy reverse proxy
Caddyfile (sample shipped in the repo):
{$TEAM_DOMAIN} {
reverse_proxy niuniu-server:3000
}
Caddy will request an HTTPS certificate from Let’s Encrypt. If you use an internal CA, add tls /path/to/cert.pem /path/to/key.pem to the site block.
A.6 Verify
curl -I https://team.example.com/api/health
# Expected: HTTP/2 200
Open https://team.example.com in a browser and log in with admin and the password you set.
Method B: Bare-metal deployment
B.1 Install runtime dependencies
niuniu-server spawns git, node, python3, and claude as child processes for agent workflows, so install them up front:
# Debian / Ubuntu
sudo apt update
sudo apt install -y git curl ca-certificates python3 postgresql
# Node.js 20 LTS
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt install -y nodejs
# Claude Code and Codex CLI (either one or both)
sudo npm install -g @anthropic-ai/claude-code @openai/codex
On RHEL / Kylin / UOS, swap apt for dnf / yum; package names are identical.
B.2 Create the PostgreSQL database
sudo -u postgres psql <<'SQL'
CREATE USER niuniu WITH PASSWORD 'your-strong-password';
CREATE DATABASE niuniu_server OWNER niuniu;
GRANT ALL PRIVILEGES ON DATABASE niuniu_server TO niuniu;
SQL
If the database lives on a different host, allow the server’s subnet in pg_hba.conf and update the DSN below accordingly.
B.3 Get the niuniu-server binary
Option 1: download a pre-built release (recommended)
Grab niuniu-server-<version>-linux-<arch> from GitHub Releases, rename to niuniu-server, and install it:
sudo install -m 0755 niuniu-server-*-linux-amd64 /usr/local/bin/niuniu-server
Option 2: build from source
Requires Go 1.25+ and pnpm 9+:
git clone https://github.com/niuniu-dev/niuniu.git
cd niuniu
make build-linux
sudo install -m 0755 bin/niuniu-server-*-linux-amd64 /usr/local/bin/niuniu-server
B.4 Create the system user and directories
sudo useradd --system --home /var/lib/niuniu --create-home --shell /usr/sbin/nologin niuniu
sudo mkdir -p /etc/niuniu /var/lib/niuniu/.niuniu/workspaces
sudo chown -R niuniu:niuniu /var/lib/niuniu
B.5 Write the config
/etc/niuniu/config.yaml:
server:
host: 0.0.0.0
port: 3000
storage:
driver: postgres
postgres:
dsn: "postgres://niuniu:your-strong-password@localhost:5432/niuniu_server?sslmode=disable"
auth:
enabled: true
token_expiry: 15m
refresh_expiry: 168h
users:
- username: admin
password: your-admin-password
display_name: Admin
role: admin
agent:
default: claude-code
claude_code:
command: claude
args: []
workspace:
base_dir: /var/lib/niuniu/.niuniu/workspaces
harness:
enabled: true
sudo chown niuniu:niuniu /etc/niuniu/config.yaml
sudo chmod 0640 /etc/niuniu/config.yaml
B.6 systemd service unit
/etc/systemd/system/niuniu-server.service:
[Unit]
Description=Niuniu Team Server
Documentation=https://github.com/niuniu-dev/niuniu
After=network-online.target postgresql.service
Wants=network-online.target
[Service]
Type=simple
User=niuniu
Group=niuniu
Environment=HOME=/var/lib/niuniu
ExecStart=/usr/local/bin/niuniu-server --config /etc/niuniu/config.yaml
Restart=on-failure
RestartSec=5s
WorkingDirectory=/var/lib/niuniu
LimitNOFILE=65536
ProtectSystem=strict
ProtectHome=read-only
ReadWritePaths=/var/lib/niuniu
NoNewPrivileges=true
[Install]
WantedBy=multi-user.target
Start it:
sudo systemctl daemon-reload
sudo systemctl enable --now niuniu-server
sudo systemctl status niuniu-server
journalctl -u niuniu-server -f # live logs
Note the
Environment=HOME=/var/lib/niuniuline — theclaudeCLI stores OAuth credentials under$HOME/.claude/, soHOMEmust match the systemd user’s real home directory, otherwise the agent will fail to log in to the model.
B.7 Reverse proxy (Caddy example)
sudo apt install -y caddy
/etc/caddy/Caddyfile:
team.example.com {
reverse_proxy localhost:3000
}
sudo systemctl reload caddy
Nginx users can use the following equivalent — note that WebSocket / SSE require proxy_http_version 1.1 plus the Upgrade header:
server {
listen 443 ssl http2;
server_name team.example.com;
ssl_certificate /etc/letsencrypt/live/team.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/team.example.com/privkey.pem;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
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_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
}
}
B.8 Verify
curl -I https://team.example.com/api/health
# Expected: HTTP/2 200
Open https://team.example.com in a browser and log in with admin and the password you set.
First admin
Both methods use the admin account from auth.users in server.yaml / config.yaml for first login. Once logged in, immediately:
- Go to Settings → Users and change the admin password to a new value — then write the new value back into the config file so the plaintext password doesn’t linger
- Create regular member accounts; keep
adminfor operations only, not daily development - (Optional) enable MFA from Settings → Users → Security (TOTP)
Backups
PostgreSQL is the critical asset — schedule a weekly pg_dump:
# Docker Compose
docker compose exec postgres pg_dump -U niuniu niuniu_server | gzip > backup-$(date +%F).sql.gz
# Bare metal
sudo -u postgres pg_dump niuniu_server | gzip > backup-$(date +%F).sql.gz
Also include the workspace tree (Docker: the server-data volume / bare metal: /var/lib/niuniu/.niuniu/) in the same incremental backup. Retain 4–8 weeks.
Upgrading
Docker Compose:
cd niuniu/relay/deploy
git pull
docker compose pull
docker compose up -d --force-recreate niuniu-server
Bare metal:
# Download the new binary (or rerun make build-linux)
sudo systemctl stop niuniu-server
sudo install -m 0755 niuniu-server-*-linux-amd64 /usr/local/bin/niuniu-server
sudo systemctl start niuniu-server
journalctl -u niuniu-server -f
Always back up the database before upgrading. Schema migrations run automatically on service start; a failed migration keeps the old schema intact instead of corrupting data.
Next steps
- Core concepts — understand organizations, members, and resource ownership
- 5-minute quickstart — get your first workspace running