Remix applications can run in several deployment models. This guide covers a server-rendered Remix application on Node.js, deployed from a Git repository to Ubuntu or another Linux VPS. It does not cover a purely static export.

One note on versions before we start. Remix's framework work merged into React Router, so what you are running is either Remix v2 or React Router in Framework Mode (v7 or the current v8 line). The deployment shape is the same in both cases — a Node process serving SSR, supervised by a process manager, behind a reverse proxy — so everything below applies either way. Remix 3 is a separate beta that replaces React entirely; it is not what this guide targets. If you want the DeployHQ-side configuration specifically — build commands, SSH commands, the exact fields to fill in — our [Remix deployment guide](https://www.deployhq.com/guides/remix) covers that, and this post covers the server underneath it.

The goal is a repeatable production deployment: fetch a known revision, install dependencies, build the application, restart the Node.js process, proxy traffic through Nginx, and verify the release before treating it as production-ready.

## What you need before deploying a Remix app

Before you start, make sure you have:

- A Remix application in a Git repository.
- SSH access to a Linux server.
- A supported Node.js version for the application and its dependencies.
- A domain name pointed at the server.
- The production environment variables required by the application.
- Permission to install packages and configure services on the server.
- A clear application port for the local Node.js process.
- A plan for process supervision, logs, HTTPS, backups, and rollback.

