Table of Contents
This document covers the full path from local Astro development to automated deployment. The pipeline is: build locally or in CI → produce dist/ → sync to a server directory over SSH with rsync → serve static files with Nginx or similar.
1. Requirements
| Item | Version or note |
|---|---|
| Node.js | 22 (match CI) |
| Package manager | pnpm (packageManager in package.json can pin the version) |
| Astro | 5.x |
| Server | SSH (port 22) open; Nginx or another static file server installed |
Create a new project:
pnpm create astro@latest
cd <project-name>
pnpm install
2. Astro commands
2.1 Project scripts (package.json)
Typical definitions:
{
"scripts": {
"dev": "astro dev",
"build": "astro build",
"preview": "astro preview",
"astro": "astro"
}
}
Run them with:
pnpm dev # dev server, default http://localhost:4321
pnpm build # production build → dist/
pnpm preview # serve dist/ locally
Some projects chain extra steps in build, e.g. pnpm assets:sync && astro build && pagefind --site dist. In CI, run the same pnpm run build as production—not bare astro build—unless you have verified they are equivalent.
2.2 Astro CLI
Invoke via pnpm astro:
pnpm astro dev [--host 0.0.0.0] [--port 4321]
pnpm astro build
pnpm astro preview [--host 0.0.0.0] [--port 4321]
pnpm astro check # types and templates with @astrojs/check
pnpm astro sync # generate .astro/types.d.ts
pnpm astro add <integration>
Host and port can be set in astro.config.mjs under server, or overridden with HOST / PORT (or ASTRO_HOST / ASTRO_PORT).
2.3 Build output
With output: 'static', astro build writes all static assets to dist/ at the project root. Deployment uploads the contents of dist/, not the source tree.
Verify locally:
pnpm build
pnpm preview
3. Key configuration
Deployment-related fields in astro.config.mjs:
import { defineConfig } from 'astro/config';
export default defineConfig({
output: 'static',
site: 'https://example.com',
});
| Field | Purpose |
|---|---|
output: 'static' | Pure static HTML/CSS/JS; suitable for rsync deploy |
site | Canonical base URL for sitemap, RSS, etc.; set to your domain |
4. Server: SSH key
On the server, generate a key pair for GitHub Actions:
ssh-keygen -m PEM -t rsa -b 4096 -C "github-actions-deploy" -f ~/.ssh/github_actions
Leave the passphrase empty (Actions cannot prompt for it).
Add the public key and fix permissions:
cat ~/.ssh/github_actions.pub >> ~/.ssh/authorized_keys
chmod 700 ~/.ssh
chmod 600 ~/.ssh/authorized_keys
Incorrect permissions cause sshd to reject key authentication.
Export the private key for GitHub Secrets:
cat ~/.ssh/github_actions
The output must include the -----BEGIN ... PRIVATE KEY----- and -----END ... PRIVATE KEY----- lines.
5. GitHub Secrets
Under Settings → Secrets and variables → Actions, create:
| Name | Value |
|---|---|
SERVER_HOST | Server IP or hostname |
SERVER_USER | SSH username |
SERVER_SSH_KEY | Full private key from section 4 |
Do not commit keys or passwords to the repo; reference them only as ${{ secrets.* }} in the workflow.
6. GitHub Actions workflow
Create .github/workflows/deploy.yml:
name: Build and Deploy Astro
on:
push:
branches:
- main
jobs:
build-and-deploy:
runs-on: ubuntu-latest
concurrency:
group: deploy-${{ github.ref }}
cancel-in-progress: true
steps:
- name: Checkout Code
uses: actions/checkout@v5
- name: Setup pnpm
uses: pnpm/action-setup@v6
with:
version: 10.28.0
- name: Setup Node.js
uses: actions/setup-node@v5
with:
node-version: '22'
cache: 'pnpm'
- name: Install Dependencies
run: pnpm install --frozen-lockfile
- name: Restore Image Caches
uses: actions/cache@v5
with:
path: |
node_modules/.astro/assets
public/generated-previews
key: ${{ runner.os }}-image-cache-${{ hashFiles('src/assets/images/**/*.avif', 'src/assets/images/config.json', 'src/assets/photos/**/*.avif', 'src/assets/photos/**/*.json', 'src/utils/asset-image.ts', 'src/utils/public-image.ts', 'src/utils/photography.ts', 'src/utils/hero-image.ts', 'astro.config.mjs', 'package.json', 'pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-image-cache-
- name: Build Astro Site
run: pnpm run build
- name: Check Deploy Host Connectivity
env:
REMOTE_HOST: ${{ secrets.SERVER_HOST }}
run: |
set -eu
getent ahosts "$REMOTE_HOST"
timeout 10 bash -c 'cat < /dev/null > /dev/tcp/'"$REMOTE_HOST"'/22'
- name: Deploy to Server
uses: easingthemes/ssh-deploy@main
env:
SSH_PRIVATE_KEY: ${{ secrets.SERVER_SSH_KEY }}
ARGS: "-rl --delete --info=progress2,stats2 --human-readable -i --exclude=files/images-originals/*** --exclude=files/photos-originals/***"
SOURCE: "dist/"
REMOTE_HOST: ${{ secrets.SERVER_HOST }}
REMOTE_USER: ${{ secrets.SERVER_USER }}
TARGET: "/var/www/your-site"
6.1 Step reference
| Step | Purpose |
|---|---|
concurrency | Cancels an in-progress deploy when a new push arrives on the same ref |
pnpm install --frozen-lockfile | Install exactly what the lockfile specifies |
Restore Image Caches | Cache Astro image pipeline output; adjust hashFiles paths for your project, or remove if unused |
pnpm run build | Run the full package.json build script |
Check Deploy Host Connectivity | DNS + TCP port 22 check before rsync; optional |
easingthemes/ssh-deploy | rsync from SOURCE to TARGET over SSH |
6.2 rsync arguments
| Flag | Meaning |
|---|---|
-r | Recursive |
-l | Preserve symlinks |
--delete | Remove files on the remote that are not in the source |
-i | Transport over SSH |
--exclude=... | Skip matching paths |
SOURCE: "dist/" — the trailing slash syncs the inside of dist/ onto TARGET, without an extra dist/ directory on the remote.
Set TARGET to the actual web root on the server.
7. Web server configuration
Example Nginx block pointing at the deploy directory:
server {
listen 80;
server_name example.com;
root /var/www/your-site;
index index.html;
location / {
try_files $uri $uri/ $uri.html =404;
}
}
Configure HTTPS separately (e.g. Let’s Encrypt). root must match workflow TARGET.
Paths maintained only on the server (e.g. uploaded originals) should be listed in rsync --exclude so --delete does not remove them.
8. Deployment procedure
- Change code locally; run
pnpm buildto confirm the build succeeds. - Commit and push to
main. - GitHub Actions runs the workflow automatically.
- Open the repo Actions tab; expand failed steps to read logs.
For a one-off manual deploy:
pnpm build
rsync -rl --delete -e ssh dist/ user@host:/var/www/your-site/
9. Troubleshooting
Key authentication fails
~/.sshshould be700,authorized_keysshould be600.- Private key in the Secret must be complete, with correct line breaks.
SERVER_USERmust match the account on the server.
Build succeeds but the site is unchanged
TARGETand Nginxrootmust be the same directory.- Check Actions logs for rsync permission errors.
--delete removed extra files on the server
- The remote is forced to match
dist/. Paths that must survive on the server but are not in the build need--exclude.
Dev port already in use
PORT=4322 pnpm dev
Or change server.port in astro.config.mjs.