What YOURLS asks of the machine
The requirements page names a web server, PHP and a database, and recommends HTTPS; the PHP extensions behind them come from composer.json in the repository.
| Component | Requirement | Where it is stated |
|---|---|---|
| Web server | Apache 2.4+ with mod_rewrite; an nginx server block is documented too | Requirements; server configuration page |
| PHP | 8.1 or later, with the cURL extension if you use the API | Requirements; install.php refuses older PHP |
| PHP extensions | PDO with pdo_mysql, DOM, filter, hash, PCRE | composer.json |
| Database | MySQL 5.5+ or MariaDB 10.0+ | Requirements |
The docs publish no RAM or core figure. The public-shortening page instead prices a redirect: next to no CPU, and two read plus two write queries against the database. Our conclusion from that: size for database writes, which means local SSD or NVMe under MariaDB, not core count. Each logged click stores the visitor's IP address, referrer, user agent and country code (per yourls_log_redirect in the source); YOURLS_NOSTATS set to true stops redirect logging and the stats with it. For site analytics beyond click counts, see Matomo on a dedicated server.
Which machines fit
A shortener plus its database is one service on one box, which is how our dedicated server range describes the Moonshot cartridges. The single-socket line is described as a web or application server with its database on the same machine, and suits an instance sharing the box with other sites. Each line rents you the entire physical machine, root included, on an unmetered port, and the IPv4 address the short domain points at is that machine's own.
- Console: every racked single-, dual- and quad-socket server has a full out-of-band console (HPE iLO, Dell iDRAC or a Supermicro BMC), as do the m510 and m710x cartridges; cartridges without one get remote power cycling but no screen.
- IPv6 is in no default build. It is added at checkout at no charge; add it if the short domain gets an AAAA record.
- Ready (racked) servers are live about 30 minutes once payment is approved; a built-to-order line takes 4 to 24 hours.
- Basic management is included: a 12-hour response and 24-hour hardware replacement.
The X-Gene ARM cartridge is out: it boots Ubuntu 16.04 and 18.04 and nothing else, and every command here assumes Ubuntu 24.04 LTS. Pick a line whose system list includes 24.04. For other Linux images, see Linux dedicated servers.
Installing nginx, PHP-FPM, MariaDB and the release
Order matters. DNS goes first: the short domain's A record, plus an AAAA if you took IPv6, must resolve before anything else, because Let's Encrypt checks it. The firewall comes next: IPV6=yes is written before the first ufw allow so every rule gets its IPv6 copy, SSH is allowed, ufw is switched on, and only then are the web ports opened. The certificate is issued before any nginx file names its path. Only then come the database, the files and config.php.
# as root on Ubuntu 24.04 LTS, once s.example.com resolves to this server
sed -i 's/^IPV6=.*/IPV6=yes/' /etc/default/ufw
ufw allow OpenSSH
ufw --force enable
ufw allow 80,443/tcp
apt update && apt install -y nginx mariadb-server php-fpm php-mysql php-curl php-xml certbot
certbot certonly --webroot -w /var/www/html -d s.example.com -m you@example.com \
--agree-tos --no-eff-email --non-interactive --deploy-hook "systemctl reload nginx"
DBPASS=$(openssl rand -hex 24)
mariadb <<SQL
CREATE DATABASE yourls CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'yourls'@'localhost' IDENTIFIED BY '$DBPASS';
GRANT ALL PRIVILEGES ON yourls.* TO 'yourls'@'localhost';
SQL
V=1.10.6 # the newest tag on github.com/YOURLS/YOURLS/releases
mkdir -p /var/www/yourls
curl -fsSL https://github.com/YOURLS/YOURLS/archive/refs/tags/$V.tar.gz | tar -xz --strip-components=1 -C /var/www/yourls
cd /var/www/yourls/user && cp config-sample.php config.php
chown root:www-data config.php && chmod 660 config.php
sed -i "s|your db user name|yourls|; s|your db password|$DBPASS|; s|https://your-own-domain-here.com|https://s.example.com|; s|modify this text with something random|$(openssl rand -hex 32)|" config.php
- The certbot flags answer the three questions it would otherwise ask on a first run (an email address, the subscriber terms, sharing that email with the EFF).
--agree-tosrecords that you accept Let's Encrypt's terms, so read them first. The deploy hook reloads nginx after each renewal. - The database password goes to MariaDB on standard input, so it never appears in
ps; the final sed does carry it, and the newYOURLS_COOKIEKEY, as arguments for a moment. On a box where only you log in, that is acceptable; on a shared one, drop those two substitutions and type both values into config.php by hand. - Then edit
config.phpwith nano and replace the sample'username' => 'password',line with your login and a long password. LeaveYOURLS_PRIVATEat true: that is what puts the admin area behind the login. YOURLS_SITEis lowercase with no trailing slash; never mix www and non-www.config.phpis group-writable for now because YOURLS rewrites the password as a hash; the next section closes it.
Running install.php before the domain serves YOURLS
The installer creates the tables from your config and asks for no login (admin/install.php has no auth check), so ours runs on a server block bound to 127.0.0.1, reachable only through your own SSH login. The second file is the docs' nginx block with our additions: the HTTP redirect, the certificate paths and index index.php, which makes /admin/ load instead of answering 403. Check the socket name with ls /run/php/.
# file: /etc/nginx/sites-available/yourls-install (ours: loopback only, removed after the install)
server {
listen 127.0.0.1:8080;
root /var/www/yourls;
index index.php;
location / { try_files $uri $uri/ /yourls-loader.php$is_args$args; }
location ~ \.php$ { include fastcgi.conf; fastcgi_pass unix:/run/php/php8.3-fpm.sock; }
}
# file: /etc/nginx/sites-available/yourls
server {
listen 80;
listen [::]:80;
server_name s.example.com;
location /.well-known/acme-challenge/ { root /var/www/html; }
location / { return 301 https://$host$request_uri; }
}
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name s.example.com;
ssl_certificate /etc/letsencrypt/live/s.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/s.example.com/privkey.pem;
root /var/www/yourls;
index index.php;
location / { try_files $uri $uri/ /yourls-loader.php$is_args$args; }
location ~ \.php$ {
include fastcgi.conf;
fastcgi_index index.php;
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
}
}
- Enable the loopback block only:
ln -s /etc/nginx/sites-available/yourls-install /etc/nginx/sites-enabled/, thennginx -t && systemctl reload nginx. - From your workstation, forward a port through your own login:
ssh -L 9080:127.0.0.1:8080 -N you@s.example.com. - Browse straight to
http://localhost:9080/admin/install.phpand press Install YOURLS. Plain/admin/would bounce you to the https address: includes/Config/Init.php sends a 307 toyourls_admin_url('install.php'), and includes/functions-links.php builds that URL fromYOURLS_SITE. A warning that.htaccesscould not be written is expected on nginx. Ignore the link to the admin page for now. - Go public:
rm /etc/nginx/sites-enabled/yourls-install /etc/nginx/sites-enabled/default,ln -s /etc/nginx/sites-available/yourls /etc/nginx/sites-enabled/, thennginx -t && systemctl reload nginx. - Sign in at
https://s.example.com/admin/. In the source (includes/auth.php) the clear-text password is rewritten as aphpass:hash after the first successful sign-in. Confirm withgrep -c phpass: /var/www/yourls/user/config.php, then lock the file withchmod 440 /var/www/yourls/user/config.php, one of the modes the docs suggest.
The API and the signature token
The API is yourls-api.php at the site root and takes its parameters by GET or POST. Its built-in actions: shorturl (optional keyword and title), expand, url-stats, stats (filter top, bottom, rand or last, plus a limit), db-stats and version. Output is json, jsonp or xml, and simple for shorturl, expand and version.
- Authentication is a username and password, or the signature token: a 32-character secret on the admin Tools page that works for the API only, never for signing in. A public install has none.
- A POST keeps the token out of nginx's access log:
curl -s https://s.example.com/yourls-api.php -d signature=TOKEN -d action=shorturl -d format=json --data-urlencode url=https://example.com/a/long/path. Shell history and, briefly,psstill see it. - For an expiring credential, send
timestamp,hash=sha256and, assignature, the sha256 of the timestamp followed by the token. It is accepted withinYOURLS_NONCE_LIFEseconds of its timestamp, 43,200 (12 hours) by default peryourls_get_nonce_lifein includes/functions-auth.php; the docs page also mentions one hour, which the code does not bear out. - In the source (
yourls_auth_signature) the token is derived from the username andYOURLS_COOKIEKEY, so a password change keeps it. The docs' reset is a fresh cookie key. That also ends every login session, because functions-auth.php derives the login cookie (yourls_cookie_value) from the same key.
Plugins, privacy and a public front page
- A plugin is a
plugin.phpin its own folder underuser/plugins, activated on the admin's Manage Plugins page. The release bundles a few core plugins (hyphens in URLs, random short URLs, samples); others are listed in the YOURLS/awesome repository, which the docs say vouches for nothing. If pages break, their first step is to disable every plugin and re-enable them one at a time. - A public shortening page ships as
sample-public-front-page.txt. The docs warn it draws spam and point to anti-spam plugins and an allowed-protocols list,$yourls_allowedprotocols.
Backups, restore and upgrades
Everything that is yours is the database, user/config.php and user/plugins; the rest comes back from the release archive. Our job below dumps the database as root (root reaches MariaDB through its Unix socket on Ubuntu) and tars the two paths. Cron's default /bin/sh has no pipefail, so a failed dump piped into gzip would pass as success; the SHELL line plus set -o pipefail stop the job there instead. Save it with a trailing newline. The archive holds config.php with its database password, so treat it as a secret, and copy both files off the machine yourself: a backup on the same disks fails with them.
# file: /etc/cron.d/yourls-backup (our schedule: 02:40 daily; copies older than 14 days are deleted)
SHELL=/bin/bash
40 2 * * * root set -o pipefail; umask 077; mkdir -p -m 700 /var/backups/yourls && mariadb-dump --single-transaction yourls | gzip > /var/backups/yourls/db-$(date +\%F).sql.gz && tar -czf /var/backups/yourls/files-$(date +\%F).tar.gz -C /var/www/yourls user/config.php user/plugins && find /var/backups/yourls -type f -mtime +14 -delete
- Check the pair before changing anything, in one shell: `D=2026-10-20; sudo test -s /var/backups/yourls/db-$D.sql.gz && sudo gzip -t /var/backups/yourls/db-$D.sql.gz && sudo tar -tzf /var/backups/yourls/files-$D.tar.gz > /dev/null && sudo sh -c "zcat /var/backups/yourls/db-$D.sql.gz | tail -n 1" | grep -q '^-- Dump completed' && echo pair-ok`. Continue only on pair-ok.
- Know what that proves: both files exist, are not empty and decompress or list without error, and the dump ends with the completion comment mariadb-dump writes when it finishes. It does not prove the data loads cleanly; only a trial restore on another machine does.
- Load the database:
sudo sh -c "zcat /var/backups/yourls/db-$D.sql.gz | mariadb yourls". The dump drops and recreates each table. - Put the files back:
sudo tar -xzf /var/backups/yourls/files-$D.tar.gz -C /var/www/yourls. Extracted as root, config.php keeps the owner and mode it was saved with.
Upgrades follow the docs: the admin area flags a new version; back up the database, then extract the new release over the old one (V set to the new tag, then the same curl ... | tar line as the install). The archive has no config.php, so yours is untouched; it does carry user/plugins with the bundled core plugins, which are replaced, while plugin folders you added stay. Then open /admin/; when the schema has changed, includes/Config/Init.php redirects you to the upgrade page first.
The official Docker image
yourls on Docker Hub is a Docker Official Image whose maintainer is the YOURLS project itself (the YOURLS/containers repository), in apache (latest), fpm and fpm-alpine variants. It builds config.php from environment variables: YOURLS_SITE, YOURLS_USER and YOURLS_PASS are required, and each has a _FILE twin that reads the value from a file such as a Docker secret, keeping it out of docker inspect.
The Hub's compose example maps 8080:8080. A port given without an address listens on every host address, 0.0.0.0 and [::]. Docker's NAT rules for published ports bypass ufw on IPv4; on IPv6 they bypass it only with Docker's own IPv6 networking enabled, and without it docker-proxy, a process ufw filters, answers on [::]. Map 127.0.0.1:8080:8080 and proxy to it from host nginx or Nginx Proxy Manager; on Docker Engine 28+ that binding answers the host only. Two more points from the sources: config-container.php in YOURLS/containers takes the cookie key from YOURLS_COOKIEKEY and otherwise keeps the sample's placeholder, so set it; and behind any proxy, clicks are logged with the proxy's address unless a plugin trusts it through the get_ip_trusted_proxies filter.
Licence: MIT
YOURLS is MIT-licensed (LICENSE in the repository, and composer.json), copyright The YOURLS Contributors. Our general reading, not legal advice: you may run, change and sell it, privately or commercially, without publishing your changes. Anyone who passes copies on, modified or not, owes the recipients the copyright notice and the permission text with them. The Docker image also carries software from its base system under other licences, which the image page leaves the user to check.
Questions
Can several people share one YOURLS install?
Yes, as separate logins: $yourls_user_passwords takes as many username and password pairs as you list, and every clear-text entry is hashed at the next successful sign-in by anyone. Core YOURLS has no roles, so every login manages every link. Each user also gets their own API signature token.
Does YOURLS need Apache, or will nginx do?
The requirements page names Apache 2.4 with mod_rewrite, but the server configuration page publishes an nginx server block, and that is what this page uses. The one rule that matters on either server: requests for paths that are not real files go to yourls-loader.php. Without it, short links return 404.
Can I switch to mixed-case short codes later?
YOURLS_URL_CONVERT sets 36 (lowercase) or 62 (mixed case). The docs advise against changing it once links exist, so decide before the first link is made.