---
title: Server
description: Run Gitupcms on a Linux VM with systemd and Caddy. This is how app.gitupcms.com runs.
---

app.gitupcms.com runs on an AWS Lightsail instance (4 GB, Ubuntu) with Caddy in front for HTTPS. Any Linux VM works the same way.

1. **Install Node, pnpm and Caddy**

    ```bash
    curl -fsSL https://deb.nodesource.com/setup_24.x | sudo -E bash -
    sudo apt-get install -y nodejs build-essential git rsync
    sudo npm i -g pnpm@10
    # Caddy: see caddyserver.com/docs/install
    ```

2. **Create a service user and folders**

    ```bash
    sudo useradd --system --home /var/lib/gitupcms --shell /usr/sbin/nologin gitupcms
    sudo mkdir -p /opt/gitupcms /var/lib/gitupcms/data /var/lib/gitupcms/previews
    sudo chown -R gitupcms:gitupcms /opt/gitupcms /var/lib/gitupcms
    ```

3. **Write the environment**

    ```ini /etc/gitupcms.env
    BETTER_AUTH_URL=https://cms.example.com
    BETTER_AUTH_SECRET=...
    GITHUB_CLIENT_ID=...
    GITHUB_CLIENT_SECRET=...
    DB_PATH=/var/lib/gitupcms/data/gitupcms.db
    HOST=127.0.0.1
    PORT=4321
    PREVIEW_RUNNER=1
    PREVIEW_DIR=/var/lib/gitupcms/previews
    PREVIEW_DOCKER=node:24-bookworm-slim
    ```
    Make it readable only by root and the service: `chown root:gitupcms`, `chmod 640`.

    `PREVIEW_DOCKER` runs each site's install and `astro dev` in a container. Install Docker, let the service use it and keep containers off the cloud metadata address:
    ```bash
    curl -fsSL https://get.docker.com | sudo sh
    sudo usermod -aG docker gitupcms
    sudo docker pull node:24-bookworm-slim
    sudo iptables -I DOCKER-USER -d 169.254.169.254 -j DROP   # persist with iptables-persistent
    ```
    Docker group membership is root-equivalent for the service user; leave `PREVIEW_DOCKER` unset if you don't run previews.

4. **Copy and build**

    ```bash
    rsync -az --exclude node_modules --exclude dist --exclude .env --exclude data ./cms/ server:/opt/gitupcms/
    sudo -u gitupcms sh -c 'cd /opt/gitupcms && pnpm install --frozen-lockfile && pnpm build'
    ```

5. **Run it with systemd**

    ```ini /etc/systemd/system/gitupcms.service
    [Unit]
    Description=Gitupcms
    After=network.target

    [Service]
    User=gitupcms
    Group=gitupcms
    WorkingDirectory=/opt/gitupcms
    EnvironmentFile=/etc/gitupcms.env
    Environment=NODE_ENV=production
    ExecStart=/usr/bin/node ./dist/server/entry.mjs
    Restart=always
    NoNewPrivileges=true
    ProtectSystem=strict
    ProtectHome=true
    PrivateTmp=true
    ReadWritePaths=/var/lib/gitupcms /opt/gitupcms /tmp

    [Install]
    WantedBy=multi-user.target
    ```
    ```bash
    sudo systemctl enable --now gitupcms
    ```

6. **HTTPS with Caddy**

    ```txt /etc/caddy/Caddyfile
    cms.example.com {
      encode zstd gzip
      reverse_proxy 127.0.0.1:4321 {
        flush_interval -1
      }
    }
    ```
    `flush_interval -1` keeps the AI Editor's streamed answers flowing. Point a DNS A record at the server and reload Caddy; it gets the certificate itself.

7. **Live preview (optional)**

    With `PREVIEW_HOST=https://*.preview.cms.example.com`, the AI Editor runs each page's scripts on its own subdomain. Add a wildcard A record (`*.preview.cms.example.com`) and a wildcard certificate, which needs a DNS challenge: a Caddy build with your DNS provider ([caddy-dns](https://github.com/caddy-dns)), e.g. Route 53:
    ```txt /etc/caddy/Caddyfile
    *.preview.cms.example.com {
      tls {
        dns route53
      }
      reverse_proxy 127.0.0.1:4322 {
        flush_interval -1
      }
    }
    ```
    Give Caddy the DNS credentials through a systemd `EnvironmentFile`, and drop `--environ` from its `ExecStart`, or it writes them to the log.

## Updating

Copy the new source, then build and restart:

```bash
sudo -u gitupcms sh -c 'cd /opt/gitupcms && pnpm install --frozen-lockfile && pnpm build'
sudo systemctl restart gitupcms
```

## Logs and backups

- Logs: `journalctl -u gitupcms -f`. Without `SMTP_URL`, email sign-in codes appear here.
- Back up the SQLite file in `DB_PATH` (for example with `sqlite3 gitupcms.db ".backup backup.db"`). Content is in git; the database holds users, workspaces, settings, media metadata, comments and feedback.
