Guide

Magic Deploy Script Cookbook

Copy-paste starters for the Before launch and After deploy steps, organized by stack. Lift what fits; ignore the rest.

What these two fields do

Magic Deploy's generic (non–App Store) flow has two script hooks. You'll see them when you're deploying a web or server app — not the App Store / Play Store panes.

Exit codes matter. LingCode watches for non-zero exits in both phases. A failing test, grep -q, or explicit || exit 1 will halt the deploy and surface the error in the progress log — which is exactly what you want. Don't write scripts that silently swallow failures.

Why the two-phase split? These two phases run in fundamentally different execution contexts. Before launch runs on your Mac — it has access to your source tree, your local Node / Python / Go / Ruby toolchain, and your dev-only credentials. After deploy runs over SSH on the server — it has access to system services (nginx, systemd, pm2), the real database, and production secrets. Mixing them up — e.g. trying to run your build toolchain on a minimal production box, or running systemctl reload nginx locally — is a category error that kills most first-time deploy attempts.

Before launch (local) — by stack

Static HTML / hand-written site

Nothing to build. Either leave the field empty, or add a sanity check so a typo doesn't ship a broken site:

test -f index.html || { echo >&2 "Missing index.html"; exit 1; }

Next.js (static export)

npm ci
npm run build
npm run export
test -f out/index.html || { echo >&2 "Export failed — no out/index.html"; exit 1; }

If you're using the newer App Router with output: 'export' in next.config.js, skip the export step — build writes to out/ directly.

Next.js (SSR / Node runtime)

npm ci
npm run build
test -d .next || { echo >&2 "No .next dir"; exit 1; }

Vite / React / Vue / Svelte

npm ci
npm run build
test -f dist/index.html || { echo >&2 "Build missing dist/index.html"; exit 1; }

Replace dist/ with build/ for create-react-app projects.

Astro

npm ci
npm run build
test -d dist || exit 1

Hugo

hugo --minify --gc
test -f public/index.html || exit 1

Jekyll

bundle install --path vendor/bundle
bundle exec jekyll build
test -f _site/index.html || exit 1

Node server (Express / Fastify / Hono)

npm ci
npm run build            # if you have a TypeScript step
npm test                 # fail the deploy on red tests
test -f dist/server.js || exit 1

For Bun projects, swap npm ci for bun install --frozen-lockfile, and npm run build for bun run build.

Python (FastAPI / Flask / Django)

Python apps usually don't "build" — but you want frozen deps and collected static files:

python -m venv .venv
. .venv/bin/activate
pip install --upgrade pip
pip install -r requirements.txt
python -m pytest -x                    # fail fast on red tests
python manage.py collectstatic --noinput  # Django only
deactivate

Go

Build a static Linux binary on your Mac, ship it:

GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -ldflags="-s -w" -o dist/app ./cmd/app
test -x dist/app || exit 1

Rust

cargo test
cargo build --release
test -x target/release/myapp || exit 1

For cross-compilation to Linux x86_64 from a Mac, set up a cross-toolchain and use --target x86_64-unknown-linux-musl.

PHP / Laravel

composer install --no-dev --optimize-autoloader --no-interaction
npm ci && npm run build
php artisan config:cache
php artisan route:cache
php artisan view:cache

Ruby on Rails

bundle install --deployment --without development test
bundle exec rails assets:precompile
test -d public/assets || exit 1

Package everything as a tarball

Some server setups want a single archive rather than individual files rsync'd. Put this as the last line of Before launch:

tar -czf deploy.tar.gz -C dist .
# or, including everything except .git and OS junk:
zip -q -r deploy.zip . -x '*.git/*' -x '*/.DS_Store' -x 'node_modules/*'

After deploy (remote) — by need

Each line below runs in its own SSH session. {DateTime} expands to a timestamp LingCode generates at run time (e.g. 20260419-133742).

Fix file permissions

chmod -R a+rX .
# If the web server runs as www-data:
chown -R www-data:www-data /var/www/myapp

Reload nginx (static or reverse-proxied sites)

sudo nginx -t && sudo systemctl reload nginx

The -t flag validates config before reloading — safer than a blind reload that can kill a running server with a broken config.

