---
title: "Shell Deploy Script Patterns for Static Frontends"
aliases: [deploy-script, cp-recursive, frontend-deploy, apache-reload]
tags: [shell, bash, deploy, apache, nginx, static-files]
sources:
  - "daily/2026-04-15.md"
created: 2026-04-15
updated: 2026-04-15
---

# Shell Deploy Script Patterns for Static Frontends

Bash deploy scripts for static frontend projects have two common failure modes: incomplete file copying and serving stale files after deploy. Both are silent — the script exits 0 but the deployment is wrong.

## Key Points

- `cp frontend/*` glob does **not** recurse into subdirectories — use `cp -r frontend/. "$DEST/"` instead
- `cp -r frontend/.` (with dot) copies all contents including hidden files and nested dirs; `cp -r frontend/` copies the directory itself into the dest
- Always add `mkdir -p "$DEST"` before the copy as a safety net
- After copying static files, reload the web server: `systemctl reload apache2` (or `nginx -s reload`) — otherwise the old cached version is served
- Distinguish `setup.sh` (first-time init) from `deploy.sh` (all subsequent updates) — only `deploy.sh` runs on updates

## Details

### The cp Glob Pitfall

```bash
# WRONG — skips subdirectories silently
cp frontend/* /var/www/html/

# CORRECT — copies all contents recursively, including hidden files
cp -r frontend/. /var/www/html/
```

The shell glob `frontend/*` expands to all non-hidden entries in `frontend/` but `cp` without `-r` will skip any entries that are directories (it prints a warning but exits 0). Using `frontend/.` instead of `frontend/` as the source avoids creating a nested directory inside the destination.

### Apache / Nginx Reload Requirement

Web servers cache configuration and sometimes static file handles. After overwriting files in the web root:

```bash
# Apache
systemctl reload apache2

# Nginx
nginx -s reload
# or
systemctl reload nginx
```

`reload` is preferred over `restart` — it re-reads config and gracefully hands off connections without dropping active requests.

### Recommended deploy.sh Structure

```bash
#!/bin/bash
set -e

FRONTEND_DIR="/var/www/html/myapp"
BACKEND_DIR="/opt/myapp"

# 1. Copy frontend files
mkdir -p "$FRONTEND_DIR"
cp -r frontend/. "$FRONTEND_DIR/"

# 2. Rebuild and restart backend
cd "$BACKEND_DIR"
docker compose build
docker compose up -d

# 3. Reload web server
systemctl reload apache2

echo "Deploy complete"
```

### setup.sh vs deploy.sh Distinction

- `setup.sh` — runs once at first install: creates directories, sets permissions, initializes the database, sets up systemd services
- `deploy.sh` — runs on every update: copies code, rebuilds containers, reloads servers

Never run `setup.sh` after initial setup — it may overwrite config files or re-initialize databases.

## Related Concepts

- [[wiki/concepts/msal-vanilla-js-pkce]] — deploy pattern for social-reporting-tool where this was encountered
- [[wiki/architecture/docker-compose-patterns]] — container restart patterns that work alongside deploy scripts

## Sources

- [[daily/2026-04-15.md]] — Fixed broken deploy scripts in social listening project (commit `7a70283`)
