Every homelab eventually runs into the same annoyance: you want to share a link to something running on your own network — a Grafana dashboard, a Jellyfin invite, a photo album in Immich — and the URL is an ugly mess of ports and query strings. Pasting https://10.0.20.15:8443/share?token=8f3a2b9c1e into a group chat is not exactly friendly. That's where Shlink comes in.

Shlink is a self-hosted URL shortener. Instead of trusting Bitly or TinyURL with your links (and their tracking), you run your own shortener on your own domain, on hardware you control. In this guide, you'll set one up inside an LXC container on Proxmox VE, using Docker to keep the install clean and easy to update.

What You Will Learn

By the end of this tutorial you'll have a working Shlink instance running in an unprivileged LXC container, reachable from your browser, with a web dashboard for creating and managing short links. Specifically, you'll:

  • Create a Debian-based LXC container on Proxmox VE with the right settings for running Docker inside it
  • Install Docker and Docker Compose inside that container
  • Deploy Shlink's backend and its web client using a docker-compose file
  • Generate an API key and connect the web client to it
  • Create your first short link and understand where the data lives

None of this requires previous Docker or Proxmox experience. If you've never created an LXC container before, that's fine — we'll walk through it step by step.

What Is This Feature?

Shlink is an open-source URL shortening server, written in PHP, that you run yourself. It has two main pieces: the Shlink server itself, which handles the actual redirects and exposes a REST API, and Shlink Web Client, a browser-based dashboard that talks to that API so you don't have to memorize curl commands to create a link.

An LXC container is Proxmox's version of a lightweight virtual machine. Unlike a full VM, it doesn't run its own kernel — it shares the host's kernel and only virtualizes the user-space environment. That makes it start in about a second and use a fraction of the RAM a VM would need for the same job. For something like Shlink, which doesn't need much CPU or memory, an LXC container is a much better fit than spinning up a whole VM.

Docker, meanwhile, is a way of packaging an application together with everything it needs to run — libraries, runtime, configuration — into a single unit called a container image. You don't have to install PHP, configure a web server, or fight with dependency versions. You just tell Docker to run the image, and it works the same way every time.

Why Would You Use It?

The obvious reason is vanity: a link like go.yourdomain.com/x7k2 looks a lot more trustworthy than a raw IP address with a port number bolted on. But there's more to it than looks.

Running your own shortener means nobody else sees where your links point or how often they get clicked. Public shorteners log every redirect, and some have shut down entirely over the years, breaking every link they ever generated. A self-hosted instance doesn't have that problem — your links keep working as long as your server does.

It's also genuinely useful for a homelab. You can hand a friend a short link instead of a screen-sharing session to explain how to reach your Plex server externally, or use short codes internally as quick bookmarks to services with long, hard-to-remember addresses. I use mine mostly for the second case — it's faster than digging through browser bookmarks for something I access twice a year.

Prerequisites

Before starting, make sure you have:

  • A working Proxmox VE host (this guide was written against the 8.x series, but the steps are the same on 9.x)
  • At least 1 GB of free RAM and 8 GB of free disk space on a storage pool the container can use
  • A Debian 12 LXC template downloaded, or access to download one
  • Basic comfort typing commands into a terminal — you don't need to know what they do yet, we'll explain each one

A domain name is optional. Shlink works fine on your local network with just an IP address and a port, though short links obviously look better with a real domain attached. If you want links reachable from outside your home, you'll also want a reverse proxy with a valid TLS certificate in front of it — that's outside the scope of this article, but any of the usual options work fine.

Step-by-Step Tutorial

1. Download the Debian 12 template

Log into the Proxmox web interface, click your node in the left-hand tree, then go to local (your-node) > CT Templates > Templates. Search for debian-12-standard and click Download.

If you'd rather do it from the shell, SSH into your Proxmox host and run:

pveam update
pveam available | grep debian-12
pveam download local debian-12-standard_12.7-1_amd64.tar.zst

The exact filename will include whatever the current point release is, so check the output of the available command rather than copying the version number above verbatim.

2. Create the container

