Skip to content

Remote Deploy

Deploy to a remote Linux server (VPS, dedicated server, or cloud VM) via SSH. The deploy system uses rsync (or scp fallback) to upload your project and Docker Compose to run it on the remote host.

How Remote Deploy Works

Your Machine                        Remote Server (VPS)
     │                                      │
     │  Step 1: Configure workflow          │
     │  deploy/workflow.remote.json         │
     │                                      │
     │  Step 2: Run remote workflow         │
     │  maker deploy:workflow:remote        │
     │                                      │
     │  ── ssh mkdir -p /home/deploy/app ──>│
     │                                      │
     │  ── rsync ./ user@host:/target/ ────>│
     │      (excludes node_modules, .git,   │
     │       dist, .env*)                   │
     │                                      │
     │  ── ssh docker network create ─────> │
     │  ── ssh docker compose up -d ───────>│
     │      (server infra)                  │
     │  ── ssh docker compose up -d ───────>│
     │      --build (app)                   │
     │                                      │
     │                              ┌───────┴────────┐
     │                              │  App running at │
     │                              │  your-domain.com │
     │                              └────────────────┘

Prerequisites

  • Remote server running Linux with Docker and Docker Compose installed
  • SSH access with key-based authentication
  • A domain pointing to your server's IP (for nginx-proxy + letsencrypt SSL)

TIP

You don't need to create deploy/.env or deploy/server/.env manually. Just edit the .env.example files generated by deploy:init, and the workflow will copy them to .env automatically on first run.

Step 1 — Generate Deploy Scaffolding

Run this locally (one-time):

bash
npm run maker deploy:init
bash
pnpm maker deploy:init
bash
yarn maker deploy:init
bash
bun maker deploy:init

This generates the full deploy/ directory — including deploy/workflow.remote.json. See Deploy Overview for details.

Step 2 — Configure Remote Workflow

The deploy:init command already created deploy/workflow.remote.json. Edit it with your server's details:

json
{
  "remote": {
    "host": "203.0.113.10",
    "user": "deploy",
    "port": 22,
    "keyPath": "~/.ssh/id_rsa",
    "targetPath": "/home/deploy/nexgen"
  },
  "upload": {
    "source": ".",
    "targetSubPath": "."
  },
  "databaseImport": {
    "enabled": false,
    "file": "deploy/nexgen.sql",
    "database": "nexgen",
    "container": "mysql-global",
    "user": "root"
  },
  "preDeployCommands": [
    "docker rm -f old-app 2>/dev/null || true"
  ]
}
FieldPurpose
remote.hostServer IP or hostname
remote.userSSH user
remote.portSSH port (default 22)
remote.keyPathPath to your SSH private key
remote.targetPathDirectory on the remote server where the project will be uploaded
upload.sourceLocal directory to upload (default .)
upload.targetSubPathSubdirectory under targetPath for the upload
databaseImport.enabledWhether to import a SQL dump after deploy
databaseImport.filePath to the SQL dump file
databaseImport.databaseTarget database name
databaseImport.containerDocker container name (mysql-global or postgres-global)
databaseImport.userDatabase user (root / postgres)
preDeployCommandsCommands run on the remote host before starting the app

rsync options

By default the CLI uses scp to upload files. To use rsync instead (faster incremental sync), add these fields to the config:

FieldPurpose
rsyncPathPath to the rsync binary on the remote host (e.g. "rsync")
rsyncSshPathPath to the SSH binary used by rsync (e.g. "ssh")
rsyncSshOptionsExtra SSH options passed to rsync (e.g. ["-o", "StrictHostKeyChecking=no"])

Example with rsync enabled:

json
{
  "remote": { ... },
  "rsyncPath": "rsync",
  "rsyncSshPath": "ssh",
  "rsyncSshOptions": ["-o", "StrictHostKeyChecking=no"]
}

When rsyncPath is not set, the CLI falls back to scp. Both approaches exclude node_modules, .git, dist, and .env* from the upload.

Step 3 — Deploy

Full pipeline (upload + server infra + app)

bash
npm run maker deploy:workflow:remote
bash
pnpm maker deploy:workflow:remote
bash
yarn maker deploy:workflow:remote
bash
bun maker deploy:workflow:remote

