Skip to content

Running a local server ​

The only way to know what your .htaccess actually does is to serve it with a real Apache. This page sets one up on your machine, configured the way the live service is configured, so ids/my-project/ answers at http://localhost:8080/my-project/.

This page has had limited testing

All four setups have been run end to end against this repository's own ids/ directory, and they agree with each other: the identifier root, the bare no-slash form, sub-paths, Accept negotiation, the site root, a missing identifier and a request for an .htaccess file all behave as described below.

The unprivileged setup is the newest and least exercised of the four, and one option within it has not been run at all: pointing it at an already-installed Apache with W3ID_ALLOW_SYSTEM_HTTPD. What has been tested there is the Apache it builds for itself.

That is not much exposure, and the setups touch Docker versions, Apache builds and host networking that vary. If something here does not work for you, or works only after you change it, please open an issue rather than working around it quietly — that feedback is what this page needs most.

You do not need this page for a simple redirect. Read your rules carefully, run the checks, and confirm the target resolves. Come here when a rule is not matching and you cannot see why, when you are doing content negotiation, or when you want to be sure before asking a volunteer to review it.

What it emulates ​

The local server mirrors the live single-server configuration: the same Apache modules, the same AllowOverride, the same document root layout, the same Options. Two things are deliberately different.

  • No redirect to HTTPS. The live server bounces all plain HTTP to https://w3id.org/. Locally that would send every test request to the real service, so you would be testing the deployed rules instead of your own.
  • No TLS. Plain HTTP is enough, because rewrite rules do not behave differently by scheme. See HTTPS if you need it anyway.

The module list is restricted on purpose, to exactly what the live server runs. If you use a directive from some other Apache module, you get a 500 here rather than a working local test and a broken deployment. Adding a LoadModule line to make your own rules work is the wrong fix — production will not have it.

With Docker ​

The recommended path. From the repository root:

sh
cd tools/server
docker compose up

Leave it running. In another terminal:

sh
curl -sI http://localhost:8080/my-project/

To stop it, press Ctrl-C, or run docker compose down from tools/server/.

Apache's logs go to the container's output, so the terminal running docker compose up shows every request and every error. That log is the only thing that names the file and line when an .htaccess fails to parse.

sh
docker compose logs -f w3id      # if you started it with -d

The identifier tree is mounted read-only. The server cannot modify the files you are about to commit.

Without building an image ​

If you would rather not wait for a build, the stock Apache image can be pointed at the tree directly. From the repository root:

sh
docker run --rm -p 8080:80 \
  -v "$PWD/ids:/usr/local/apache2/htdocs:ro" \
  -v "$PWD/tools/server/conf:/w3id/conf:ro" \
  httpd:2.4 \
  httpd -D FOREGROUND -c "Include /w3id/conf/docker-direct.conf"

This is the less faithful path. Because the image's own configuration runs first, it does not give you the restricted module set, so an .htaccess that works this way can still fail in production. Use it to get moving; use docker compose up when the answer matters.

With a native Apache ​

Closest to how the service actually runs. On Debian or Ubuntu:

sh
sudo apt install apache2
sudo a2enmod rewrite headers setenvif env mime dir alias autoindex negotiation \
  deflate filter reqtimeout access_compat

Copy the site configuration into place and edit one line in it — the Define W3ID_REPO at the top must be the absolute path of your checkout:

sh
sudo cp tools/server/conf/w3id-site.conf /etc/apache2/sites-available/
sudo editor /etc/apache2/sites-available/w3id-site.conf
sudo a2ensite w3id-site
sudo apache2ctl configtest
sudo systemctl reload apache2

configtest before reloading, every time. A syntax error in the site file stops Apache from starting at all, rather than failing one directory.

Apache must be able to read your checkout. If it is under /home, either move it somewhere like /srv/w3id.org or grant the www-data user traversal — a 403 on every URL usually means it cannot get through a parent directory.

Logs land in /var/log/apache2/w3id-local-error.log.

Without Docker or root ​

For a machine where you cannot install anything — a container, a locked-down workstation, a shared host — or where you would simply rather not. This builds an Apache into your own cache directory and runs it as you. Nothing is installed system-wide and no step uses sudo.

Build it once. This downloads Apache, APR and PCRE2, checks them against checksums pinned in the script, and compiles them. Expect a few minutes, most of it Apache itself:

sh
tools/server/bin/build-httpd

Then start it. It serves the ids/ directory of the checkout the script itself lives in, so if you have several worktrees each one serves its own tree:

sh
tools/server/bin/run-server

That stays in the foreground until you press Ctrl-C. To leave one running instead:

sh
tools/server/bin/run-server start
tools/server/bin/run-server status
tools/server/bin/run-server stop

status prints where the error and access logs are. run-server test checks the configuration without starting anything, and prints the full httpd command it would run, so nothing about the setup is hidden from you.

The port is 8080, the same one every example on this page uses. W3ID_PORT changes it, and W3ID_BASE is how you tell resolve-identifier:

sh
W3ID_PORT=8081 tools/server/bin/run-server start
W3ID_BASE=http://localhost:8081 tools/server/bin/resolve-identifier my-project

