Why host your own URL shortener
A public shortener decides for you: it can change its terms, cap the number of links, insert an interstitial page or shut down, and your printed links stop working. Its statistics belong to it, and its shared domain is regularly blocked by anti-phishing filters because other people abuse it.
With Kutt on your VPS, the link go.your-domain.com/offer is yours for good: you can change its destination without reprinting the poster, see who clicked and from where, and no third party sees your campaigns. For an agency, it is also a service to offer every client, on their own domain.
What Kutt brings
- Links on your domain — every short link carries your brand and stays valid even if you change tools later.
- Custom aliases —
/launch,/webinar, or a generated six-character code without ambiguous characters. - Password and expiry — a protected link for a client document, a link that stops redirecting when an offer ends.
- Private statistics — visits over time, browsers, operating systems, countries and referrers, visible only to the owner.
- REST API — one key per user to create, update and delete links from your scripts and tools.
- Administration — team account creation, review of every link, banning an abusive link or domain.
Kutt, Shlink and a public service
Scroll the table
| Kutt | Shlink | Public service (bit.ly…) | |
|---|---|---|---|
| Web interface | Built in | Separate web client | Yes |
| REST API | Yes, one key per user | Yes, keys managed from the command line | Yes, often paid |
| Your own domain | Yes, included | Yes, included | Paid option |
| Statistics | In your database | In your database | With the provider |
| Team accounts | Yes, created by the administrator | No, access by API key | Depends on the plan |
| Storage | SQLite, PostgreSQL or MySQL | SQLite, PostgreSQL, MySQL or SQL Server | Managed by the provider |
Prerequisites
An Ubuntu 24.04 VPS with 512 MB of RAM and 5 GB of disk is enough: Kutt runs in a single Node.js container and uses about 200 MB of memory. You need Docker with the Compose plugin, nginx and certbot for HTTPS, and a DNS name pointing to the VPS.
Choose that name carefully, because it forms every link: the shorter it is, the shorter your links. A subdomain such as go.your-domain.com or a small dedicated domain works very well. Kutt writes it into every link it creates, so changing it later does not rewrite links already shared.
Install Kutt with Docker Compose
Prepare the directory and the secrets
Create the application directory and a
.envfile holding the secret key (it signs sessions) and the short-link domain. Replacego.example.comwith your own name.mkdir -p /opt/kutt && cd /opt/kutt printf 'SECRET_KEY=%s\nDOMAIN=go.example.com\n' "$(openssl rand -hex 32)" > .env chmod 600 .envWrite the compose file
Create
compose.yaml. The port is published on the loopback interface only: nginx alone reaches it. Public sign-up and anonymous links stay closed, and the SQLite database lives in a named volume.services: kutt: image: kutt/kutt:v3.2.6 restart: unless-stopped ports: - "127.0.0.1:8107:3000" environment: JWT_SECRET: ${SECRET_KEY:?SECRET_KEY requis} DEFAULT_DOMAIN: ${DOMAIN:?DOMAIN requis} DB_CLIENT: better-sqlite3 DB_FILENAME: /var/lib/kutt/data.sqlite TRUST_PROXY: "true" DISALLOW_REGISTRATION: "true" DISALLOW_ANONYMOUS_LINKS: "true" volumes: - kutt-data:/var/lib/kutt - kutt-custom:/kutt/custom volumes: kutt-data: {} kutt-custom: {}Start and check
Start the stack. Kutt applies its migrations at start-up, then the health probe must answer 200.
docker compose up -d docker compose ps curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8107/api/v2/healthPublish Kutt over HTTPS
Declare an nginx virtual host that forwards requests to the local port, then request the certificate with certbot. The
X-Forwarded-Forheader lets Kutt record the real visitor address (TRUST_PROXY).server { server_name go.example.com; location / { proxy_pass http://127.0.0.1:8107; proxy_set_header Host $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } }certbot --nginx -d go.example.com
Create the administrator account straight away
Kutt creates no account in advance: as long as no user exists, the home page redirects to the create admin form, and the first visitor becomes administrator. Open your domain as soon as HTTPS works and create your account with a strong password. The form then refuses any further creation.
Create links through the API
In your account settings, generate an API key, then send it in the X-API-KEY header. The JSON body accepts target (the long address) and, optionally, customurl, password, description and expire_in (a duration such as 7 days).
curl -s -X POST https://go.example.com/api/v2/links \
-H "X-API-KEY: $KUTT_API_KEY" \
-H 'Content-Type: application/json' \
-d '{"target": "https://example.com/une/adresse/tres/longue", "customurl": "promo", "expire_in": "7 days"}'The response contains the link field, the full short link. The same endpoint with GET lists your links with their visit count, and /api/v2/links/<id>/stats returns the detailed statistics of one link. That is what lets you wire Kutt into a CRM, an invoicing script or an email campaign: every outgoing link is shortened and measured without manual work.
Working as a team
Sign-up is closed: the administrator creates accounts from the admin page. Each member has their own links, private statistics and API key, revoked by generating a new one. The administrator sees every link on the instance and can ban an abusive one; this matters, because a shortener carrying your domain puts its reputation at stake.
Kutt also accepts additional domains, added in the settings. Each must point to the VPS and have its own certificate: start with a single domain, then add the others one by one.
Enable email
Kutt is installed with email disabled: no password reset by email and no address verification. To enable them, add MAIL_ENABLED: "true" and the MAIL_HOST, MAIL_PORT, MAIL_USER, MAIL_PASSWORD and MAIL_FROM variables of your SMTP server to the container environment, then restart with docker compose up -d. Keep the administrator password safe in the meantime: without email, losing it has to be fixed in the database.
Sizing the VPS
For a team and a few thousand links, 512 MB of RAM and one vCPU are plenty: a redirect is a simple database read. The SQLite database grows with recorded visits, in the order of a few hundred bytes per click. Beyond hundreds of thousands of clicks a day, move to PostgreSQL (DB_CLIENT: pg) and enable Redis (REDIS_ENABLED) for caching.
Backup and updates
All the data lives in the kutt-data volume: the SQLite database holds accounts, links and statistics. Stop the container for a moment to get a consistent copy, then archive the volume.
docker compose stop kutt
docker run --rm -v kutt_kutt-data:/data -v "$PWD":/backup alpine \
tar czf /backup/kutt-$(date +%F).tar.gz -C /data .
docker compose start kuttTo update, read the project's release notes, change the image tag, then pull the new image: migrations run at start-up. Always take a backup right before.
# changer l'étiquette de l'image dans compose.yaml, puis :
docker compose pull && docker compose up -dOne-click install from the ServOrbit Marketplace
On a ServOrbit VPS, Kutt installs from the Marketplace in your client area: choose the address — a short subdomain of one of your domains or the VPS's free subdomain, HTTPS included — and click Install. The script deploys the container, generates the secret key, sets the link domain and configures nginx. All that is left is to open your domain to create the administrator account, then your first API key.