A complete guide to zero-downtime automated deployments.
Pushdeck deploys your application by cloning a fresh copy of your repository, running your custom scripts, then switching your web server to the new release in one instant step. Your visitors never see partially-deployed code.
# What your server looks like after a few deployments:
/var/www/myapp/
├── releases/
│ ├── 20260401120000/ # old release (kept for rollback)
│ ├── 20260402090000/ # older release
│ └── 20260402160000/ # ← newest release
├── .env # shared, never inside a release
└── current -> releases/20260402160000/ # symlink your web server points to
{deploy_path}/current once and forget it. Pushdeck updates current on every deployment. For a Laravel app, your document root would be {deploy_path}/current/public.
Every deployment runs the following steps in strict sequence. If any step fails, the deployment stops and your live release is left untouched.
Go to the Hooks tab and drag your hooks into the position you want. Hooks above Activate New Release run before the new release goes live; hooks below run after.
| Requirement | Details |
|---|---|
| A Linux server | With SSH access and git installed. You will use your existing SSH user (e.g. ubuntu), and that user needs write access to your deploy path. |
| SSH access | Pushdeck generates an SSH key for you. You'll add its public key to ~/.ssh/authorized_keys on your server. You never need to share your own private keys. |
| GitHub or GitLab | Your application code in a repository. |
| Personal Access Token | From GitHub or GitLab with read access to your repo. See section 3 for step-by-step instructions. |
Log in and click New Project from the dashboard.
| Field | What to enter | Example |
|---|---|---|
| Name | A label for this project, just for your reference. | My Laravel App |
| Deploy Path | The directory on your server where Pushdeck will store releases. Your SSH user must have write access to this path (see step 2). | /var/www/myapp |
| Retain Releases | How many old releases to keep for rollback. Recommended: 5. | 5 |
/var/www/myapp/current (or /var/www/myapp/current/public for Laravel). Set it once and leave it. Pushdeck updates what current points to on every deploy.
Pushdeck generates a unique SSH key pair for each server you add. You get a public key to place on your server. You never share any of your own private keys with Pushdeck. You stay in full control: remove the key from your server at any time and Pushdeck can no longer connect.
On your project overview, click Add Server and fill in the details:
| Field | What to enter |
|---|---|
| Name | Any label you like, e.g. production or web-01. |
| Host | Your server's IP address or domain, e.g. 203.0.113.10. |
| Port | SSH port, usually 22. |
| Username | The user Pushdeck should connect as. Use your existing SSH user, e.g. ubuntu on AWS or root on a fresh VPS. That user needs read/write access to your deploy path. |
Click Generate Key & Continue. Pushdeck creates the key pair and takes you to the server page where the public key is waiting for you.
Copy the public key shown on the server page. Then SSH into your server as the same user you entered in step 1 and run:
# Make sure the .ssh directory exists
mkdir -p ~/.ssh && chmod 700 ~/.ssh
# Open (or create) the authorized_keys file, paste the key on a new line, then save
nano ~/.ssh/authorized_keys
# Set the correct permissions
chmod 600 ~/.ssh/authorized_keys
# Make sure your user has write access to the deploy path
sudo chown -R ubuntu:ubuntu /var/www/your-app
Replace ubuntu and /var/www/your-app with your actual username and deploy path. If you are using a separate deployment user, SSH as that user instead and follow the same steps.
Back in Pushdeck, click Test Connection on the project overview. You need a green connected badge before you can deploy.
authorized_keys on your server and the connection is gone immediately. Nothing to change in Pushdeck.
13.205.191.222.
If your server restricts SSH access by IP, add this address to your allowlist so Pushdeck can connect.
Click the Repository tab on your project, then Connect Repository.
Pushdeck uses your token only to clone the repository during deployments. We recommend using the most scoped token your provider supports so only that one repository is accessible, nothing else on your account.
GitHub offers two types of tokens. We recommend the fine-grained token as it can be locked to a single repository with read-only access.
Go to GitHub → Settings → Developer settings → Personal access tokens → Fine-grained tokens. Or go directly to github.com/settings/personal-access-tokens/new.
Give it a name like Pushdeck - my-app and set an expiry.
Under Repository access, choose Only select repositories and pick the one repo you are deploying.
Under Permissions, expand Repository permissions, find Contents and set it to Read-only. That is the only permission needed.
Click Generate token, copy it immediately (GitHub only shows it once), and paste it in Pushdeck.
repo scope. Fine-grained is better because it restricts access to one repository instead of all of them.
For GitLab, use a project access token rather than a personal access token. It is scoped to a single project and does not touch anything else on your account.
Open your GitLab project, go to Settings → Access Tokens.
Give it a name, set an expiry, choose role Reporter, and tick the read_repository scope only.
Click Create project access token, copy it immediately, and paste it in Pushdeck.
Hooks are shell scripts that run on your servers during every deployment. Go to the Hooks tab and click Add Hook.
| Field | Description |
|---|---|
| Name | A label shown in the pipeline, e.g. Install Dependencies. |
| Script | Shell commands to run, one per line. They execute from the release directory. |
| On Servers | Checkboxes for which servers this hook runs on. Leave all unchecked to run on every server. |
| Run As (optional) | Only needed if you want a specific command to run as a different OS user than the one Pushdeck connects as, e.g. restarting PHP-FPM as www-data. If your SSH user already owns your project files, you can leave this blank and everything just works. |
After saving, drag the hook in the pipeline table to set when it runs. Drop it above Activate New Release to run before the new release goes live, or below to run after.
Place these hooks above Activate so they run before the new release goes live:
# Install Composer dependencies (no dev packages)
composer install --no-dev --no-interaction --prefer-dist --optimize-autoloader
# Run database migrations
php artisan migrate --force
# Cache config, routes, and views for production
php artisan config:cache
php artisan route:cache
php artisan view:cache
Place these hooks below Activate so they run after the new release is live:
# Restart the queue worker
php artisan queue:restart
# Reload PHP-FPM (use your actual service name)
sudo systemctl reload php8.3-fpm
{deploy_path}/.env on your server automatically. Every deployment then links it into the new release. You never have to manage it manually.
# Install production dependencies
npm ci --omit=dev
# Build assets (if applicable)
npm run build
# After activate: reload with PM2
pm2 reload ecosystem.config.js --env production
Place these hooks above Activate so the release is ready before it goes live:
# Install dependencies
pip install -r requirements.txt
# Run database migrations (Django)
python manage.py migrate --noinput
# Collect static files (Django)
python manage.py collectstatic --noinput
# After activate: restart the app service (gunicorn, uvicorn, etc.)
sudo systemctl restart gunicorn
# Any shell commands work; they run from the release directory
bundle install --without development test
bundle exec rake assets:precompile
Every deployment clones a fresh copy of your repository into a new release folder. That means anything generated at runtime (user uploads, logs, cache files) would be lost on the next deploy unless you tell Pushdeck to preserve it.
Linked Folders solve this. You define folder paths relative to your project root, and on every deploy Pushdeck:
{deploy_path}/shared/ if it does not already exist.The result is that all releases point at the same physical folder on disk, so data accumulates across deployments instead of disappearing.
Go to the Hooks tab and click the Linked Folders button in the top right. Add one path per input field, then click Save.
Given a deploy path of /var/www/myapp and a linked folder of storage, your server looks like this:
/var/www/myapp/
shared/
storage/ # real folder, never deleted
releases/
20260404120000/
storage # symlink to ../../shared/storage
20260403110000/
storage # symlink to ../../shared/storage
current # symlink to latest release
shared/ directory is created automatically on your first deploy. You do not need to create it manually. On rollback, the previous release still points at the same shared folder, so no data is lost there either.
Laravel generates files at runtime that must survive deployments. Add these two paths:
| Path | Why |
|---|---|
storage | Laravel writes logs, compiled views, sessions, queue jobs, and uploaded files here. Without it, logs disappear and session-based users get logged out on every deploy. |
public/uploads | If you store user-uploaded files under public/ instead of storage/, preserve this path too. |
Only needed if your app stores uploaded files on the local disk (not S3 or cloud storage). If so, add this as a hook below Activate:
php artisan storage:link
This creates a public/storage link so files saved to storage/app/public are accessible in the browser. It is safe to run on every deploy as it does nothing if the link already exists.
| Stack | Common paths to link |
|---|---|
| Node.js | public/uploads, data, logs |
| Django / Python | media (MEDIA_ROOT), staticfiles if collected at runtime |
| Ruby on Rails | public/uploads, log, tmp |
| WordPress / PHP | wp-content/uploads |
| Any stack | Any folder your app writes to at runtime that should not be wiped on deploy |
storage, not /var/www/myapp/storage. Paths starting with / or ../ are rejected.
Click the Environment tab to store your .env file contents securely. On each deployment, Pushdeck automatically links this file into the release as .env so you never have to copy it manually. For a Laravel app, paste the full contents of your production .env here, including APP_KEY, database credentials, and any third-party API keys.
.env file to your repository.
Before deploying, confirm:
APP_KEY is set before your first deploy.Click Deploy Now from the project overview, the Deployments tab, or the dashboard. The deployment runs in the background and you will be taken to the deployment detail page where each pipeline step updates in real time.
The deployment detail page shows the pipeline as a table of steps. Each row is one step on one server.
| Column | Meaning |
|---|---|
| Server | Which server this job ran on. |
| Status | Finished success Failed error Running in progress Pending not started yet |
| Started / Finished | Local timestamps (converted to your browser timezone). |
| Duration | How long that step took on that server. |
| Click to view the raw SSH output for that step in a modal. |
When a step fails, all remaining steps are immediately marked as failed so you can see the full picture right away. Steps that ran successfully before the failure keep their output and timestamps. Click the eye icon to inspect them.
Go to the Deployments tab. Any deployment with a success status that is not the current active release will show a Rollback button.
Clicking Rollback re-runs only the Activate step (and any hooks positioned below it) against the selected release without re-cloning. The old release folder is already on disk, so the switch is nearly instant.
Trigger a deployment automatically from GitHub Actions, GitLab CI, or any HTTP client by hitting your project's Deploy Hook URL. Find it on the project overview under Deploy Hook URL.
# Trigger a deploy with curl (POST)
curl -X POST https://yourpushdeck.com/deploy/YOUR_TOKEN
- name: Deploy
run: curl -X POST ${{ secrets.PUSHDECK_DEPLOY_URL }}
Open the Notifications tab on your project to be told when a deployment finishes. Two channels are available and you can use either or both.
Connect a Slack Incoming Webhook and Pushdeck posts a message to your chosen channel on every success or failure, with the commit, who triggered it, and the duration.
Go to api.slack.com/apps → Create New App → From scratch.
Name it (e.g. "Pushdeck") and pick your workspace.
Open Incoming Webhooks and turn it on.
Click Add New Webhook to Workspace and choose the channel for deploy updates.
Copy the URL (it starts with https://hooks.slack.com/) and paste it into the Notifications tab. Use Send a test notification to confirm.
Point the Notification URL at any endpoint and Pushdeck sends an HTTP POST with a JSON body each time a deployment reaches a final state. The request carries the header User-Agent: Pushdeck-Webhook/1.0.
Events (the event field):
deployment.completed — a deployment finished. Read status for success or failed.test — sent only when you click "Send a test notification".Example payload:
{
"event": "deployment.completed",
"status": "success", // or "failed"
"deployment": {
"id": 123,
"release": "20260402070456",
"status": "success",
"is_rollback": false,
"started_at": "2026-04-02T07:04:38+00:00",
"finished_at": "2026-04-02T07:04:56+00:00",
"duration_seconds": 18, // null if unknown
"failure_reason": null, // set when status is "failed"
"failure_category": null, // repository | server | hook | general
"url": "https://app.pushdeck.dev/projects/1/deployments/123"
},
"commit": {
"hash": "abc1234fde...", // null if not captured
"short_hash": "abc1234f",
"message": "Fix login",
"author": "Jane",
"url": "https://github.com/owner/repo/commit/abc1234f"
},
"project": {
"id": 1,
"name": "My App",
"repository": "owner/repo",
"branch": "main"
},
"triggered_by": { "name": "Jane" }, // null for webhook/CI deploys
"timestamp": "2026-04-02T07:04:56+00:00"
}
Invite teammates to collaborate on a project from the Team tab (visible to the project owner). Access is scoped per project: someone can be an admin on one project and view-only on another.
| Role | What they can do |
|---|---|
| Owner | The creator. Full control, including team management and deleting the project. Cannot be removed. |
| Admin | Manage servers, repository, hooks, environment, settings, and notifications, and deploy or roll back. Cannot manage the team or delete the project. |
| Deployer | Deploy and roll back, and manage notifications. No access to settings, servers, repository, or secrets. |
| Viewer | Read-only: view the project and its deployment history. Can also manage notifications. |
Beyond the role, you can fine-tune individual permissions per person (for example, grant a viewer the ability to manage hooks, or revoke notification access from someone who otherwise has it by default). Permissions can be set at invite time and changed later.
On the Team tab, enter the person's email, pick a role, optionally customize permissions, and send the invitation.
They receive an email. If they do not have a Pushdeck account yet, they can create one straight from the link.
The project appears on their dashboard only after they explicitly accept. Until then it shows as a pending invitation on both sides.
www-data) via passwordless sudo.nc -zv HOST PORT).~/.ssh/authorized_keys on the server for the correct user.ls -la /var/www/myapp. If not, run sudo chown -R your-user:your-user /var/www/myapp.owner/repo spelling and that the token has read access.sudo apt install git.Pushdeck automates what you would do manually on your server. If a step fails, a good first check is to try the same thing yourself: SSH into the server, navigate to the same directory, and run the same command with the same user. If it fails there too, the issue is with the server configuration, not Pushdeck.
Common things to verify:
If the same command works fine when you run it manually on the server but keeps failing through Pushdeck, open a Help request and share the step output. We are happy to help.