Deploying locally with Documenter.jl
September 23, 2025
When creating documentation for a Julia package, it is the de-facto standard to
use Documenter.jl.
For building the static HTML pages, it offers two main functions:
makedocs
and deploydocs
.
While you can execute makedocs
on your local machine without
problems, the same does not hold for deploydocs
as it pushes the
built pages to some remote git repository (e.g. on GitHub).
When you want to handle deploying your documentation more manually without a
hosting service like GitHub pages, this becomes a nuisance since
deploydocs
is also responsible for the version selection interface.
So we want to call it but not with the predefined configuration.
Here, I would like to demonstrate how you can trick
Documenter into
deploying your documentation locally.
The strategy
We will make use of the fact that git does not care at all where a
remote repository is located.
It can be somewhere on the network across the globe or it can be right on the
same disk.
Therefore, we will create a local remote repository .pseudo_remote
inside your docs
folder, direct Documenter to deploy into this
repository, and finally clone/pull from it into a public
folder,
which you can then put on your static hosting service or whatever.
The execution
Inside your docs
folder, where you probably also have a
make.jl
file, create a new deploy.jl
file.
In it, we first import the Documenter
module:
using Documenter
Next, we make sure that the .pseudo_remote
respository exists.
If the directory does not exist yet, we create it and initialize a bare git
repository.
It is important that it is a bare repository, otherwise it cannot function as a
remote.
Documenter uses git via Documenter.git()
, so let us do the same.
pseudo_remote = abspath(".pseudo_remote")
if !isdir(pseudo_remote)
mkdir(pseudo_remote)
cd(pseudo_remote) do
run(`$(Documenter.git()) init --bare`)
end
end
Now, we get to the interesting part.
Documenter has an API it calls
deployment systems.
A deployment system determines if and how and where documentation is deployed,
so that is exactly what we need.
To create our own such system, we need to subtype DeployConfig
:
struct LocalDeploy <: Documenter.DeployConfig end
The first thing LocalDeploy
has to decide is if the docs should
be deployed at all by adding a method to
Documenter.deploy_folder
that returns a
DeployDecision
.
This is relevant for CI systems where you might want to deploy only under
certain conditions.
We will just manually trigger deployment so we can simply always decide to
deploy by setting the all_ok
keyword to true
.
Some of the other keyword arguments we just propagate.
Documenter.deploy_folder(::LocalDeploy; repo, branch, kwargs...) =
Documenter.DeployDecision(; all_ok = true, branch, repo, subfolder = "main")
Documenter also wants to know where it can find the remote repository and what
protocol it is supposed to use for pushing (HTTPS or SSH).
We tell it to use HTTPS since this requires no concept of usernames or keys that
we would not have in our local-only setting.
And where to find the remote repository?
Well, that is just our local folder .pseudo_remote
!
Documenter.authentication_method(::LocalDeploy) = Documenter.HTTPS
Documenter.authenticated_repo_url(::LocalDeploy) = pseudo_remote
And with that we are ready to deploy.
We call the deploydocs
function and tell it to deploy to our pseudo
remote repository and use LocalDeploy()
as configuration.
This also assumes that we want to deploy the docs from the main
branch.
deploydocs(;
repo = pseudo_remote,
branch = "main",
deploy_config = LocalDeploy(),
devurl = "main",
)
The remote repository now contains the deployed HTML pages.
However, it is the nature of bare git repositories that we cannot directly
access the files.
Instead, we must clone it or, if we already did that before, pull from it.
We use the local folder public
as the target for this operation:
deploy_dir = "public"
if isdir(deploy_dir)
cd(deploy_dir) do
run(`$(Documenter.git()) pull`)
end
else
run(`$(Documenter.git()) clone $pseudo_remote $deploy_dir`)
end
What you now want to do with the files in public
is up to you.
If, for example, you want to publish them to your static web hosting service,
you could copy them there via
scp
:
items = filter(!endswith(".git"), readdir(deploy_dir; join = true))
run(`scp -r $items user@your.domain:path/to/Package.jl`)
From your package's root directory you can then run
julia --project=docs make.jl
julia --project=docs deploy.jl
to deploy your documentation.
Summary of deploy.jl
using Documenter
pseudo_remote = abspath(".pseudo_remote")
if !isdir(pseudo_remote)
mkdir(pseudo_remote)
cd(pseudo_remote) do
run(`$(Documenter.git()) init --bare`)
end
end
struct LocalDeploy <: Documenter.DeployConfig end
Documenter.deploy_folder(::LocalDeploy; repo, branch, kwargs...) =
Documenter.DeployDecision(; all_ok = true, branch, repo, subfolder = "main")
Documenter.authentication_method(::LocalDeploy) = Documenter.HTTPS
Documenter.authenticated_repo_url(::LocalDeploy) = pseudo_remote
deploydocs(;
repo = pseudo_remote,
branch = "main",
deploy_config = LocalDeploy(),
devurl = "main",
)
deploy_dir = "public"
if isdir(deploy_dir)
cd(deploy_dir) do
run(`$(Documenter.git()) pull`)
end
else
run(`$(Documenter.git()) clone $pseudo_remote $deploy_dir`)
end
# only if you want to directly put the docs on a server
items = filter(!endswith(".git"), readdir(deploy_dir; join = true))
run(`scp -r $items user@your.domain:path/to/Package.jl`)