If you are choosing a VPS or preparing a Linux host, review [Linux server deployment commands and practices](https://www.deployhq.com/blog/essential-linux-server-commands-for-deployment) and [the guide to choosing a Linux distribution for production deployments](https://www.deployhq.com/blog/linux-distros-for-deployment).

A server-rendered Remix application needs a running Node.js process. Nginx will receive public requests and forward them to that process; it is not serving the Remix application as a collection of static files.

## How a Git-based Remix deployment works

A Git-based deployment separates source control from the running application. A typical release follows this sequence:

1. Select the intended branch, tag, or commit.
2. Fetch that revision on the server.
3. Install dependencies using the repository's lockfile.
4. Make the production environment available to the build and runtime.
5. Build the Remix application.
6. Update or restart the supervised Node.js process.
7. Check logs and health responses.
8. Keep the previous working revision available until the new release is verified.

This is more reliable than copying a folder from a laptop because the deployed revision and release steps can be repeated. For background on the underlying approach, see [setting up Git-based deployment on a virtual private server](https://www.deployhq.com/blog/setting-up-git-based-deployment-on-a-virtual-private-server-vps).

For teams that want to replace manual server work with a consistent deployment process, [Git deployment automation with DeployHQ](https://www.deployhq.com/blog/mastering-git-deployments-with-deployhq-a-comprehensive-guide) can connect a repository to deployment steps on the Linux server.

You can wire that up either now or later. If you would rather have the automation in place before you start hand-rolling releases, [deployments that trigger on every push](https://www.deployhq.com/features/automatic-deployments) is the piece that removes the manual step — connect the repository once and the sequence below runs itself on each commit. If you prefer to get one release out by hand first so you understand what the automation is doing, carry on and we will come back to it in the last section.

## Prepare the Linux server and Node.js runtime

Install the operating-system packages your application needs, then install the Node.js version required by the repository. The exact installation method depends on your Linux distribution and your team's runtime policy.

Check the version after installation:

```
node --version
npm --version
```

Compare the result with the version expected by the project. Check the repository's package metadata, documentation, and lockfile rather than assuming that the newest Node.js release is compatible.

The server should also have:

- Git for retrieving the application.
- A process manager such as PM2.
- Nginx for public HTTP traffic.
- A firewall configured to allow only the services that need to be public.
- A method for collecting or inspecting application and web-server logs.

Do not expose the Node.js application port directly to the internet if Nginx is intended to be the public entry point. Bind the application to the local interface where practical, and allow Nginx to proxy requests to it.

## Create the application directory and deployment user

Create a dedicated non-root user for the application or deployment process. Production application processes should not run as root: a vulnerability in the application should not automatically provide unrestricted access to the server.

Give the user ownership of the application directory and only the permissions it needs. Keep the repository, build output, runtime files, and logs in a predictable location, such as a directory under `/var/www/` or the deployment user's home directory.

The exact directory layout is a team decision, but it should support these requirements:

- The deployment user can fetch code and write release files.
- Nginx can read any files it must serve directly.
- Environment files are not publicly readable through the web root.
- The process manager can start the application as the non-root user.
- A previous release or revision can be identified during rollback.

For a broader hardening review, use [this Linux server security checklist for deployments](https://www.deployhq.com/blog/secure-linux-server-for-deployments).

## Clone the Remix repository and install production dependencies

Switch to the application user, move to the application directory, and clone the repository using the access method your team has approved. Keep repository credentials and private deploy keys out of the application directory and out of Git history.

After checking out the intended revision, install dependencies with the repository's lockfile. For an npm-based project, that commonly means:

```
npm ci
```

Use the package manager and lockfile that the repository actually specifies. Do not replace a lockfile-based installation with an ad hoc install just because it is shorter.

Build tools are often needed during compilation but not at runtime. Follow the application's package configuration and deployment design when deciding which dependencies remain available after the build. Removing packages too early can make the production start command fail, while keeping unnecessary tooling in a public application directory increases clutter and maintenance work.

## Configure Remix environment variables safely

Keep production secrets out of Git. Configure them on the server or through the deployment system, and make sure the running process receives them at startup.

A `.env.example` file is useful for documenting required variable names and safe placeholder values. A `.env` file contains environment-specific values and should not be committed when it contains secrets. The distinction, and practical ways to manage these files, are covered in [this guide to `.env` files and environment variables](https://www.deployhq.com/blog/understanding-env-files-and-environment-variables).

Before building, make an inventory of the variables required for:

- Database or API connections.
- Session and authentication configuration.
- Public application URLs.
- Third-party services.
- Runtime feature settings.
- Any build-time values read by the Remix application.

Do not assume that a variable available in your local shell will exist in production. Missing values can cause a build failure, a startup failure, or an application that loads but fails on its first request.

Also check how Remix exposes values to browser code. Values intended for the client must not contain private credentials merely because they are convenient to configure in the same environment file.

## Build the Remix app for production

Run the production build before starting the application:

```
npm run build
```

The exact build behavior is defined by the scripts in `package.json`. If the repository uses a different package manager or script name, follow that project configuration instead of changing the deployment command blindly.

A failed build should stop the release. Do not restart the process with an older build while reporting the new revision as deployed unless your deployment procedure clearly records that outcome.

Once the build succeeds, make the start command explicit. For many Node-based Remix applications, the repository provides a production start script that can be run with:

```
npm run start
```

Confirm the expected port and host in the application's configuration. Start the process locally on the server first and make a local request before putting Nginx in front of it. This separates application failures from reverse-proxy failures.

## Run the Remix Node server with PM2

A terminal session is not a process supervisor. Use PM2 or an equivalent process manager to keep the Node.js process running after you disconnect, restart it after an unexpected exit, and restore it after a server reboot.

The PM2 command should invoke the application's documented production start script, not a development server. For example, the process definition can be configured around the repository's start command and the port expected by Nginx.

After starting the process, check that it is online and inspect its logs. Then configure the process manager's startup integration according to the Linux distribution and PM2 documentation so the application returns after a reboot.

Do not consider the process setup complete until you have tested both failure recovery and reboot recovery. A process that works only while an SSH session is open is not a production deployment.

If the application is expected to remain available during releases, plan the restart strategy before automating it. [Zero-downtime deployment strategies](https://www.deployhq.com/blog/zero-downtime-deployments-keeping-your-application-running-smoothly) explain the trade-offs between restart, rolling, blue-green, and other release patterns.

This is also the point where the build step stops being a one-liner you run over SSH. Installing dependencies and running `npm run build` on the production box means a failed build happens _on_ the server, with a half-updated release directory. Running those steps in a [build pipeline](https://www.deployhq.com/features/build-pipelines) instead means the compile happens before anything is transferred, and a broken build never reaches the server at all.

## Configure Nginx as a reverse proxy

Nginx should terminate public HTTP traffic and proxy requests to the local Remix process. A minimal server block has these responsibilities:

- Listen for the domain name.
- Forward requests to the local application port.
- Preserve the original host and client information through forwarded headers.
- Support connection handling required by the application.
- Keep the Node.js port private from public traffic.

A representative proxy configuration looks like this:

```
server {
    listen 80;
    server_name example.com;

    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}
```

Use the port configured for your application, and adapt the server block to your operating system and domain setup. Test the Nginx configuration before reloading it. If Nginx returns a 502 response, verify that the Node.js process is running, listening on the expected address and port, and reachable by the Nginx user.

For a comparison of common reverse proxies and web servers, see [Nginx vs Apache vs Caddy](https://www.deployhq.com/blog/nginx-vs-apache-vs-caddy-choosing-the-right-web-server).

## Add HTTPS, firewall rules, and basic server security

Once HTTP proxying works, configure HTTPS for the domain and redirect plain HTTP traffic as appropriate for your certificate setup. Do not send production credentials or session data over an unencrypted connection.

Review the firewall so that SSH is restricted appropriately and only required public ports are exposed. Keep the application port private if Nginx is the public entry point.

Also confirm that:

- SSH authentication uses an approved method.
- The application runs as a non-root user.
- Secrets are readable only by the process that needs them.
- Operating-system and application updates have an owner.
- Logs are retained long enough to investigate failures.
- Backups cover important data and configuration.
- The previous working application revision can be restored.

## Automate future Git deployments with DeployHQ

Once the first release works manually, move the repeatable sequence into deployment steps rather than continuing to copy files or run commands from memory. Connect the Git repository to the Linux server in [DeployHQ](https://www.deployhq.com), then make the deployment process perform the actions your application requires:

1. Retrieve the intended revision.
2. Install dependencies with the repository's lockfile.
3. Make production environment variables available without committing secrets.
4. Run the production build.
5. Restart or reload the supervised application process.
6. Verify the local and public endpoints.

### Driving it from the terminal

The [DeployHQ](https://www.deployhq.com) CLI can create and inspect deployments without opening the dashboard. Authenticate once — credentials are stored in your OS keyring — and check the session:

```
dhq auth login
dhq auth status
```

List what the account can see:

```
dhq projects list
dhq servers list
```

[Deploy](https://www.deployhq.com) a branch to a named server and watch it to completion:

```
dhq deploy -p your-project -s production --branch main --wait
```

With `--wait` the CLI shows live progress and returns a non-zero exit code on failure, which is what makes it usable from a script. Server names are fuzzy-matched, so `-s prod` will find Production Web.

Before a first deployment, verify that [DeployHQ](https://www.deployhq.com) can actually reach the repository and the server — this runs the same connectivity checks a deployment runs, and catches wrong credentials, firewall rules, and SSH key problems before they surface as a failed release:

```
dhq test-access -p your-project -s production --wait
```

Afterwards, inspect the result:

```
dhq deployments show <deployment-id> -p your-project
dhq deployments logs <deployment-id> -p your-project
```

The exact project, branch, and server values depend on your [DeployHQ](https://www.deployhq.com) setup.

This approach gives small teams a recorded release sequence and makes it easier to repeat the same steps for staging and production. It also provides a clearer place to stop a release when dependency installation or the production build fails.

If you have not provisioned the Linux box yet, `dhq launch --vps` can provision and deploy to a managed one in a single command — worth knowing about, though the rest of this guide assumes a server you manage yourself.

## Verify the deployment and troubleshoot common failures

Verification should cover more than the final browser page. After each release:

1. Check the process manager status.
2. Review the application logs for startup errors.
3. Test the application locally on its configured port.
4. Validate the Nginx configuration and reload result.
5. Request the public HTTPS URL.
6. Check the browser for server-rendering, asset, and client-side errors.
7. Confirm that the deployed revision is the intended one.
8. Keep the previous working revision available until verification is complete.

Common failures include:

- **Missing environment variables:** compare production configuration with `.env.example` and inspect startup or request logs.
- **Node.js version mismatch:** compare the server runtime with the version expected by the repository.
- **Failed dependency installation:** confirm that the lockfile, package manager, and deployment user are consistent.
- **Failed production build:** run the build in the same environment used by the deployment process and inspect the first error.
- **Incorrect file permissions:** ensure the application user can read its files and write only where required.
- **Occupied application port:** identify the process using the port and align the application and Nginx configuration.
- **Nginx 502 response:** check that the Node.js process is online and listening where `proxy_pass` expects it.
- **Process missing after reboot:** test the process manager's startup integration rather than manually starting the application after every restart.
- **Unexpected browser errors:** inspect asset URLs, forwarded headers, HTTPS configuration, and server logs together.

For SSH, SFTP, and FTP connection problems during deployment, use [this server connection troubleshooting guide](https://www.deployhq.com/blog/troubleshooting-server-issues-with-deploy). For deployment-specific failures, preserve the logs and the revision identifier before changing the server again.

## Production checklist

Before calling the Remix deployment production-ready, confirm:

- The Git repository and intended revision are known.
- SSH access works without using the root account for application processes.
- The required Node.js version is installed.
- Dependencies install from the repository's lockfile.
- Production environment variables are configured outside Git.
- The Remix production build succeeds.
- The application starts with its documented production command.
- PM2 or an equivalent process manager supervises the Node.js process.
- The process returns after a server reboot.
- Nginx proxies the correct local port and preserves forwarded headers.
- HTTPS is enabled for the public domain.
- Firewall rules expose only required services.
- Application and Nginx logs are available.
- Backups cover important data and configuration.
- A previous working revision can be restored.
- Future releases follow the same Git-based sequence.

A Remix production deployment becomes much easier to operate when every release follows the same path: fetch the intended Git revision, install dependencies, build, restart under supervision, proxy through Nginx, verify, and retain a rollback option.

The manual version of that sequence is worth doing once, because it is how you learn what your application actually needs from a server. It is not worth doing twice. [Start a free](https://www.deployhq.com/signup)[DeployHQ](https://www.deployhq.com) account and turn the checklist above into a deployment that runs on every push instead of a list of SSH commands you re-type from memory.

* * *

For deployment guidance, contact [support@deployhq.com](mailto:support@deployhq.com) or follow [DeployHQ](https://www.deployhq.com) on [X](https://x.com/deployhq).

