395 lines
9.5 KiB
Markdown
395 lines
9.5 KiB
Markdown
# 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:
|
||
|
||
```bash
|
||
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:
|
||
|
||
```bash
|
||
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:
|
||
|
||
```text
|
||
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
|
||
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
|
||
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:
|
||
|
||
```text
|
||
http://<lxc-ip>:3000
|
||
http://<lxc-ip>:3001
|
||
```
|
||
|
||
Updates after the initial installation are handled through:
|
||
|
||
```bash
|
||
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:
|
||
|
||
```bash
|
||
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:
|
||
|
||
```bash
|
||
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:
|
||
|
||
```bash
|
||
useradd --system \
|
||
--create-home \
|
||
--home-dir /opt/ghostgrid \
|
||
--shell /usr/sbin/nologin \
|
||
ghostgrid
|
||
```
|
||
|
||
Repository:
|
||
|
||
```text
|
||
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:
|
||
|
||
```bash
|
||
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:
|
||
|
||
```text
|
||
Repository > Settings > Deploy Keys > Add Deploy Key
|
||
```
|
||
|
||
Keep the deploy key read-only.
|
||
|
||
Accept the host key and clone the repository via SSH:
|
||
|
||
```bash
|
||
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:
|
||
|
||
```bash
|
||
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`:
|
||
|
||
```bash
|
||
chmod 600 /opt/ghostgrid/.git/config
|
||
```
|
||
|
||
## 4. Configure the Application Secret
|
||
|
||
Create `/opt/ghostgrid/.env`.
|
||
|
||
The application loads this file through `dotenv`.
|
||
|
||
```bash
|
||
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
|
||
|
||
```bash
|
||
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:
|
||
|
||
```bash
|
||
nano /etc/sudoers.d/ghostgrid
|
||
```
|
||
|
||
Add the following rule:
|
||
|
||
```text
|
||
ghostgrid ALL=(root) NOPASSWD: /usr/bin/systemctl restart ghostgrid, /usr/bin/systemctl restart ghostgrid-dev
|
||
```
|
||
|
||
Validate the sudoers file if desired:
|
||
|
||
```bash
|
||
visudo -c
|
||
```
|
||
|
||
## 7. Future Deployments
|
||
|
||
Deploy production:
|
||
|
||
```bash
|
||
sudo -u ghostgrid /opt/ghostgrid/deploy/deploy.sh main
|
||
```
|
||
|
||
Deploy staging:
|
||
|
||
```bash
|
||
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:
|
||
|
||
1. Both services are running:
|
||
|
||
```bash
|
||
systemctl status ghostgrid ghostgrid-dev
|
||
```
|
||
|
||
2. Recent logs do not show crashes:
|
||
|
||
```bash
|
||
journalctl -u ghostgrid -n 50
|
||
journalctl -u ghostgrid-dev -n 50
|
||
```
|
||
|
||
3. Both applications are reachable in the browser:
|
||
|
||
```text
|
||
http://<lxc-ip>:3000
|
||
http://<lxc-ip>:3001
|
||
```
|
||
|
||
4. Login or registration loads correctly.
|
||
|
||
5. Create a device and reload the page. The device should remain available, confirming SQLite persistence.
|
||
|
||
6. Restart the service or reboot the LXC container. Login and data should still be available.
|
||
|
||
7. Verify the deployment flow:
|
||
|
||
```text
|
||
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:
|
||
|
||
```bash
|
||
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:
|
||
|
||
```text
|
||
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:
|
||
|
||
```text
|
||
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:
|
||
|
||
```env
|
||
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.
|