Deploy Docusaurus to Your Own Server with DeployHQ
Most guides for deploying Docusaurus assume you've already picked a managed static host: Netlify, Vercel, Cloudflare Pages, GitHub Pages, Render, or one of the half-dozen other options the official deployment docs walk through. This one assumes the opposite. You've already decided you're not using one of those — maybe the docs sit behind a VPN, maybe compliance rules where the bytes live, maybe you're hosting at a sub-path under a marketing site, or maybe you're already paying for the VPS and don't want another bill. Whatever the reason, you need a build pipeline that runs npm run build somewhere clean and ships the resulting build/ directory to a server you control.
That's the gap DeployHQ fills. The official Docusaurus deployment docs cover the managed hosts well and then keep the self-hosting note short, because self-hosting depends entirely on your server. DeployHQ slots between those two worlds: it gives you a managed build environment that produces the static output, and then transfers that output to whatever target you control — a Linux VPS, a shared host with SFTP, an S3 bucket fronted by CloudFront, or an Azure Blob container behind Azure Front Door.
By the end of this guide, you'll have a Docusaurus site that rebuilds automatically on every push to your tracked branch, ships only the compiled build/ directory to your server, and can be reverted to any previous deploy with one click — all without giving up control of where the files actually live.
Prerequisites
You'll need three things before starting.
A Git repository hosting your Docusaurus site, on GitHub, GitLab, or Bitbucket. DeployHQ can deploy from GitHub to your server, deploy from GitLab, or deploy from Bitbucket — pick whichever already hosts your repo. Private repositories work; OAuth handles the access.
A deploy target. This is whatever ends up serving your static site to readers. In practice that means a Linux VPS with SSH access, a shared host that exposes SFTP or FTP, or an object-storage bucket — Amazon S3, Azure Blob Storage, or Rackspace Cloud Files. Docusaurus emits a fully static site by default, so any target that can serve static files works. No Node runtime is required on the destination server.
A Docusaurus site that builds locally. Run npm run build on your machine and confirm it produces a build/ directory containing index.html and the rest of the compiled output. If the local build fails, it'll fail on DeployHQ too — fix it first.
One version note: Docusaurus 3.x requires Node.js 20.0 or above. You'll pick this version in DeployHQ's build environment in a later step, but it's worth confirming your local Node matches so the local and remote builds behave the same way.
Why deploy Docusaurus to your own server
Self-hosting Docusaurus is the right call in a few specific situations.
Internal documentation that needs to sit behind a VPN, SSO gateway, or IP allow-list. Managed static hosts can do some of this, but it's usually cleaner to put the files on a server already behind your existing access controls.
Compliance constraints on data residency or vendor approval. If your security team has cleared a particular VPS provider or cloud region, shipping there avoids another procurement cycle.
Sub-path hosting, where Docusaurus lives at /docs/ underneath an existing marketing site at the same domain. Managed static hosts make this awkward; a directory on your existing web server makes it trivial.
An existing server you're already paying for. Spare capacity on a VPS that's already provisioned, monitored, and backed up means adding a Docusaurus site to it is mostly free.
A preference for treating deploy targets as infrastructure-as-code. With BYO-server you keep web-server config, TLS termination, and cache headers in the same repos as the rest of your infrastructure.
Self-hosting is the wrong call when you're starting from zero. If you don't already have a server and the site is a brand-new public property, the managed static hosts in the official Docusaurus deployment docs will be faster. This guide is for the other group — people who've already decided they want managed build and self-hosted output.
Connect your repository
Sign up for a DeployHQ account and create a new project. The project is the unit that wraps one repository and its associated build configuration; you can attach multiple servers to it later (for production and staging).
Connect your Git provider when prompted. DeployHQ authenticates over OAuth for GitHub, GitLab, and Bitbucket, which means private repositories work without any further token juggling. Pick the repository that holds your Docusaurus site and the branch you want to treat as the source of truth — main is the standard pick for production. You'll wire up a separate branch for staging in the multi-environment section below.
At this point DeployHQ knows where your source code lives. It does not yet know what to do with it. The next two sections — Configure the build and Configure the deploy target — define the pipeline. Internally, DeployHQ will clone your repository into its own build infrastructure on every deploy, run whatever commands you give it, and then transfer the output to your server. Your repository itself is never pushed to the destination; only the files you choose to deploy are.
Configure the build
A build pipeline is a series of shell commands DeployHQ runs inside an isolated build environment before transferring files to your server. For a Docusaurus site, the pipeline is short: install dependencies, then run the build.
Add the following command as the build pipeline step:
npm ci && npm run build
Use npm ci rather than npm install. The ci variant installs strictly from the lockfile, fails fast on any mismatch, and produces deterministic results across runs — exactly what you want in a build pipeline. The build then runs Docusaurus's standard command, which writes the static output to the build/ directory.
If you use a different package manager, swap the install/build pair:
- Yarn:
yarn install --frozen-lockfile && yarn build - pnpm:
pnpm install --frozen-lockfile && pnpm build - Bun:
bun install --frozen-lockfile && bun run build
Set the Node version to 20 or higher to match Docusaurus 3.x's requirement. DeployHQ's automated build pipeline supports selecting a Node version per project from its build-environment configuration, alongside the install and build commands you've already added.
Environment variables
Docusaurus sites frequently read configuration from process.env at build time. The canonical examples: docusaurus.config.js reading process.env.URL so the same source builds for production and staging, an analytics ID injected via process.env.GA_TRACKING_ID, or Algolia DocSearch credentials read from process.env.ALGOLIA_APP_ID and process.env.ALGOLIA_API_KEY.
DeployHQ exposes a config-variables section per server (and per project) that sets these values inside the build environment. The variables are encrypted at rest, can be scoped to all servers or to specific environments, and can be locked to prevent accidental display in the UI. Referenced inside DeployHQ's own commands or config files, custom variables use the %VARIABLE_NAME% syntax — but inside your Node build the same value is available as process.env.VARIABLE_NAME as usual. There's nothing Docusaurus-specific to do; the standard process.env pattern from the framework docs works as-is.
Keep secrets like Algolia API keys in DeployHQ config vars rather than in your repository. The encryption and scoping are the point of moving them out of source control.
Deployment subdirectory
This is the single most important configuration step in the entire guide, and skipping it is the most common cause of "why did my whole repo end up on my server" support tickets.
Set the deployment subdirectory to build.
DeployHQ's default behavior, on a freshly-created server within a project, is to transfer the entire repository root to your deploy target. For a typical web app that's fine; for Docusaurus it's wrong, because the repository root contains node_modules, src, docs, static, the Docusaurus config files, and your full Git history's worth of source — not the compiled output. Telling DeployHQ that the deployment subdirectory is build restricts the transfer to only the files Docusaurus actually produced.
The subdirectory is a per-server setting, so set it on each server you attach to the project. The same project might deploy to production on one server (subdirectory build) and to a staging environment on another (also subdirectory build).
Configure the deploy target
DeployHQ supports three categories of deploy target for a static site. Pick the one that matches what you've already got.
Deploy to a Linux VPS over SSH
Add a server to your project and choose the SSH protocol. DeployHQ generates a deploy key for the server; paste its public half into ~/.ssh/authorized_keys on the destination machine under whichever user account will own the files (commonly a dedicated deploy user, sometimes www-data or the user that owns your webroot).
Set the deployment path to the directory your web server serves from. Typical paths look like /var/www/docs.example.com/current/, /home/deploy/sites/docs/, or /srv/www/docs/. Whatever you pick, make sure the user the SSH key belongs to has write access to that directory.
The web-server configuration — Nginx, Apache, or Caddy — stays your responsibility. Docusaurus is a static site, so the server config is straightforward: serve files out of the deployment path, fall back to index.html for unmatched routes, and serve index.html for clean URLs. A minimal Nginx server block for a Docusaurus site at the root of a domain looks like this:
server {
listen 443 ssl http2;
server_name docs.example.com;
root /var/www/docs.example.com/current;
index index.html;
location / {
try_files $uri $uri/index.html $uri.html =404;
}
}
The try_files directive is what makes both /getting-started/ and /getting-started work without surprising the reader with a 404.
Deploy to shared hosting over SFTP or FTP
The flow is identical to the SSH path, except the protocol is SFTP (preferred, since it's encrypted) or FTP. DeployHQ asks for the host, username, password (or SSH key for SFTP), and the remote path. Set the deployment subdirectory to build as above; set the remote path to whatever directory cPanel, DirectAdmin, or Plesk serves from — commonly public_html/docs/ or httpdocs/.
Shared hosts generally don't let you write a custom Nginx or Apache config. That usually doesn't matter for Docusaurus, because the default index.html resolution works for most pages. If you run into 404s on clean URLs, drop an .htaccess file into the deployed directory with a few rewrite rules, or contact the host about enabling MultiViews.
Deploy to S3, Azure Blob, or Rackspace Cloud Files
Object storage is a good fit when you want to front a static site with a CDN — CloudFront in front of S3, Azure Front Door in front of Blob Storage, or Akamai in front of Rackspace. DeployHQ writes files directly into the bucket; the CDN serves them to readers.
Three things to keep in mind for object-storage targets. The bucket needs static-website-hosting enabled (S3) or static-content serving configured (Azure, Rackspace) before deploys will produce a working site. The deployment subdirectory is still build. Cache-control headers and CORS are your responsibility — typical Docusaurus assets fingerprint their filenames, so long-lived cache headers on /assets/* and shorter cache on index.html is a common pattern.
Handle baseUrl, trailing slashes, and sub-path hosting
This is where BYO-server deployments earn their keep, and where most Docusaurus self-hosting guides go silent. The official deployment docs note that each static hosting provider behaves differently around trailing slashes — and they're right, because each managed host has its own opinions baked in. When you control the web server, you also control the behavior. Three common configurations cover most cases.
Root-domain hosting
Your docusaurus.config.js has url: 'https://docs.example.com' and baseUrl: '/'. This is the simplest case. Set the DeployHQ deployment path to your webroot — /var/www/docs.example.com/current/ — and the Nginx block from the SSH section above will work without changes. No special routing is required.
Sub-path hosting
Docusaurus is served at /docs/ underneath an existing marketing site at https://example.com. Set url: 'https://example.com' and baseUrl: '/docs/' in docusaurus.config.js (both leading and trailing slashes are required on baseUrl, per the Docusaurus configuration docs).
On the deploy side, set the DeployHQ deployment path to /var/www/example.com/docs/. Your existing Nginx server block for example.com needs a location /docs/ directive that points at this directory:
location /docs/ {
alias /var/www/example.com/docs/;
try_files $uri $uri/index.html $uri.html =404;
}
The alias is important — root would append /docs/ to the path again and produce a double directory. The try_files line behaves the same way as in the root case, just scoped to the sub-path.
Trailing-slash behavior
Docusaurus exposes the trailingSlash config so you can choose how URLs and emitted filenames should look. With a managed host the right value depends on the host's own URL rewriting. With BYO-server, the right value depends on the web-server config you write.
If you leave trailingSlash at its default and serve via the Nginx try_files block above, both /page/ and /page resolve to the same index.html. If you set trailingSlash: true, Docusaurus emits every page as a directory with index.html inside, which is the safest configuration for Nginx and Apache because it matches how try_files naturally resolves clean URLs. If you set trailingSlash: false, Docusaurus emits flat .html files, and you typically need either try_files $uri $uri.html =404 (Nginx) or Options +MultiViews (Apache) to serve /page from page.html.
Caddy is the simplest of the three because its file-server handler already treats try_files as a default — file_server with no extra config matches Docusaurus's default output. Apache users hosting trailingSlash: false should add Options +MultiViews and an .htaccess rewrite rule that falls back to index.html for unmatched paths.
The point is that this decision is yours to make once and freeze in your web-server config. You don't have to chase whatever opinion your managed host has this quarter.
Deploy and verify
Push a commit to your tracked branch. DeployHQ receives the webhook, runs the build pipeline inside its build environment, and transfers the contents of the build/ directory to your deploy target. The first deploy is a full transfer of every file. Subsequent deploys send only the files that changed — DeployHQ tracks the previous deploy state and computes a diff before transferring, which is what makes incremental deploys fast.
On SSH targets, DeployHQ performs an atomic deployment by writing files into a release directory and then switching a current symlink to point at the new release. Readers either see the old release or the new one — never a half-deployed mix. The same Git-driven flow is what powers automatic deployments on every push.
Verify three things after the first deploy.
Load the home page in a browser. If it renders, the build worked and the transfer landed in the right place. If you see a directory listing or a nginx welcome page, the web-server root (or alias) doesn't match the deployment path.
Load a deep URL — say https://docs.example.com/getting-started/ — and confirm it resolves to the right Docusaurus page. If you see a 404, the web-server try_files directive isn't catching the route. Run curl -I https://docs.example.com/getting-started/ to see whether the response is a 404 from Nginx or from your application; the answer tells you which config to fix.
Check the deploy log in the DeployHQ dashboard. The log shows the build output, the file diff transferred, and any errors. Successful builds produce a green check on the deploy entry; failed builds show the failing command and its stderr.
Roll back a bad deploy
Things break. The Docusaurus build picks up a broken plugin upgrade; a typo lands in the navbar and gets screencapped in three Slack channels; the new versioned docs section breaks the sidebar layout. The remedy on managed static hosts is usually to revert the commit, wait for the rebuild, and apologize. On DeployHQ you skip the wait.
Every deploy can be reverted with one-click rollback from the dashboard. Pick the previous deploy from the project's history, click rollback, and the deploy target is restored to that release. On atomic SSH targets the rollback is the same atomic symlink switch as a normal deploy — readers either see the broken release or the previous one, never a half-rolled-back mix. On non-atomic targets (FTP, some shared hosts) the rollback re-transfers the previous release's files.
A few things rollback doesn't fix, and it's worth being explicit about them. Rollback restores the file state on the deploy target. It does not undo DNS changes, web-server config changes, TLS certificate changes, or anything outside the files DeployHQ deployed. If your breakage is in nginx.conf rather than index.html, rolling back the deploy won't help — you need to revert the server config separately. The same applies to third-party services: an Algolia DocSearch index refresh, an analytics property migration, or a CDN cache that needs invalidation are all outside DeployHQ's scope.
That said, for the common breakage class — a bad commit produced a bad build that produced a broken site — rollback is the fastest path back to a working site, measured in the time it takes to click the button.
Multi-environment setup
A standard Docusaurus team setup runs production at docs.example.com, staging at docs-staging.example.com, and uses staging for content review before merging changes into the production branch.
DeployHQ models this directly. Attach two servers to the same project: one is your production server (deploy path /var/www/docs.example.com/current/), the other is your staging server (deploy path /var/www/docs-staging.example.com/current/). Map the production server to the main branch and the staging server to a staging branch. From that point on, pushing to main deploys to production and pushing to staging deploys to staging, with no extra ceremony.
Each server has its own config-variables block, which is how you give each environment a different Docusaurus configuration without forking the source. Set URL=https://docs.example.com on the production server and URL=https://docs-staging.example.com on staging. Reference the variable inside docusaurus.config.js:
module.exports = {
url: process.env.URL || 'https://docs.example.com',
baseUrl: '/',
// ...
};
The same pattern applies to Algolia DocSearch (a separate index per environment), Google Analytics (a separate property per environment), or feature flags. Each server's config vars are scoped to that server, so secrets don't leak between environments.
For pull-request preview deploys, you can stand up a third server pointed at a preview branch — or, for teams using GitHub flow, treat staging as the preview environment and merge to main only after a content reviewer approves on staging.
Troubleshooting
Four problems cover most of the support tickets we see on Docusaurus deploys.
Build fails with "out of memory"
Large Docusaurus sites — especially ones with versioned docs, hundreds of pages, or large MDX components — can exceed Node's default heap size during the production build. The fix is to raise the limit:
NODE_OPTIONS=--max-old-space-size=4096 npm run build
4096 is megabytes of heap; raise it further if the build still fails. The right setting depends on the build-environment memory available to your DeployHQ project, so check the build log for the actual OOM line before guessing.
404s on every non-root route
The home page loads but /getting-started/, /blog/, and every other route returns a 404. This is almost always a baseUrl or try_files problem. Check docusaurus.config.js first — baseUrl must include both leading and trailing slashes (e.g. /docs/, not docs or /docs). Then check the web-server config. Running curl -I https://yoursite/getting-started/ will tell you whether the 404 is coming from Nginx (no matching location, no try_files fallback) or from a successful response that just happens to be the Docusaurus 404 page.
The whole repository ended up on the server
Source files, node_modules, the repo's full Git tree — all of it landed on the deploy target instead of just build/. The deployment subdirectory wasn't set to build. Open the project's server configuration, set the subdirectory to build, and redeploy. The next deploy will transfer only the compiled output.
Search index missing or stale
Algolia DocSearch and the Docusaurus local-search plugin both emit their indexes into build/ at build time. If search is broken after a deploy, run npm run build locally and confirm the search index files appear in build/. If they're present locally but missing on the server, check the DeployHQ deploy log for the transfer; an aggressive ignore pattern can sometimes skip them.
Closing and next steps
If you've landed here, managed static hosts aren't the right fit. DeployHQ's BYO-server-with-managed-build setup is the closest thing to a managed-host developer experience while still owning your deploy target.
The setup is small: connect a repository, add npm ci && npm run build, set the deployment subdirectory to build, point a server at the right path, and push. Every commit becomes a deploy. Bad commits roll back in one click. New environments are a server attachment, not a separate platform.
Start deploying your Docusaurus site to your own server — create a free DeployHQ account and connect your first repository.
For related deployment guides on the same surface, see Aider as a terminal-based AI pair programmer or Cursor with Git-driven deployments.
Questions? Email support@deployhq.com or reach us at @deployhq.