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.
- Before launch (local) runs one command per line in your project folder before rsync. Use it to build assets, run tests, verify outputs, and prepare the exact directory tree you want uploaded. Any non-zero exit code here aborts the deploy.
- After deploy (remote) runs one SSH session per line on the server after upload finishes. Use it to fix permissions, restart services, run migrations, warm caches, or perform a health check.
{DateTime}inside the string is expanded to the current timestamp when the script runs — handy for release directories.
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:
- Before launch — scroll to the end of the log and find the failing line. Everything after
npm ERR!,error:, orcannot statis usually the cause. Fix, retry. - After deploy — remember this ran over SSH on the server. The error is whatever the remote command printed. Common culprits: missing
sudo, missing environment variable (SSH sessions don't inherit your interactive shell'sPATH), or the command not being onPATHat all (use absolute paths like/usr/bin/systemctlif in doubt). - Silent failures — if a deploy "succeeded" but the site is broken, you probably skipped a health check. Add one as the last After-deploy line and you'll catch these immediately.
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:
- LingCode deploy glossary — terms like JWT, keystore, and more.
- rsync manual — what LingCode uses under the hood to sync your files.
- systemctl reference — for restarting services on Linux boxes.
- pm2 quick start — Node process manager used widely in the After-deploy scripts.
- nginx web server admin guide — nginx config, reload semantics, and virtual hosts.
- Docker Compose — for containerized deploys.
- OpenSSH manual — how the SSH connections LingCode opens actually work.
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