This single command:

  1. Creates target directory on the remote server: ssh user@host mkdir -p /home/deploy/nexgen
  2. Runs pre-deploy commands on the remote host (if any are defined in preDeployCommands)
  3. Uploads the project via rsync (excludes node_modules, .git, dist, .env*, logs):
    bash
    rsync -avz --delete --exclude=node_modules --exclude=.git \
      --exclude=dist --exclude=/.env* --exclude=*.log \
      -e "ssh -p 22 -i ~/.ssh/id_rsa" \
      ./ deploy@203.0.113.10:/home/deploy/nexgen/
  4. Creates Docker networks on remote (if missing):
    bash
    ssh user@host docker network create nginx-proxy
    ssh user@host docker network create infra
  5. Starts server infra on remote:
    bash
    ssh user@host docker compose -f /path/to/deploy/server/docker-compose.yml up -d
  6. Syncs pgAdmin servers.json — loads the server configuration into the pgAdmin container
  7. Builds and starts app on remote:
    bash
    ssh user@host docker compose -f /path/to/deploy/docker-compose.yml up -d --build --force-recreate
  8. Imports database (if databaseImport.enabled: true)

Step by step

Alternatively, run parts of the pipeline:

bash
npm run maker deploy:workflow:remote -- --server-only
npm run maker deploy:workflow:remote -- --app-only
npm run maker deploy:db:import:remote -- --file=deploy/nexgen.sql --database=nexgen
bash
pnpm maker deploy:workflow:remote --server-only
pnpm maker deploy:workflow:remote --app-only
pnpm maker deploy:db:import:remote --file=deploy/nexgen.sql --database=nexgen
bash
yarn maker deploy:workflow:remote --server-only
yarn maker deploy:workflow:remote --app-only
yarn maker deploy:db:import:remote --file=deploy/nexgen.sql --database=nexgen
bash
bun maker deploy:workflow:remote --server-only
bun maker deploy:workflow:remote --app-only
bun maker deploy:db:import:remote --file=deploy/nexgen.sql --database=nexgen

deploy:db:import:remote auto-detects MySQL or PostgreSQL from the databaseImport.container value in the workflow config.

SSL with letsencrypt

The server infra includes jrcs/letsencrypt-nginx-proxy-companion for automatic SSL certificates. To use it:

  1. Set your domain in deploy/.env:

    VIRTUAL_HOST=app.example.com
    LETSENCRYPT_HOST=app.example.com
    LETSENCRYPT_EMAIL=admin@example.com
  2. Configure the nginx vhost override in deploy/server/nginx-vhost/app.example.com:

    # This file is mounted into nginx-proxy
    # Customize proxy settings if needed
  3. Ensure your domain's DNS points to the server's IP

  4. Run deploy:workflow:remote — nginx-proxy routes your domain to the app container and letsencrypt automatically obtains and renews certificates.

Promote Workflow

The promote workflow runs the full local pipeline (tests the build on your machine) then deploys to remote:

bash
npm run maker deploy:workflow:promote
bash
pnpm maker deploy:workflow:promote
bash
yarn maker deploy:workflow:promote
bash
bun maker deploy:workflow:promote

This is useful for pre-production validation — if the local build succeeds, it proceeds to deploy to the remote server.

Full Architecture on Remote

Remote Server
├─ Docker networks: nginx-proxy, infra

├─ Server Infra Containers
│  ├─ nginx-proxy (ports 80, 443) → routes to app
│  ├─ letsencrypt → auto SSL on port 443
│  ├─ mysql-global (port 3306, infra only)
│  ├─ postgres-global (port 5432, infra only)
│  ├─ redis-global (port 6379, infra only)
│  ├─ phpmyadmin → mysql admin at http://ip:8082
│  └─ pgadmin → postgres admin at http://ip:8083

└─ App Container (infra + nginx-proxy networks)
   ├─ supervisord
   │  ├─ auto-migrate.sh (one-shot)
   │  ├─ maker serve --prod (port 3000)
   │  ├─ maker queue:work (if Redis)
   │  └─ maker schedule:work
   └─ nginx-proxy routes app.example.com → app:3000

Security Notes

  • Never commit deploy/.env or deploy/server/.env to version control (they contain secrets).
  • The server infra .env.local.example is for local Docker Desktop overrides — do not use it in production.
  • Change default MySQL/Postgres passwords in deploy/server/.env before production.
  • Generate strong secrets for JWT_ACCESS_SECRET, JWT_REFRESH_SECRET, and COOKIE_SECRET.
  • The app container does not expose ports directly — all traffic goes through nginx-proxy.
  • Set AUTO_MIGRATE=false in production if you prefer to run migrations manually.

Released under the MIT License.