Two limits are worth knowing before you choose this setup. The build has no mod_ssl, so HTTPS is not available here — use Docker or a native Apache if you need it. And if you already have Apache installed and would rather not build one, W3ID_ALLOW_SYSTEM_HTTPD=1 makes run-server use it, at a cost: a distribution Apache links some modules into the binary, where no configuration can unload them, so a rule relying on a module the live server does not have can pass locally and still fail in production. That is the one thing this setup is otherwise good at catching.

Checking a redirect ​

There is a script for the requests worth making:

sh
tools/server/bin/resolve-identifier my-project

It reports the status and Location for the identifier root, the bare form with no trailing slash, and each of the Accept headers that matter, then follows the whole chain to see where it ends up.

Give it sub-paths as extra arguments — including the ones you did not mean to match, since a rule catching a request by accident is exactly what this finds:

sh
tools/server/bin/resolve-identifier my-project vocab vocab/Thing 1.2.0

For a first run, examples is a real identifier already in the tree, so it works before you have written anything of your own:

sh
tools/server/bin/resolve-identifier examples

Set W3ID_BASE if you changed the port:

sh
W3ID_BASE=http://localhost:9000 tools/server/bin/resolve-identifier my-project

Everything the script does, you can do by hand — see What to check in the testing guide for the individual curl invocations.

Reading the result ​

A 302 or 303 with a location: pointing where you intended is success. Check the URL character by character; a redirect to the wrong place is still a redirect.

A 301 on the bare form is normal. Because your identifier is a real directory, Apache redirects /my-project to /my-project/ before any of your rules run, and your rule fires on the second request. Do not try to remove that hop — the directives that appear to fix it cause a 403 instead. See htaccess/no-options-directive.

A 500 is a syntax error, and it affects every URL under that directory, not just the rule you got wrong. The server log names the file and the line. The usual cause is a space inside the flag brackets — htaccess/no-flag-whitespace.

A 404, or your rule not firing at all, usually means the pattern never matched. The pattern is relative to the directory, so a leading slash or a repeated directory name silently matches nothing — htaccess/pattern-relative-to-dir. An unescaped dot is the other common cause — htaccess/escape-literal-dots.

A 406 means a content-negotiation catch-all is matching real requests — htaccess/no-406-fallback.

A 403 on every URL is a filesystem permission problem on the native setup, not a rule problem.

When a rule will not match and you have run out of ideas, turn on mod_rewrite's tracing and watch it decide:

apache
LogLevel warn rewrite:trace3

Add that to the virtual host — not to your .htaccess, where it does not belong and would not be accepted. It logs every rewrite Apache attempts, which is verbose and worth it.

HTTPS and certificates ​

You almost certainly do not need this. Rewrite rules do not behave differently over HTTPS, so http://localhost:8080 tests the same rules. The one thing plain HTTP cannot exercise is a rule that inspects %{HTTPS} or %{SERVER_PORT}, which is rare and discouraged.

If you do need it, generate a certificate for localhost. Run this from the tools/server directory, which has a tls/ entry in .gitignore — so a private key generated there cannot be committed by accident:

sh
cd tools/server
mkdir -p tls
openssl req -x509 -newkey rsa:2048 -nodes -days 365 \
  -keyout tls/localhost.key \
  -out tls/localhost.crt \
  -subj '/CN=localhost' \
  -addext 'subjectAltName=DNS:localhost,IP:127.0.0.1'

Then enable mod_ssl and point a <VirtualHost *:8443> at those two files with SSLEngine on, including the same conf/w3id.conf as the HTTP host.

Be aware of what a self-signed certificate does and does not get you:

  • curl needs -k, otherwise it refuses to connect. That also disables the check you might have been hoping to make.
  • Browsers show a full-page interstitial, and there is no way to make a self-signed certificate simply look valid. Getting a green padlock means creating a local certificate authority, adding it to your operating system's trust store, and issuing a certificate from it — mkcert automates this. Trusting your own CA has real consequences for your machine; understand them before you do it.
  • Do not test HTTPS in a browser that has visited the real w3id.org. The live service sends HSTS, so the browser may refuse to let you click through a certificate warning for that hostname at all. Use localhost, which is what the certificate above is for.

When it will not start ​

Address already in use — something already holds port 8080. Change the host port in tools/server/compose.yaml, or, on the native setup, check whether /etc/apache2/ports.conf also listens on 8080; the site file adds its own Listen.

Every URL returns 404, including / — the document root is not pointing at ids/. Under Docker, check you ran the command from the repository root so $PWD/ids resolved. Under Apache, check Define W3ID_REPO.

Your rules are completely ignored, but files are served — mod_rewrite is not loaded, or AllowOverride is not All. This is the failure that looks like success: Apache serves the tree and silently discards every rule in it.

Changes to an .htaccess seem not to take effect — they should apply on the next request, with no reload. If they do not, you are probably editing a different file than the one being served; check the path the container has mounted.

What this cannot tell you ​

A local server proves what Apache does with your rules. It says nothing about whether the answer is right.

It does not know where your identifier is supposed to point, and it does not check that the target is alive — the redirect target is on the public internet and is somebody else's server. Confirm that separately:

sh
curl -sIL https://example.org/vocab.ttl | grep -iE '^(HTTP|content-type)'

And when the pull request is merged, check it for real, because the deployed configuration is the only one that counts:

sh
curl -sIL https://w3id.org/my-project/ | grep -iE '^(HTTP|location)'