9.5 KiB
GhostGrid Deployment
This document describes how to deploy GhostGrid inside a Proxmox LXC container.
GhostGrid runs as a single Node.js process. In production mode, the Express backend serves both the built frontend from dist/ and the API on 0.0.0.0.
The container runs two GhostGrid instances in parallel. Each instance has its own working directory, SQLite database, port, and systemd service.
| Instance | Branch | Directory | Port | systemd Service |
|---|---|---|---|---|
| Production | main |
/opt/ghostgrid |
3000 |
ghostgrid |
| Staging | dev |
/opt/ghostgrid-dev |
3001 |
ghostgrid-dev |
Deployment Model
GhostGrid uses a manual deployment model:
git pull
npm run build
systemctl restart <service>
The helper script deploy/deploy.sh automates this flow. It accepts the target branch as an argument:
deploy/deploy.sh main
deploy/deploy.sh dev
If no argument is provided, the script defaults to main.
Access
Both instances are exposed directly in the LAN:
| Instance | URL |
|---|---|
| Production | http://<lxc-ip>:3000 |
| Staging | http://<lxc-ip>:3001 |
There is currently no reverse proxy and no TLS termination.
Recommended Setup: Automated Proxmox Installer
The recommended installation method is the standalone helper script:
deploy/proxmox-ghostgrid.sh
It follows the style of the Proxmox VE helper scripts and performs the full setup automatically.
The installer creates the LXC container, installs Node.js 20, clones both branches, builds both instances, and configures two systemd services:
| Service | Branch | Port |
|---|---|---|
ghostgrid |
main |
3000 |
ghostgrid-dev |
dev |
3001 |
Each instance receives its own randomly generated JWT_SECRET.
Run the installer on the Proxmox host as root.
Example using the script directly from the repository:
bash <(curl -fsSL https://git.airit.rocks/jbrueckner/GhostGrid/raw/branch/main/deploy/proxmox-ghostgrid.sh)
Alternatively, copy the script locally and run it:
bash proxmox-ghostgrid.sh
The script asks for the container ID, hostname, resources, network settings, and a read-only Gitea access token.
The token is only required while the repository is private. If the repository is public, the token can be left empty.
At the end of the installation, the script prints both application URLs:
http://<lxc-ip>:3000
http://<lxc-ip>:3001
Updates after the initial installation are handled through:
deploy/deploy.sh main
deploy/deploy.sh dev
Manual Installation
The following steps describe a manual installation of the production instance on branch main.
The staging instance follows the same pattern, using branch dev, directory /opt/ghostgrid-dev, service ghostgrid-dev, and port 3001.
1. Create the LXC Container
Create the container on the Proxmox host.
Recommended baseline:
- Debian 12 template
- Unprivileged container
- 1–2 vCPU
- 1 GB RAM
- 8 GB disk
- Static IP or DHCP reservation in the LAN
Example using the Proxmox CLI:
pct create <vmid> local:vztmpl/debian-12-standard_*.tar.zst \
--hostname ghostgrid \
--cores 2 \
--memory 1024 \
--rootfs local-lvm:8 \
--net0 name=eth0,bridge=vmbr0,ip=dhcp \
--unprivileged 1
pct start <vmid>
pct enter <vmid>
Replace <vmid> with the desired container ID.
2. Install Base Packages and Node.js 20 LTS
Run the following commands inside the container:
apt update
apt install -y curl git ca-certificates build-essential python3
curl -fsSL https://deb.nodesource.com/setup_20.x | bash -
apt install -y nodejs
node --version
The expected Node.js version is v20.x.
build-essential and python3 are fallback dependencies in case better-sqlite3 cannot use a prebuilt binary and needs to compile locally.
3. Create the Service User and Clone the Repository
Create a dedicated system user:
useradd --system \
--create-home \
--home-dir /opt/ghostgrid \
--shell /usr/sbin/nologin \
ghostgrid
Repository:
https://git.airit.rocks/jbrueckner/GhostGrid.git
For unattended deployments, the ghostgrid user needs read-only repository access.
There are two recommended options.
Option A: SSH Deploy Key
Create an SSH key for the service user:
sudo -u ghostgrid ssh-keygen -t ed25519 -N "" -f /opt/ghostgrid/.ssh/id_ed25519
sudo -u ghostgrid cat /opt/ghostgrid/.ssh/id_ed25519.pub
Add the public key in Gitea:
Repository → Settings → Deploy Keys → Add Deploy Key
Keep the deploy key read-only.
Accept the host key and clone the repository via SSH:
sudo -u ghostgrid ssh-keyscan git.airit.rocks >> /opt/ghostgrid/.ssh/known_hosts
sudo -u ghostgrid git clone git@git.airit.rocks:jbrueckner/GhostGrid.git /opt/ghostgrid
If Gitea uses a non-default SSH port, configure it either with -p <port> or through /opt/ghostgrid/.ssh/config.
When the repository is cloned via SSH, deploy/deploy.sh can use the configured Git remote without further changes.
Option B: HTTPS with Read-Only Access Token
Create a Gitea access token with read-only repository permissions, for example with the read:repository scope.
Clone the repository using the token:
sudo -u ghostgrid git clone \
https://oauth2:<READ_ONLY_TOKEN>@git.airit.rocks/jbrueckner/GhostGrid.git \
/opt/ghostgrid
Protect the Git config because the token is stored in .git/config:
chmod 600 /opt/ghostgrid/.git/config
4. Configure the Application Secret
Create /opt/ghostgrid/.env.
The application loads this file through dotenv.
printf 'JWT_SECRET="%s"\n' "$(openssl rand -hex 32)" > /opt/ghostgrid/.env
chown ghostgrid:ghostgrid /opt/ghostgrid/.env
chmod 600 /opt/ghostgrid/.env
The JWT_SECRET must be unique and must not be committed to Git.
5. Build the Application and Enable the systemd Service
cd /opt/ghostgrid
sudo -u ghostgrid npm ci
sudo -u ghostgrid npm run build
cp deploy/ghostgrid.service /etc/systemd/system/ghostgrid.service
systemctl daemon-reload
systemctl enable --now ghostgrid
systemctl status ghostgrid
The service should be active (running).
6. Allow Controlled Service Restarts for Deployments
The unprivileged ghostgrid user needs permission to restart the GhostGrid services during deployment.
Create the sudoers file:
nano /etc/sudoers.d/ghostgrid
Add the following rule:
ghostgrid ALL=(root) NOPASSWD: /usr/bin/systemctl restart ghostgrid, /usr/bin/systemctl restart ghostgrid-dev
Validate the sudoers file if desired:
visudo -c
7. Future Deployments
Deploy production:
sudo -u ghostgrid /opt/ghostgrid/deploy/deploy.sh main
Deploy staging:
sudo -u ghostgrid /opt/ghostgrid-dev/deploy/deploy.sh dev
Running deploy.sh without an argument deploys main.
Verification
After installation or deployment, verify the following:
-
Both services are running:
systemctl status ghostgrid ghostgrid-dev -
Recent logs do not show crashes:
journalctl -u ghostgrid -n 50 journalctl -u ghostgrid-dev -n 50 -
Both applications are reachable in the browser:
http://<lxc-ip>:3000 http://<lxc-ip>:3001 -
Login or registration loads correctly.
-
Create a device and reload the page. The device should remain available, confirming SQLite persistence.
-
Restart the service or reboot the LXC container. Login and data should still be available.
-
Verify the deployment flow:
local change → push to dev → deploy.sh dev → test → merge to main → deploy.sh main
Manual Setup of the Staging Instance
The automated installer creates the staging instance automatically.
For a manual setup, repeat the production steps with the following changes:
| Setting | Production | Staging |
|---|---|---|
| Branch | main |
dev |
| Directory | /opt/ghostgrid |
/opt/ghostgrid-dev |
| Port | 3000 |
3001 |
| Service | ghostgrid |
ghostgrid-dev |
| Database | /opt/ghostgrid/ghostgrid.db |
/opt/ghostgrid-dev/ghostgrid.db |
Example:
sudo -u ghostgrid git clone --branch dev <REPO-URL> /opt/ghostgrid-dev
printf 'JWT_SECRET="%s"\n' "$(openssl rand -hex 32)" > /opt/ghostgrid-dev/.env
chown ghostgrid:ghostgrid /opt/ghostgrid-dev/.env
chmod 600 /opt/ghostgrid-dev/.env
cd /opt/ghostgrid-dev
sudo -u ghostgrid npm ci
sudo -u ghostgrid npm run build
cp deploy/ghostgrid-dev.service /etc/systemd/system/ghostgrid-dev.service
systemctl daemon-reload
systemctl enable --now ghostgrid-dev
Notes
Each instance uses its own SQLite database:
| Instance | Database |
|---|---|
| Production | /opt/ghostgrid/ghostgrid.db |
| Staging | /opt/ghostgrid-dev/ghostgrid.db |
The database files are intentionally not tracked in Git.
For backups, include the SQLite database and its related WAL/SHM files if present:
ghostgrid.db
ghostgrid.db-wal
ghostgrid.db-shm
Ports 3000 and 3001 are exposed directly in the LAN.
If a reverse proxy or TLS termination is required later, add it separately, for example with nginx or Caddy forwarding to:
127.0.0.1:3000
127.0.0.1:3001
Device status is provided by CheckMK.
To enable periodic status synchronization, configure the following variables in the relevant .env file:
CHECKMK_API_URL=
CHECKMK_API_SECRET=
If these variables are not set, CheckMK synchronization is disabled. Devices remain in status unknown and are not available for booking.