If your build pipeline generates compiled assets (such as CSS and JS files from Vite, Webpack, or similar tools) that need to be served from cloud storage like Cloudflare R2, you can configure DeployHQ to automatically sync those assets during each deployment.

This is a common pattern for frameworks like Laravel, where the application is deployed to a web server but static assets are served from a CDN-backed object storage bucket. Rather than manually syncing assets after each deployment, you can set up DeployHQ to handle this automatically using a server group.

## How it works

When your build pipeline runs (for example, `npm run build` for Vite), the compiled assets are generated on our build servers. DeployHQ then deploys all files, including the build output, to every server in the deployment.

By creating a server group that contains both your web server and a cloud storage server, DeployHQ will deploy your full application to the web server and only the compiled assets to the cloud storage, all in a single deployment. You control what goes where using per-server [excluded files](Article: #49).

## Step 1: Add a cloud storage server

In your project, go to **Servers & Groups** and click **New Server**. Choose the appropriate protocol for your storage provider:

- **S3-Compatible Storage** for Cloudflare R2, Wasabi, Backblaze B2, MinIO, or other S3-compatible services
- **Amazon S3 Bucket** for AWS S3

For Cloudflare R2, select **S3-Compatible Storage** and configure it as follows:

- **Endpoint URL**: `https://<ACCOUNT_ID>.r2.cloudflarestorage.com`
- **Bucket Name**: Your R2 bucket name (e.g., `my-site-assets`)
- **Access Key ID**: Your R2 API token access key
- **Secret Access Key**: Your R2 API token secret key
- **Path Prefix**: Optionally set a prefix if you want assets stored in a subdirectory within the bucket

For detailed instructions on configuring S3-compatible storage providers, see our [S3-Compatible Storage guide](Article: #640).

### Setting request headers

You may also want to configure request headers on your cloud storage server to set appropriate cache control for your assets. For example, since build tools like Vite include content hashes in filenames, you can safely set long cache durations:

- **Header Key**: `Cache-Control`
- **Header Value**: `max-age=31536000`
- **Pattern**: `*`

## Step 2: Create a server group

Go to **Servers & Groups** and click **New Server Group**. Give the group a descriptive name (e.g., "Production + Assets") and choose your preferred transfer order:

- **Parallel**: Both servers are deployed to at the same time, which is faster
- **Sequential**: One server completes its deployment before the next begins

Once the group is created, edit both your existing web server and the new cloud storage server, and assign them to this server group using the **Group** dropdown at the top of each server's settings page.

For more information on server groups, see [Working with multiple servers](Article: #161).

## Step 3: Configure excluded files per server

This is the key step. You need to configure [excluded files](Article: #49) so that the cloud storage server only receives the compiled assets, not your entire application.

Go to **Settings > Excluded Files** in your project and add exclusion rules for the cloud storage server. When adding each rule, uncheck **Exclude this file on all current and future servers?** and select only the cloud storage server.

### Example: Laravel with Vite

For a Laravel project using Vite, the compiled assets are output to `public/build/`. You would exclude everything except that directory on your cloud storage server:

| Excluded path | Applies to |
|---|---|
| `app` | Cloud storage server only |
| `app/**` | Cloud storage server only |
| `bootstrap` | Cloud storage server only |
| `bootstrap/**` | Cloud storage server only |
| `config` | Cloud storage server only |
| `config/**` | Cloud storage server only |
| `database` | Cloud storage server only |
| `database/**` | Cloud storage server only |
| `lang` | Cloud storage server only |
| `lang/**` | Cloud storage server only |
| `public/index.php` | Cloud storage server only |
| `public/.htaccess` | Cloud storage server only |
| `resources` | Cloud storage server only |
| `resources/**` | Cloud storage server only |
| `routes` | Cloud storage server only |
| `routes/**` | Cloud storage server only |
| `storage` | Cloud storage server only |
| `storage/**` | Cloud storage server only |
| `vendor` | Cloud storage server only |
| `vendor/**` | Cloud storage server only |
| `artisan` | Cloud storage server only |
| `composer.json` | Cloud storage server only |
| `composer.lock` | Cloud storage server only |
| `package.json` | Cloud storage server only |
| `package-lock.json` | Cloud storage server only |
| `vite.config.js` | Cloud storage server only |
| `.env.example` | Cloud storage server only |

Remember that you need two rules per directory: one for the directory itself and one for its contents (using `**`). See the [Excluded Files documentation](Article: #49) for more details on pattern matching.

Alternatively, you can add a `.deployignore` file to your repository for broader exclusions, though `.deployignore` does not support per-server rules. If you need different exclusion rules for different servers, use the DeployHQ interface.

### Tip: Use whitelisting for simpler configuration

If your project has many top-level directories, it may be easier to exclude everything and then whitelist the assets directory. You can do this by combining a broad exclusion rule with a whitelist rule:

- `**` (exclude everything)
- `!public/build/**` (whitelist the build output)

This approach requires fewer rules and is easier to maintain as your project structure changes.

## Step 4: Configure your application

Make sure your application is configured to reference assets from your cloud storage URL. In Laravel, this is typically done by setting the `ASSET_URL` environment variable to point to your R2 bucket's public URL, or by configuring the `asset_url` value in `config/app.php`.

## Step 5: Deploy

When you trigger a deployment (manually or via [automatic deployments](Article: #147)), the following happens:

1. DeployHQ checks out your code and runs your build pipeline (e.g., `npm run build`)
2. The compiled assets are generated on the build server
3. DeployHQ deploys the full application (including built assets) to your web server
4. DeployHQ deploys only the built assets to your cloud storage server (other files are excluded)

Both transfers happen within the same deployment, so your assets and application code stay in sync.

## Alternative approach: SSH commands

If you prefer not to use a server group, or you need behaviour beyond what the native S3 protocol supports (for example `aws s3 sync --delete`, custom storage classes, or other CLI flags), you can use an [SSH command](Article: #57) to drive the AWS CLI yourself.

SSH commands only run on servers whose protocol supports remote shell execution - that is, SSH or Shell servers. They cannot be attached to an S3 server (the S3 protocol has no shell), and the build server that runs your build pipeline is not a configurable SSH command target. Attach the command to your SSH/Shell deployment server (typically your web server) and have that server push the assets to S3:

```
aws s3 sync public/build/ s3://your-bucket-name/ \
  --endpoint-url https://YOUR_ACCOUNT_ID.r2.cloudflarestorage.com \
  --delete
```

Add this as an **after deployment** command on the SSH/Shell server. The `--delete` flag mirrors the source directory to the bucket, removing any objects in the destination that are not present in the source. The native DeployHQ S3 protocol does not accept arbitrary CLI flags, so this is the recommended path if you need that behaviour. See [Configuring an Amazon S3 Bucket](Article: #35#how-deployhq-deploys-to-s3) for details on how the native protocol handles deletions.

If you would rather run the sync from the build environment rather than from a deployment server, use a [build pipeline](Article: #301) step that includes the AWS CLI command. Build commands run on DeployHQ's build infrastructure as part of the build stage and can produce or push artifacts before the transfer stage begins.

Either approach requires the AWS CLI to be available on the server running the command and credentials to be configured there (for example, via environment variables or an AWS credentials file). The server group approach described above is preferred when you only need to upload changed assets, as it keeps everything managed within DeployHQ and does not require additional tools on your server.

## Supported storage providers

This guide applies to any S3-compatible storage provider supported by DeployHQ, including:

- Cloudflare R2
- Amazon S3
- Wasabi
- Backblaze B2
- DigitalOcean Spaces
- MinIO
- Katapult Object Storage

See [Configuring S3-Compatible Storage](Article: #640) for provider-specific setup instructions.
