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/nexwire"
  },
  "upload": {
    "source": ".",
    "targetSubPath": "."
  },
  "databaseImport": {
    "enabled": false,
    "file": "deploy/nexwire.sql",
    "database": "nexwire",
    "container": "mysql-global",
    "user": "root"
  },
  "preDeployCommands": ["docker rm -f old-app 2>/dev/null || true"],
  "rsyncPath": "rsync",
  "rsyncSshPath": "ssh",
  "rsyncSshOptions": ["-o", "StrictHostKeyChecking=no"]
}
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
rsyncPathPath to the rsync binary (e.g. "rsync"). When set, rsync is used instead of scp
rsyncSshPathPath to the SSH binary used by rsync (e.g. "ssh")
rsyncSshOptionsExtra SSH flags passed to rsync (e.g. ["-o", "StrictHostKeyChecking=no"])

rsync Version Requirement ​

rsync version compatibility matters — using a very different version (e.g. major version mismatch) between local and remote can cause protocol handshake failures. Check both versions before deploying:

Check both versions before deploying:

bash
rsync --version      # local
ssh user@host "rsync --version"   # remote

If versions differ, you have two options:

  1. Match the versions — install the same rsync build on both sides (e.g. choco install rsync on Windows, sudo apt install rsync on the remote)
  2. Fall back to scp — remove rsyncPath from workflow.remote.json (or set it to ""). The deploy system will automatically use scp instead of rsync

TIP

scp works as a reliable fallback when rsync versions can't be matched. It uploads files but doesn't support incremental sync or --delete. Remove rsyncPath to enable scp mode.

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/nexwire
  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/nexwire/
  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/nexwire.sql --database=nexwire
bash
pnpm maker deploy:workflow:remote --server-only
pnpm maker deploy:workflow:remote --app-only
pnpm maker deploy:db:import:remote --file=deploy/nexwire.sql --database=nexwire
bash
yarn maker deploy:workflow:remote --server-only
yarn maker deploy:workflow:remote --app-only
yarn maker deploy:db:import:remote --file=deploy/nexwire.sql --database=nexwire
bash
bun maker deploy:workflow:remote --server-only
bun maker deploy:workflow:remote --app-only
bun maker deploy:db:import:remote --file=deploy/nexwire.sql --database=nexwire

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 to disable automatic migrations and run them manually instead.

Released under the MIT License.