Restart a Node app under pm2

pm2 reload all --update-env
pm2 save

reload is zero-downtime; restart isn't. Use reload in prod unless your app can't hot-swap.

Restart a Node app via systemd

sudo systemctl restart myapp.service
sudo systemctl is-active --quiet myapp.service || exit 1

Restart a Python app (Gunicorn / Uvicorn under systemd)

sudo systemctl restart gunicorn
sudo systemctl is-active --quiet gunicorn || exit 1

Docker Compose redeploy

cd /srv/myapp && docker compose pull
cd /srv/myapp && docker compose up -d --remove-orphans
docker image prune -f

Run database migrations

cd /var/www/myapp && php artisan migrate --force       # Laravel
cd /var/www/myapp && bundle exec rails db:migrate      # Rails
cd /var/www/myapp && alembic upgrade head              # SQLAlchemy/FastAPI
cd /var/www/myapp && npx prisma migrate deploy         # Prisma

Timestamped release directories with atomic swap

Zero-downtime deploys usually look like: upload to a new versioned dir, flip a symlink. LingCode expands {DateTime} to a timestamp you can use as the release dir name:

cp -r /var/www/uploads/. /var/www/releases/{DateTime}
ln -sfn /var/www/releases/{DateTime} /var/www/current
sudo systemctl reload nginx

Combine with a Before launch step that prepares a clean /var/www/uploads-equivalent on the local side.

Health check

Run this as the last remote line. If it fails, the deploy is marked failed and you know before users do:

curl -sfSL http://localhost:3000/healthz > /dev/null || { echo "Health check failed"; exit 1; }

Cache warm

curl -sfL https://yoursite.com/ > /dev/null
curl -sfL https://yoursite.com/sitemap.xml > /dev/null

Log a deploy marker

echo "{DateTime}  deploy from LingCode" | sudo tee -a /var/log/deploys.log

Combined patterns

Static site on nginx (simplest useful flow)

Before launch (local):

npm ci
npm run build
test -f dist/index.html || exit 1

After deploy (remote):

chmod -R a+rX .
sudo nginx -t && sudo systemctl reload nginx
curl -sfSL http://localhost/ > /dev/null

Node app under pm2 with zero-downtime releases

Before launch (local):

npm ci
npm run build
npm test
tar -czf deploy.tar.gz dist/ package.json package-lock.json

After deploy (remote):

cd /var/www/myapp && tar -xzf deploy.tar.gz
cd /var/www/myapp && npm ci --omit=dev
pm2 reload ecosystem.config.js --update-env
sleep 2 && curl -sfSL http://localhost:3000/healthz > /dev/null || exit 1

Django on Gunicorn + nginx

Before launch:

python -m venv .venv
. .venv/bin/activate
pip install -r requirements.txt
python manage.py collectstatic --noinput
deactivate

After deploy:

cd /var/www/myapp && . .venv/bin/activate && python manage.py migrate --noinput
sudo systemctl restart gunicorn
sudo nginx -t && sudo systemctl reload nginx
curl -sfSL http://localhost/ > /dev/null

Dockerized app with image pinning

Before launch:

docker build -t myapp:{DateTime} .
docker save myapp:{DateTime} | gzip > image.tar.gz

After deploy:

cd /srv/myapp && gunzip < image.tar.gz | docker load
cd /srv/myapp && sed -i "s|IMAGE_TAG=.*|IMAGE_TAG={DateTime}|" .env
cd /srv/myapp && docker compose up -d
curl -sfSL http://localhost/healthz > /dev/null

Debugging failed scripts

Both phases stream their output into the Magic Deploy progress log, prefixed by which command just ran. When something fails:

Tip: test scripts locally first. Before you run a deploy that ends in sudo systemctl restart gunicorn, SSH into the box yourself and run each line manually. If one doesn't work standalone, it won't work inside Magic Deploy either, and you'll have spent a 2-minute rsync for nothing.

Further reading

Classic documentation for the tools these scripts invoke:

Shipping to a store instead?

See the App Store Connect setup guide (iOS + macOS) or the Google Play service account guide.

Download LingCode for Mac