Deployment guide

Kutt on a VPS: a self-hosted URL shortener with an API

Deploy on a VPS Cloud →

Tutorial

Kutt on a VPS: a self-hosted URL shortener with an API

Self-hosting7 min read4 steps

A bit.ly link in a text-message campaign is a click that goes through a service you do not control, a statistic you do not own and a domain that is not yours. Kutt, an open-source URL shortener under the MIT licence, brings all of that back home: every short link carries your domain, every click stays in your database, and a REST API lets your tools shorten their own links. This guide installs Kutt with Docker Compose on a VPS, publishes it over HTTPS behind nginx, closes public sign-up, then shows how to create links through the API.

Contents· Why host your own URL shortener1/12
  1. 01Why host your own URL shortener
  2. 02What Kutt brings
  3. 03Kutt, Shlink and a public service
  4. 04Prerequisites
  5. 05Install Kutt with Docker Compose
  6. 06Create the administrator account straight away
  7. 07Create links through the API
  8. 08Working as a team
  9. 09Enable email
  10. 10Sizing the VPS
  11. 11Backup and updates
  12. 12One-click install from the ServOrbit Marketplace

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

KuttShlinkPublic service (bit.ly…)
Web interfaceBuilt inSeparate web clientYes
REST APIYes, one key per userYes, keys managed from the command lineYes, often paid
Your own domainYes, includedYes, includedPaid option
StatisticsIn your databaseIn your databaseWith the provider
Team accountsYes, created by the administratorNo, access by API keyDepends on the plan
StorageSQLite, PostgreSQL or MySQLSQLite, PostgreSQL, MySQL or SQL ServerManaged 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

  1. Prepare the directory and the secrets

    Create the application directory and a .env file holding the secret key (it signs sessions) and the short-link domain. Replace go.example.com with 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 .env
  2. Write 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: {}
  3. 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/health
  4. Publish Kutt over HTTPS

    Declare an nginx virtual host that forwards requests to the local port, then request the certificate with certbot. The X-Forwarded-For header 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 kutt

To 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 -d

One-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.

Your short links, on your domain

A ServOrbit Ubuntu 24.04 VPS with 512 MB of RAM is enough for Kutt. One-click install from the Marketplace, secret key generated for you, HTTPS on your domain or on the VPS's free subdomain.

Need help?

Browse our help center and FAQ, or reach our team — callback, WhatsApp or email. Support in French, English and Arabic.

Message us on WhatsAppopens in a new tab