Analytics cookies only (Google Analytics, PostHog). No ad networks, no cross-site tracking. See our privacy policy.
Updated Oct 2026
verified on Ubuntu 26.04 · Oct 2026Self-host XWiki with the postgres-tomcat image on a 2 GB VPS — pinned tag, generated DB password, xwiki.home behind Caddy HTTPS, Distribution Wizard, backups.
XWiki is a Java enterprise wiki with structured data: pages can carry typed classes and forms, be queried and rendered through scripting, and be extended from an in-app extension manager with hundreds of add-ons, which is how teams build small applications inside it. It is LGPL-2.1-or-later and runs in Tomcat against PostgreSQL, MySQL or MariaDB; the official image ships Tomcat, the JDBC driver and LibreOffice for office-document import. It is rated 3 / 5 to deploy.
The rating is about what happens after docker compose up. XWiki starts
slowly, and its first visit runs a Distribution Wizard in the browser that
creates the admin account and installs the default flavor before the wiki is
usable. Whoever reaches the wiki first runs it, so this guide gets you to that
page with HTTPS already in place. If you do not need the structured-data and
application features, BookStack (and its
deploy guide) or
Wiki.js are simpler to run. If you are replacing a
hosted product, see Confluence alternatives.
Upstream publishes no RAM minimum for the Docker image. What it does publish
is the heap: the image's Tomcat setenv.sh sets -Xmx1024m, "adding more memory (default is 512MB which is not
enough for XWiki)", and that heap plus LibreOffice is where
our 2 GB floor comes from. Users override it by setting the JAVA_OPTS
environment variable; the file's own example is -e JAVA_OPTS="-Xmx2048m",
and it only adds its own -Xmx when yours does not include one. On a 4 GB
server, adding JAVA_OPTS: -Xmx2048m to the web service's environment is
the one tuning step worth making for a busy wiki.
In our install check (GCP e2-standard-2, Ubuntu 26.04, October 2026) the stack of XWiki and PostgreSQL idled at about 740 MB of RAM and about 3.1 GB of Docker disk before the Distribution Wizard had installed the flavor. The flavor adds extensions, and the search index and attachments grow from there.
Install Docker and the Compose plugin by following Docker and Compose on Ubuntu, and confirm the versions:
docker --version
docker compose version
Open SSH plus the two ports Caddy will use. XWiki itself gets no public port:
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw --force enable
Docker publishes container ports by editing the firewall directly, which
bypasses ufw rules. That is why the compose file below binds Tomcat's port
to 127.0.0.1 instead of relying on ufw to block it.
Paid link — we earn a commission if you shop through it.
Generate the database password. Upstream's .env ships DB_PASSWORD=xwiki;
this generates one, and the same value reaches both containers through the
compose file. The block only writes .env if it does not exist, so
re-running it cannot change the password under a live database:
mkdir -p ~/xwiki && cd ~/xwiki
[ -f .env ] || echo "DB_PASSWORD=$(openssl rand -hex 16)" > .env
chmod 600 .env
Now the compose file. Apart from the port binding it is the file we verified
against a fresh server, which is upstream's postgres-tomcat compose with
two deliberate changes: a pinned XWiki tag, and a pinned PostgreSQL major in
place of upstream's postgres:latest (its own comment says to "pin a specific
major instead if you need to control when a major database upgrade reaches
your data volume").
cd ~/xwiki
cat > docker-compose.yml <<'EOF'
services:
web:
image: xwiki:18.8.0-postgres-tomcat
restart: unless-stopped
init: true
depends_on:
- db
ports:
- "127.0.0.1:8080:8080"
environment:
DB_USER: xwiki
DB_PASSWORD: ${DB_PASSWORD}
DB_DATABASE: xwiki
DB_HOST: db
volumes:
- xwiki-data:/usr/local/xwiki
db:
image: postgres:18.6
restart: unless-stopped
environment:
POSTGRES_USER: xwiki
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: xwiki
POSTGRES_INITDB_ARGS: --encoding=UTF8 --locale-provider=builtin --locale=C.UTF-8
volumes:
- postgres-data:/var/lib/postgresql
volumes:
xwiki-data:
postgres-data:
EOF
Keep init: true. Upstream's compose comment explains it: the JVM as PID 1
never reaps children, so LibreOffice processes left behind when soffice.bin
restarts would otherwise linger as zombies. The xwiki-data volume is the
permanent directory, /usr/local/xwiki/data inside the container: installed
extensions, configuration, the search index and temp files. The image's
entrypoint copies any xwiki.cfg, xwiki.properties or hibernate.cfg.xml
it finds in the permanent directory over the webapp's copy on every start,
which is what the domain step below relies on.
Start the stack and wait for Tomcat to serve the wiki:
cd ~/xwiki
docker compose up -d
out=000
for i in $(seq 1 60); do
out=$(curl -sL -o /dev/null -w '%{http_code}' http://127.0.0.1:8080/)
[ "$out" = "200" ] && break
sleep 10
done
docker compose ps
echo "wiki over loopback (after redirects): $out"
[ "$out" = "200" ] || false
The -L matters. XWiki's / redirects, to the main page on an installed
wiki and to the Distribution Wizard on a fresh one, so the loop follows the
redirects and requires the final page to be 200. Until Tomcat has deployed
the application, curl gets a connection refusal or a 404, and the loop
keeps waiting; the first start takes several minutes.
Create an A record for your hostname (say wiki.example.com) pointing at
the server. With no domain yet, an sslip.io name resolves to your server's
IP with no DNS setup, which is what this guide uses so every command runs as
written:
DOMAIN="xwiki.$(curl -4s https://ifconfig.me).sslip.io"
echo "$DOMAIN"
Keep this shell session open (or re-run the line) for the rest of the guide, and replace the value with your real hostname when you have one.
Behind a proxy XWiki builds its links from the request, which means the
X-Forwarded-* headers Caddy sends by default (below). Links built outside a
request, such as those in notification mails, need the public address spelled
out, and the stock xwiki.cfg has a property for it: xwiki.home, "the
domain name to use when creating URLs to the default wiki", which "should
contain schema, domain and (optional) port, and the trailing /". The image
keeps no copy of that file in the permanent directory until you make one, so
put a copy of the stock file there, add the line to it, and the entrypoint
installs that copy into Tomcat on every later start:
cd ~/xwiki
docker compose exec -T web sh -c "[ -f /usr/local/xwiki/data/xwiki.cfg ] || cp /usr/local/tomcat/webapps/ROOT/WEB-INF/xwiki.cfg /usr/local/xwiki/data/xwiki.cfg; grep -q '^xwiki.home=' /usr/local/xwiki/data/xwiki.cfg || echo 'xwiki.home=https://$DOMAIN/' >> /usr/local/xwiki/data/xwiki.cfg"
docker compose exec -T web grep -n '^xwiki.home=' /usr/local/xwiki/data/xwiki.cfg
One warning, from the same xwiki.cfg comment: when xwiki.home is set,
"the generated URLs will point to this server instead of the requested one",
so an address left over from an earlier deployment overrides a correctly
configured proxy. When you move
from the sslip.io name to a real domain, change this line (a sed on
^xwiki.home=), not just the Caddyfile. Restart the wiki so Tomcat picks up
the file, and wait for it again:
cd ~/xwiki
docker compose restart web
out=000
for i in $(seq 1 60); do
out=$(curl -sL -o /dev/null -w '%{http_code}' http://127.0.0.1:8080/)
[ "$out" = "200" ] && break
sleep 10
done
echo "wiki after restart: $out"
[ "$out" = "200" ] || false
Run Caddy as a container on the host network. It obtains and renews a Let's Encrypt certificate on its own (see Automatic HTTPS with Caddy) and proxies to the loopback port:
mkdir -p ~/caddy
cat > ~/caddy/Caddyfile <<EOF
$DOMAIN {
reverse_proxy 127.0.0.1:8080
}
EOF
docker rm -f caddy >/dev/null 2>&1 || true
docker run -d --name caddy --restart unless-stopped --network host \
-v ~/caddy/Caddyfile:/etc/caddy/Caddyfile:ro -v caddy_data:/data caddy:2
Caddy's reverse_proxy needs no header configuration: it passes incoming headers including Host through
unchanged and sets X-Forwarded-For, X-Forwarded-Proto and
X-Forwarded-Host, so the one-line Caddyfile is complete.
Check that HTTPS answers with a trusted certificate, retrying while the
certificate is issued. Again the request follows redirects, because /
lands on the wizard until you have run it:
out=000
for i in $(seq 1 24); do
out=$(curl -sL -o /dev/null -w '%{http_code} %{ssl_verify_result}' "https://$DOMAIN/") && [ "${out%% *}" = "200" ] && break
sleep 5
done
echo "wiki over HTTPS and certificate check: $out"
[ "${out%% *}" = "200" ] || false
A second number of 0 means the certificate verified.
Open https://wiki.example.com (your hostname) now, before you share the
address. The Distribution Wizard runs on the first visit: it asks you to
create the administrator account, then downloads and installs the default
flavor, extension by extension, which takes several minutes and a few clicks
through its steps. Whoever reaches the wiki first does this, which is why the
guide put HTTPS in front before this step rather than after.
When it finishes you land on the home page signed in as the admin. From the
administration area, set the wiki's name and the mail server; the links in
the mails XWiki sends are built from the xwiki.home value above.
An XWiki instance keeps its data in several places, and a backup needs all of them. In this setup those places are:
pg_dump is
the portable copy.xwiki-data volume): installed extensions,
configuration, the search index. Its cache subdirectory is rebuilt and can
be left out..env and docker-compose.yml, which are not in any volume.Dump the database with the stack running, then stop the wiki for a consistent copy of the volume, archive it, and start again:
cd ~/xwiki
mkdir -p ~/xwiki-backup
docker compose exec -T db pg_dump -U xwiki xwiki | gzip > ~/xwiki-backup/xwiki-$(date +%F).sql.gz
docker compose stop web
v="$(basename "$PWD")_xwiki-data"
docker run --rm -v "$v":/v:ro -v ~/xwiki-backup:/b alpine tar czf "/b/xwiki-data-$(date +%F).tgz" --exclude=./data/cache -C /v .
docker compose start web
cp .env docker-compose.yml ~/xwiki-backup/
ls -lh ~/xwiki-backup
The wiki takes its usual few minutes to come back after start. Copy the
archives off the server to object storage or another machine on a schedule.
To restore, bring up an empty stack with the same .env and compose file,
unpack the volume archive into the new xwiki-data volume, pipe the dump
into psql in the db container, and start web. An untested backup is a
guess, so restore into a throwaway server now and then.
The upgrade path for a Docker install is to start a container of the new version against the data of the old one: check that the permanent directory is on a volume (it is), read the release notes of every version between yours and the target for manual migration steps and configuration changes, stop the XWiki container while leaving the database running, apply any configuration changes the notes require, start the new version on the same volumes, and then upgrade the flavor from a browser, which the Distribution Wizard offers as soon as you log in as an administrator. Take the backup above first.
cd ~/xwiki
sed -i 's|xwiki:18.8.0-postgres-tomcat|xwiki:NEW_VERSION-postgres-tomcat|' docker-compose.yml
docker compose pull web
docker compose up -d web
Move the pin yourself rather than tracking upstream's floating
stable-postgres-tomcat or lts-postgres-tomcat tags. The PostgreSQL tag is
a separate decision: moving majors is a dump and restore, not a new image.
curl keeps returning 000 or 404 after up -d. Tomcat is still
deploying the application, which takes minutes on a first start. Follow
docker compose logs -f web; the line Using permanent directory [/usr/local/xwiki/data] means XWiki has found its volume and is initialising.
If the container restarts instead, check free -m: the heap alone is 1 GB.
The Distribution Wizard does not appear. It records its progress in the permanent directory, so a volume copied from an existing install skips it; a fresh volume runs it again. After an upgrade, it only appears to a logged-in administrator.
Links come out as http://. Either the proxy is not sending
X-Forwarded-Proto (this guide's Caddy does), or xwiki.home or
xwiki.url.protocol in xwiki.cfg names http. Check the value with the
grep from the domain step.
Links point at the old sslip.io name. xwiki.home is still set to it,
and a set xwiki.home wins over the requested host. Change the line with
sed -i 's|^xwiki.home=.*|xwiki.home=https://wiki.example.com/|' on
/usr/local/xwiki/data/xwiki.cfg inside the container and restart web.
Out of memory on a large wiki. The heap is fixed at 1 GB unless
JAVA_OPTS carries its own -Xmx. Add JAVA_OPTS: -Xmx2048m to the web
service on a server with the RAM to spare, and recreate it.
Port 8080 is reachable from the internet. The port line lost its
127.0.0.1: prefix. Fix the compose file, docker compose up -d, then check
that ss -tln | grep 8080 shows 127.0.0.1.
Certificate never issues. That is a Caddy and DNS problem rather than an
XWiki one: confirm the A record resolves (dig +short wiki.example.com) and
that ports 80 and 443 are reachable. Look at docker logs caddy.
You are done when https://wiki.example.com loads with a valid certificate,
the Distribution Wizard has finished and you can sign in as the administrator,
xwiki.home names your HTTPS address, port 8080 is bound only to
127.0.0.1, and a database dump plus the permanent-directory archive are
landing off the server.
From here, configure the mail server, pick the extensions your team needs from the extension manager, and set rights on the spaces you create. If the wiki turns out to be more platform than you need, Docmost and BookStack cover documentation without the application layer.