Click Create CT in the top-right corner of the Proxmox interface. Work through the wizard with these settings:

  • Hostname: shlink
  • Password: something you'll remember — you'll need it to log in
  • Template: the debian-12-standard template you just downloaded
  • Disk: 8 GB is plenty
  • CPU: 2 cores
  • Memory: 1024 MB, with 512 MB of swap
  • Network: bridged to vmbr0, DHCP is fine unless you prefer a static address

Leave Unprivileged container checked. This is important — unprivileged containers are the safer default, and Docker runs inside them just fine as long as you flip one setting in the next step.

If you'd rather do this from the CLI instead of the wizard:

pct create 200 local:vztmpl/debian-12-standard_12.7-1_amd64.tar.zst \
  --hostname shlink \
  --cores 2 \
  --memory 1024 \
  --swap 512 \
  --net0 name=eth0,bridge=vmbr0,ip=dhcp \
  --rootfs local-lvm:8 \
  --unprivileged 1 \
  --features nesting=1,keyctl=1

Swap the container ID (200) for whatever's free on your system, and adjust the storage name (local-lvm) to match your own setup.

3. Enable nesting

This is the step people skip and then wonder why Docker won't start. Docker needs certain kernel features and the ability to create nested namespaces, which Proxmox blocks by default in unprivileged containers for security reasons. You have to explicitly allow it.

If you used the wizard, select your new container, go to Options > Features, click Edit, and tick both Nesting and Keyctl. If you used the CLI command above, you already set this with the --features nesting=1,keyctl=1 flag, so there's nothing more to do here.

4. Start the container and install Docker

Start the container and open a console session:

pct start 200
pct enter 200

Inside the container, update the package list and install the packages Docker's install script needs:

apt update && apt install -y curl ca-certificates

Then run Docker's official convenience script, which detects your distribution and sets up the right repository automatically:

curl -fsSL https://get.docker.com | sh

This takes a minute or two on a fresh 8 GB container. Once it finishes, confirm Docker is working and that the Compose plugin came along with it:

docker --version
docker compose version

Both commands should print a version number. If they don't, something went wrong with the install script — scroll back up through its output for an error before continuing.

5. Write the docker-compose file

Create a directory for Shlink's files and a compose file inside it:

mkdir -p /opt/shlink
cd /opt/shlink
nano docker-compose.yml

Paste in the following, adjusting DEFAULT_DOMAIN to whatever address you'll actually use to reach the container:

services:
  shlink:
    image: shlinkio/shlink:stable
    restart: unless-stopped
    ports:
      - "8080:8080"
    environment:
      - DEFAULT_DOMAIN=10.0.20.50:8080
      - IS_HTTPS_ENABLED=false
    volumes:
      - shlink_data:/etc/shlink/data

  shlink-web-client:
    image: shlinkio/shlink-web-client:stable
    restart: unless-stopped
    ports:
      - "8081:80"

volumes:
  shlink_data:

If you already have a domain pointed at this container through a reverse proxy, set DEFAULT_DOMAIN to that domain and IS_HTTPS_ENABLED to true instead. Getting this value wrong won't stop Shlink from running, but it will make every short link it generates point at the wrong address, which is a confusing thing to debug later.

Save the file (Ctrl+O, then Enter, then Ctrl+X in nano) and bring the stack up:

docker compose up -d

Docker will pull both images the first time — expect this to take a couple of minutes depending on your connection. Once it's done, check that both containers are actually running:

docker compose ps

You should see two entries, both showing a status of Up.

6. Generate an API key

The web client needs an API key to talk to the Shlink backend — it doesn't create one for you automatically. Generate one by running a command inside the running Shlink container:

docker exec -it shlink-shlink-1 shlink api-key:generate

Your container name might differ slightly depending on the folder name you used — check the exact name with docker ps if the command above says it can't find the container. Copy the key it prints; you won't be able to see it again through this command, though you can always generate a new one.

7. Connect the web client

Open a browser and go to http://<container-ip>:8081. You'll land on a setup screen asking for a server name, the Shlink server's URL, and the API key. Enter:

  • Name: anything you like, this is just a label
  • URL: http://<container-ip>:8080
  • API key: the key you copied in the previous step

Click connect, and you should land on an empty dashboard ready for its first short link.

Commands Explained

