How Deployments Work
Understanding the deployment process helps you debug issues and optimize your workflow.
The Deployment Pipeline
Every deployment—CLI, Git, dashboard, API, or Content—enters the same coordinated pipeline:
Source readiness → Queue → Build image → Start candidate
→ Health check → Transactional traffic cutover → FinalizeSlipway keeps the current release serving traffic until the candidate is healthy and the proxy update has been verified.
Step-by-Step
1. Verify source readiness
Before creating active work, Slipway confirms that the target app has deployable source:
- a connected Git repository and branch;
- a source archive previously pushed by the Slipway CLI; or
- an exact commit created by the Content Manager.
Dashboard redeploys work for both Git-connected and CLI-pushed applications. If no source is available, Slipway stops during preflight and displays the actionable reason instead of creating an unexplained failed deployment.
2. Queue and coordinate
Only one deployment can build or change traffic for an app at a time. Additional deployments remain Queued and start in order after the active deployment reaches a terminal state.
This prevents two builds, rollbacks, or proxy updates for the same app from racing each other. Different apps can still deploy independently.
Slipway records the queue and lease in its database. After a Slipway or host restart, reconciliation:
- resumes eligible queued work;
- marks interrupted work with an accurate terminal state;
- removes abandoned candidate containers and proxy transactions; and
- restores the app's status from the running Docker container.
3. Package or fetch source code
$ slipway slide
▶ Packaging source...
→ Creating archive from git (respects .gitignore)
→ Archive size: 2.3 MBSlipway packages your code using git archive:
- Only tracked files are included
.gitignoreis respectednode_modules/is excluded (installed during build)- Untracked files are excluded
4. Upload to Server
▶ Uploading to server...
→ Uploading slipway-myapp-abc123.tar.gz
→ Upload complete (2.3 MB in 1.2s)The archive is uploaded to your Slipway server via HTTPS.
5. Build Docker Image
▶ Building image...
→ docker build -t slipway/myapp:abc123 .
→ Step 1/8: FROM node:22-alpine
→ Step 2/8: WORKDIR /app
→ Step 3/8: COPY package*.json ./
→ Step 4/8: RUN npm ci
→ Step 5/8: COPY . .
→ Step 6/8: RUN npm run build
→ Step 7/8: EXPOSE 1337
→ Step 8/8: CMD ["node", "app.js"]
→ Image built: slipway/myapp:abc123 (245 MB)Slipway builds a Docker image using your Dockerfile:
- Dependencies installed with
npm ci - Assets compiled (if using Shipwright/Vite)
- Image tagged with deployment ID
6. Start a candidate release
▶ Starting zero-downtime deployment...
→ Starting new container alongside old...For zero-downtime deploys:
- New container starts alongside old
- Health checks run on new container
- Once healthy, traffic switches
- Old container stops
7. Verify candidate health
▶ Starting new container...
→ docker run -d --name myapp slipway/myapp:abc123
→ Container started: myapp-abc123
→ Waiting for health check...
→ Health check passed ✓The new container:
- Connects to the Slipway network
- Receives environment variables
- Links to database services
- Starts the Sails application
8. Cut over traffic transactionally
▶ Updating proxy routes...
→ Configuring Caddy for myapp.example.com
→ Route updated ✓Caddy is updated to route traffic to the new container:
- Domain → New container port
- WebSocket support enabled
- SSL termination active
The cutover is a transaction:
- Slipway snapshots the current route.
- It writes the candidate route.
- Caddy validates and reloads the configuration.
- Slipway verifies that the expected route is active.
- Only then does it finalize the new release and stop the previous container.
If writing, reloading, or verifying the route fails, Slipway restores the previous route and keeps the previous release serving traffic.
9. Cleanup
▶ Cleaning up...
→ Stopping old container
→ Removing old container
→ Keeping last 10 images for rollback
✓ Deployed myapp (abc123) in 42s
https://myapp.example.comCancelling a deployment
Cancelling is cooperative process termination, not only a status change. Slipway stops an active Docker build, health check, source operation, or rollback; removes candidate artifacts; releases the app's deployment lease; and starts the next queued deployment when appropriate.
If cancellation happens before traffic cutover, the current release remains untouched. If a cutover transaction has begun, Slipway restores the previous verified route before cleanup.
The deployment page records who cancelled the work and whether any candidate release was removed.
Deployment history
Deployment history is ordered from the app's actual current and active state:
- executing work;
- current releases;
- queued deployments in queue order; and
- completed history from newest to oldest.
Each row identifies the app, status, source, actor, branch, commit, and creation time. The history updates active statuses without requiring a full page refresh.
The Dockerfile
Slipway requires a Dockerfile in your project root. Here's a recommended structure for Sails apps:
Basic Dockerfile
FROM node:22-alpine
WORKDIR /app
# Install dependencies
COPY package*.json ./
RUN npm ci --production
# Copy application
COPY . .
# Expose Sails port
EXPOSE 1337
# Start the application
CMD ["node", "app.js"]Production Dockerfile (with build step)
# Build stage
FROM node:22-alpine AS builder
WORKDIR /app
# Install all dependencies (including devDependencies)
COPY package*.json ./
RUN npm ci
# Copy source
COPY . .
# Build assets (Shipwright, Vite, etc.)
RUN npm run build
# Production stage
FROM node:22-alpine
WORKDIR /app
# Install tini for proper signal handling
RUN apk add --no-cache tini
# Install production dependencies only
COPY package*.json ./
RUN npm ci --production
# Copy built assets and application
COPY --from=builder /app/.tmp/public ./.tmp/public
COPY --from=builder /app/api ./api
COPY --from=builder /app/config ./config
COPY --from=builder /app/views ./views
COPY --from=builder /app/app.js ./
# Create non-root user
RUN addgroup -g 1001 -S sails && \
adduser -S sails -u 1001 -G sails
USER sails
EXPOSE 1337
# Use tini as init
ENTRYPOINT ["/sbin/tini", "--"]
CMD ["node", "app.js"]Key Dockerfile Best Practices
Use Alpine for smaller images
dockerfileFROM node:22-alpine # ~180MB vs ~1GB for full nodeLeverage layer caching
dockerfileCOPY package*.json ./ RUN npm ci COPY . . # Source changes don't invalidate npm cacheUse tini for signal handling
dockerfileRUN apk add --no-cache tini ENTRYPOINT ["/sbin/tini", "--"]Run as non-root
dockerfileUSER sailsSet NODE_ENV
dockerfileENV NODE_ENV=production
Environment Variables
During deployment, Slipway injects environment variables using a three-tier cascade:
Variable Cascade
Variables are merged in order, with later levels overriding earlier ones:
- Global variables — shared across all apps (set in Settings)
- Environment variables — per environment (set via
slipway env:set) - App variables — per app (multi-app environments only)
Built-in Variables
| Variable | Description |
|---|---|
PORT | Port to listen on (1337) |
NODE_ENV | Environment (production) |
SLIPWAY_APP | App name |
SLIPWAY_ENV | Environment name |
SLIPWAY_DEPLOYMENT_ID | Current deployment ID |
Your Variables
Variables set via slipway env:set are also injected:
DATABASE_URL=postgres://...
SESSION_SECRET=...
STRIPE_KEY=...Linked Services
When you link a database, Slipway sets:
DATABASE_URL=postgres://user:pass@host:5432/db
REDIS_URL=redis://host:6379Health Checks
Slipway checks your app is healthy before routing traffic:
Default Health Check
Slipway checks the root URL returns 200:
curl http://container:1337/Custom Health Check
Configure a dedicated health endpoint:
// api/controllers/health/check.js
module.exports = {
fn: async function () {
// Check database
await User.count()
// Check Redis (if used)
// await sails.helpers.cache.get('health');
return { status: 'healthy', timestamp: new Date() }
}
}Then configure in Slipway:
slipway project:update myapp --health-check-path=/health/checkHealth Check Settings
| Setting | Default | Description |
|---|---|---|
| Path | / | URL to check |
| Timeout | 30s | Max wait for response |
| Interval | 5s | Time between checks |
| Retries | 3 | Attempts before failing |
Zero-Downtime Deployment
Slipway achieves zero-downtime through:
1. Rolling Deployment
Time 0: [Old Container] ← Traffic
Time 1: [Old Container] ← Traffic
[New Container] starting...
Time 2: [Old Container]
[New Container] health check...
Time 3: → [New Container] ← Traffic
[Old Container] stopping...
Time 4: → [New Container] ← Traffic2. Connection Draining
Before stopping the old container:
- Existing connections are allowed to complete
- New connections go to new container
- Grace period: 30 seconds
3. Graceful Shutdown
Sails apps should handle SIGTERM:
// app.js or config/bootstrap.js
process.on('SIGTERM', async () => {
sails.log.info('Received SIGTERM, shutting down gracefully...')
// Close database connections
await sails.lower()
process.exit(0)
})Build Caching
Slipway caches Docker layers for faster builds:
Cached Layers
- Base image (
FROM node:22-alpine) - Dependencies (
npm cilayer)
Cache Invalidation
Cache is invalidated when:
package.jsonorpackage-lock.jsonchanges- Dockerfile changes
- Base image updates
Forcing Fresh Build
slipway slide --no-cacheBuild Logs
View build logs for debugging:
# During deployment
slipway slide # Logs stream automatically
# After deployment
slipway logs myapp --deployment=abc123 --buildCommon Build Errors
npm install fails:
npm ERR! code ERESOLVEFix: Check package-lock.json is committed
Build step fails:
Error: Cannot find module 'vite'Fix: Ensure build tools are in dependencies, not devDependencies
Out of memory:
FATAL ERROR: Heap out of memoryFix: Add --max-old-space-size or use multi-stage build
Deployment History
View past deployments:
slipway deployments myappOutput:
ID STATUS COMMIT MESSAGE DEPLOYED
abc123 running a1b2c3d Fix payment bug 2 minutes ago
def456 stopped e4f5g6h Add feature 2 hours ago
ghi789 stopped i7j8k9l Update deps 1 day agoEach deployment keeps:
- Docker image (for rollback)
- Build logs
- Deploy logs
- Git commit info
Troubleshooting
Deployment Stuck
# Check deployment status
slipway deployment:status myapp
# View logs
slipway logs myapp --deployment=currentContainer Won't Start
# Check container logs
slipway logs myapp
# Common issues:
# - Missing environment variables
# - Database not connected
# - Port already in useHealth Check Failing
# Test health endpoint manually
curl https://myapp.example.com/health/check
# Check app logs
slipway logs myapp -tMulti-App Deployments
Slipway supports running multiple apps in a single environment — for example, a web server and a background worker, or a frontend and API.
Each app in an environment:
- Has its own
Dockerfile(configurable per app, default:Dockerfile) - Runs in its own Docker container with a unique name
- Gets its own deployment record and build/deploy logs
- Receives the merged environment variables plus any app-specific overrides
When you deploy with slipway slide, the default app is deployed. To target a specific app:
slipway slide --app=workerWhen a webhook triggers an auto-deploy, all apps in the environment are deployed — each gets its own container rebuild and deployment record.
See Multi-App Environments for the full guide.
What's Next?
- Learn the Deploy Command options
- View Deployment Logs for debugging
- Set up Auto-Deploy for CI/CD