Strapi is an open-source, Node.js-based headless CMS that gives developers full control over their content architecture. It exposes both REST and GraphQL APIs out of the box, supports custom content types through its admin panel, and lets you choose your own database, media storage, and frontend framework. That flexibility makes it a strong fit for projects ranging from marketing sites to mobile app backends.

This guide walks through deploying a Strapi project to a production server using [DeployHQ](https://www.deployhq.com/) for automated, repeatable releases.

## Prerequisites

- A Strapi 4.x or 5.x project in a Git repository (GitHub, GitLab, or Bitbucket)
- A Linux server (Ubuntu/Debian recommended) with SSH access
- Node.js 18+ and npm installed on the server
- A production database — PostgreSQL or MySQL (Strapi uses SQLite by default locally, but SQLite is not recommended for production)
- PM2 installed globally on the server (`npm install -g pm2`)

## Strapi Project Structure

Before configuring deployment, it helps to understand what gets deployed:

```
my-strapi-project/
├── config/              # Server, database, plugin, and middleware config
│   ├── database.js      # Database connection settings
│   ├── server.js        # Host, port, app keys
│   ├── middlewares.js    # Middleware stack
│   └── env/
│       └── production/  # Production-specific overrides
│           ├── database.js
│           └── server.js
├── src/
│   ├── api/             # Custom content types, controllers, routes
│   ├── components/      # Shared reusable components
│   └── plugins/         # Custom plugins
├── public/
│   └── uploads/         # User-uploaded media (local provider)
├── database/
│   └── migrations/      # Database migration files
├── .env                 # Environment variables (never committed)
├── package.json
└── package-lock.json
```

The key directories for deployment are `config/env/production/` (production-specific settings), `src/` (your application logic), and `public/uploads/` (persistent media that must survive between releases).

## Database Configuration for Production

Strapi defaults to SQLite for local development, but production deployments should use PostgreSQL or MySQL for reliability, concurrent access, and backups.

Create a production database configuration at `config/env/production/database.js`:

```javascript
module.exports = ({ env }) => ({
  connection: {
    client: 'postgres',
    connection: {
      host: env('DATABASE_HOST', '127.0.0.1'),
      port: env.int('DATABASE_PORT', 5432),
      database: env('DATABASE_NAME', 'strapi'),
      user: env('DATABASE_USERNAME', 'strapi'),
      password: env('DATABASE_PASSWORD'),
      ssl: env.bool('DATABASE_SSL', false) && {
        rejectUnauthorized: env.bool('DATABASE_SSL_REJECT_UNAUTHORIZED', true),
      },
    },
    pool: {
      min: env.int('DATABASE_POOL_MIN', 2),
      max: env.int('DATABASE_POOL_MAX', 10),
    },
  },
});
```

Install the appropriate database driver in your project before deploying:

```bash
# For PostgreSQL
npm install pg

# For MySQL
npm install mysql2
```

## Environment Configuration

Strapi reads environment variables from `.env` and supports environment-specific configuration through the `config/env/` directory. For production, you need at minimum:

```
HOST=0.0.0.0
PORT=1337
APP_KEYS=generate-four-random-base64-keys,comma-separated
API_TOKEN_SALT=generate-random-string
ADMIN_JWT_SECRET=generate-random-string
TRANSFER_TOKEN_SALT=generate-random-string
JWT_SECRET=generate-random-string

DATABASE_HOST=127.0.0.1
DATABASE_PORT=5432
DATABASE_NAME=strapi_production
DATABASE_USERNAME=strapi
DATABASE_PASSWORD=your-secure-password

NODE_ENV=production
```

Generate the secret keys with `openssl rand -base64 32` for each value. Never reuse keys between environments.

## Media Uploads and Storage Providers

By default, Strapi stores uploaded files to `public/uploads/` on disk. This works but introduces a deployment consideration: the uploads directory must persist between releases. For [zero-downtime deployments](https://www.deployhq.com/features/zero-downtime-deployments), you will configure this as a shared path in DeployHQ.

For production at scale, consider an external provider:

```bash
# AWS S3
npm install @strapi/provider-upload-aws-s3

# Cloudinary
npm install @strapi/provider-upload-cloudinary
```

Configure your chosen provider in `config/env/production/plugins.js`. External storage eliminates shared-path complexity entirely and keeps your deployment stateless.

## DeployHQ Setup

### Create a Project

Sign up at [DeployHQ](https://www.deployhq.com/signup) and create a new project. Connect your Git repository — DeployHQ supports GitHub, GitLab, and Bitbucket. For connection issues, consult the [support documentation](https://www.deployhq.com/support).

### Configure the Server

Go to **Servers** > **New Server**:

1. Name the server and select **SSH** as the protocol
2. Enter your server hostname or IP address
3. Create a dedicated deployment user on your server:

```bash
sudo adduser deployhq
sudo usermod -a -G www-data deployhq
su - deployhq
mkdir -p ~/.ssh && chmod 700 ~/.ssh
nano ~/.ssh/authorized_keys
```

4. Paste the DeployHQ public key, save, and set permissions:

```bash
chmod 600 ~/.ssh/authorized_keys
```

5. Set the **Deployment Path** to your application directory (e.g. `/var/www/strapi-app`)
6. Enable **Perform zero-downtime deployments on this server** — this creates `current`, `releases`, and `shared` directories, so each deploy is atomic with instant rollback capability
7. Set **Environment** to `production` and enable **Automatically deploy** on push

### Shared Paths for Persistent Data

When zero-downtime deployments are enabled, each release gets its own directory. Files that must persist across releases — like user uploads — need to be configured as shared paths.

Add this as a shared directory:

- `public/uploads` — preserves uploaded media between releases

DeployHQ symlinks these paths from the `shared/` directory into each new release automatically.

### Config Files

Go to **Config Files** > **New Config File** and create a `.env` file. Paste your production environment variables. DeployHQ injects this file into every release, keeping secrets out of your repository.

### Build Pipeline

Strapi needs to install dependencies and build the admin panel on each deploy. You can use [.deploybuild.yaml](https://www.deployhq.com/support/build-pipelines/deploybuild-dot-yaml) to run these steps in DeployHQ's build environment before files are transferred to your server:

```yaml
build:
  - npm ci --production=false
  - NODE_ENV=production npm run build
```

Using `npm ci --production=false` ensures devDependencies (required for the build step) are installed. The `npm run build` command compiles the Strapi admin panel — this is a separate React application that gets built into the `build/` directory.

### Post-Deploy SSH Commands

Go to **SSH Commands** > **New SSH Command** and create these commands in order:

**Install production dependencies:**

```bash
cd %path% && npm ci --omit=dev
```

**Restart the application with PM2:**

```bash
pm2 restart strapi --update-env || pm2 start npm --name strapi -- start
```

The fallback `pm2 start` handles the first deployment when no PM2 process exists yet. The `--update-env` flag ensures PM2 picks up any changed environment variables.

## PM2 Process Management

PM2 keeps your Strapi instance running, restarts it on crashes, and manages log rotation. Create an `ecosystem.config.js` in your project root:

```javascript
module.exports = {
  apps: [
    {
      name: 'strapi',
      cwd: '/var/www/strapi-app/current',
      script: 'npm',
      args: 'start',
      env: {
        NODE_ENV: 'production',
      },
      instances: 1,
      autorestart: true,
      max_memory_restart: '1G',
    },
  ],
};
```

After the first deployment, start the process with:

```bash
pm2 start ecosystem.config.js
pm2 save
pm2 startup
```

The `pm2 startup` command generates a system service so Strapi restarts automatically if the server reboots.

## Node.js Version Management

Strapi 5.x requires Node.js 18 or 20 (LTS versions). Use a version manager like `nvm` on your server to lock the Node version:

```bash
nvm install 20
nvm alias default 20
```

Add an `.nvmrc` file to your repository root so the correct version is documented:

```
20
```

## Health Checks and Monitoring

Strapi exposes a `/_health` endpoint that returns a 204 status when the server is running. Use this for uptime monitoring:

```bash
curl -sf http://localhost:1337/_health && echo "OK" || echo "DOWN"
```

Combine this with PM2's monitoring (`pm2 monit`) to track memory usage, CPU, and restart counts. For production, set up an external monitoring service that polls the health endpoint and alerts on downtime.

## Troubleshooting Common Issues

**Admin panel shows a blank page after deploy** — The admin build is missing or outdated. Verify `npm run build` ran successfully during deployment. Check that `build/` is not in your `.gitignore` if you are building locally, or that the build pipeline completed without errors.

**Database connection refused** — Confirm the database credentials in your `.env` match the production database. Check that PostgreSQL or MySQL is running and accepting connections on the configured host and port. If using SSL, verify the certificate settings.

**Uploads disappear after deploy** — The `public/uploads` directory is not configured as a shared path. With zero-downtime deployments, each release gets a fresh directory tree. Add `public/uploads` to shared paths in DeployHQ's server settings.

**Out of memory during build** — The Strapi admin build (a React webpack build) can consume significant memory. If your server has limited RAM, build in DeployHQ's build pipeline rather than on the server. Alternatively, increase the Node memory limit: `NODE_OPTIONS=--max-old-space-size=2048 npm run build`.

**PM2 process not found** — If PM2 cannot find the `strapi` process after the first deploy, the initial SSH command should use the `||` fallback pattern shown above. Subsequent deploys will find the existing process and restart it.

---

DeployHQ automates the entire Strapi deployment pipeline — from building the admin panel to restarting the production server — so every push to your repository triggers a consistent, repeatable release. [Start your free trial](https://www.deployhq.com/signup) and connect your Strapi project in minutes.

For more deployment guides covering other frameworks and platforms, browse the [full guides library](https://www.deployhq.com/guides). If you run into issues, reach out at [support@deployhq.com](mailto:support@deployhq.com) or on [Twitter/X](https://x.com/deployhq).