CommandWhat it does
pveam downloadDownloads a container template from Proxmox's online repository to local storage
pct createCreates a new LXC container from a template with the options you specify
pct enterOpens an interactive shell inside a running container, similar to SSH but without needing a network path
curl -fsSL ... | shDownloads Docker's install script and runs it immediately; -fsSL makes curl fail silently on errors, follow redirects, and suppress the progress bar
docker compose up -dReads the docker-compose.yml file, creates the containers it describes, and runs them in the background (-d for detached)
docker exec -it <name> <cmd>Runs a command inside an already-running container; -it keeps the session interactive so you can see the output

Common Errors

"Cannot connect to the Docker daemon at unix:///var/run/docker.sock" — this almost always means the Docker service isn't running, or nesting wasn't enabled before Docker was installed. Check with systemctl status docker inside the container. If nesting was off during install, enabling it afterward and restarting the container usually fixes it without needing to reinstall Docker.

Web client shows "Invalid API key" or won't connect at all — double check you copied the entire key with no trailing space, and that the server URL you entered matches the port Shlink is actually listening on. A stray https where you meant http causes this too, since the browser will refuse the connection outright.

Short links redirect to the wrong address — this is almost always the DEFAULT_DOMAIN variable. Shlink bakes this value into every link it generates at creation time, so changing it later won't fix links you've already made. You'll need to edit affected links individually through the web client or the API.

Troubleshooting

If the container itself won't start, check the task log in the Proxmox GUI first — it usually names the exact problem, whether that's insufficient storage space or a template that failed to download completely.

If Docker installs but containers refuse to start, run docker compose logs from inside /opt/shlink. Nine times out of ten the error is either a port already in use on the container (check with ss -tulpn) or a typo in the compose file's indentation — YAML is picky about spacing, and mixing tabs with spaces will break it silently.

If you can reach Shlink from the container's own console with curl localhost:8080 but not from another machine on your network, the problem is usually the container's network configuration rather than Shlink itself. Confirm the container actually got an IP with ip addr, and that it's on the same bridge and VLAN as the machine you're browsing from.

Best Practices

Don't expose port 8080 directly to the internet. Put a reverse proxy in front of it with a proper TLS certificate — short links redirecting through plain HTTP look sketchy to anyone paying attention, and browsers increasingly flag non-HTTPS sites anyway.

Take a Proxmox snapshot of the container before running any Shlink upgrade. Rolling back a bad update to a working snapshot takes seconds; recovering a corrupted SQLite database does not.

If you plan on generating more than a few hundred links a month, consider switching Shlink's storage backend to MySQL or PostgreSQL rather than the default SQLite file. It's a config change, not a reinstall, and it avoids the write-locking issues SQLite can run into under heavier concurrent use.

Give the container a static IP or a DHCP reservation once you're happy with the setup. Reverse proxies and bookmarks both break quietly when a container's address changes underneath them.

Frequently Asked Questions

Can I run Shlink without Docker?

Yes, Shlink can be installed directly from a distributable .phar package or built from source, but it also means managing PHP versions and dependencies yourself. Docker is simpler for almost everyone.

Does Shlink support custom domains?

Yes. You can even run multiple domains from a single Shlink instance and choose which one a given link uses when you create it.

Is Shlink free?

Completely. It's open source under the MIT license, with no paid tier or feature gate.

Can more than one person use the same instance?

The web client itself doesn't have user accounts — anyone with the API key can manage links. If you need per-user access control, you'd put it behind an authentication layer like Authentik or basic auth on the reverse proxy.

Do I need a public domain to use this at home?

No. It works fine on your local network with just the container's IP address and a port number. A domain just makes the links look nicer and makes them reachable from outside your network if you set that up separately.

What database does Shlink use by default?

SQLite, stored in the data volume you defined in the compose file. That's fine for personal use; switch to MySQL or PostgreSQL if you expect heavier traffic.

Conclusion

You've now got a working, self-hosted URL shortener running in a lightweight LXC container — no VM overhead, no third-party tracking, and full control over how long your links stick around. The whole setup, from downloading the template to your first short link, takes about fifteen minutes once you've done it once.

From here, the natural next steps are putting a reverse proxy in front of it with a real domain and TLS certificate, and taking a backup job on the container so an upgrade gone wrong never costs you your link history. Both are quick additions once Shlink itself is running the way you want.