Self-hosted service
LinkStackOrg/linkstack-docker avatar
LinkStackOrg/linkstack-docker

linkstack-docker: a self-hosted link page in one container

A simple to set up docker variant of LinkStack. LinkStack is a highly customizable link sharing platform with an intuitive, easy to use user interface.

526 stars72 forksShellAGPL-3.0

At a glance

What is it?
LinkStack is a link-in-bio page that you host yourself, and the container edition is the part that makes it a five-minute project rather than an afternoon of configuration. The whole application is a web server, a language runtime and a database in one image, everything is configured by environment variable, and the only thing a user has to get right is a volume. The interesting details are in what the image does not include and in the two different ways the documentation tells you to deploy it.
Who is it for?
The container image fits somebody who wants a link page on their own server with their own domain and no third party between the visitor and the links. It does not fit somebody who wants a hosted service with a managed database, because the whole point of the image is that the data lives in a volume you control.
Can I use it commercially?
Yes, with strict conditions. AGPL-3.0 is a network copyleft licence: if people use a modified version over a network, for example as a hosted service, you must offer them its source code under the same licence.
Is it still maintained?
Activity is slowing. The repository last received commits 6 months ago.
What is it written in?
Mainly Shell, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 20, 2026, and from our analysis. They are not legal advice.

Editorial analysis

What the image is

This is the official container edition of LinkStack, and the readme's claim is that the image contains everything you need to run it, and that the container version retains all the features and the customisation options of the original.

That last clause is the important one for anybody who has used the original. The usual failure mode of a packaged edition is that it works but has lost a feature, and the guarantee here is explicit.

The application itself is a link sharing platform: a personal profile page where all your links are clickable, which the readme pitches against the social platforms that allow only one link. And the pitch of the container edition is that the deployment is as simple as pulling and deploying.

So what is in the image is worth naming, because it explains the size and the behaviour. The base is Alpine Linux, a distribution designed to be small, simple and secure. The web server is Apache, and the readme names it as free and open source and cross-platform, which is the sort of description a project writes when the choice is a platform requirement rather than a preference. The language runtime is PHP 8.3, and the version is stated with a reason: high compatibility and performance.

Those three choices together give you a predictable image. A small base means a small download and a small attack surface, a mature web server means the virtual host configuration is boring and well understood, and a current language runtime means the application runs on something that is still supported.

And the licence is the copyleft one with the network clause, which is the correct choice for a project whose whole point is that the person deploying it can see the source and change it. The repository also carries a security policy, which is the right companion to a licence that asks people to run the thing themselves.

Pulling, and the four architectures

The installation is one command.

shell
docker pull linkstackorg/linkstack

And there is an alternative mirror on a second registry, which is the other public one.

shell
docker pull ghcr.io/linkstackorg/linkstack

Publishing the same image to two registries is a small courtesy with real consequences. The first is availability: if one registry is unreachable or rate limited, the other works. The second is trust: an organisation that blocks one registry can still pull from the other. The third is that a mirror is the standard way to make sure a deployment does not depend on a single company's infrastructure.

The architecture list is the other thing in this section, and it is more interesting than a typical one because it includes two older 32-bit ARM variants alongside the two obvious ones. There is the standard 64-bit desktop architecture, the 64-bit ARM architecture, and the 32-bit ARM versions six and seven.

Those two older ones are not an accident. Version six and version seven of that architecture are what single board computers and older embedded devices run, and a self-hosted application for a personal site is exactly the kind of thing that ends up on a cheap machine at home. Many container projects publish only the two modern architectures and quietly exclude the boards people actually have.

The cost of covering them is real, because every architecture needs its own build in the pipeline, and the more architectures you publish the slower the build matrix gets. So the choice is a statement about who the project is for, and for a self-hosted personal application the answer is that the hardware in somebody's cupboard counts.

Configuration by environment, and what each variable maps to

The optional environment variables are seven, and the list is short enough to read at a glance, which is itself a design decision. Each one names a setting that would otherwise live in a configuration file, and each is documented with a link to the documentation of the thing it sets.

There is the administrator email address, which defaults to a placeholder. There are two server names, one for the plain scheme and one for the secure one, both defaulting to a local host name. There is the log level, the timezone, the memory limit for the language runtime, and the maximum upload size.

The pattern is consistent and it is a good one. Every variable is optional, every variable has a default, and every variable maps to exactly one underlying setting whose documentation is linked. So a user who wants to change one thing can find out what it maps to, and a user who changes nothing gets a working installation with a sensible set of defaults.

The defaults are also chosen carefully rather than arbitrarily. The memory limit is generous for a small application, the upload size is the kind of default a PHP installation has, and the timezone is universal rather than a guess about where the server lives. The administrator address is the exception, because there is no sensible default for it: it is a placeholder, which means the first person to deploy has one thing to fill in.

And the ports are not in this list, because they are not environment variables in the image. They are published ports, and the deployment section says you may change them to your preferred values, with both schemes exposed by default.

The result is an application whose entire configuration surface is a dozen lines of environment variables, which is what a self-hosted project needs, because the person deploying it is often not the person who wrote it, and a configuration file inside a container is a file somebody has to extract and edit.

Deploying, and the volume that matters

The deployment section opens with a warning badge recommending that you set up persistent storage, and then it gives the steps, and the second step is the one that decides whether your installation has a future.

shell
docker volume create linkstack

That creates a named volume. Then the run command, and every flag in it is there for a reason.

shell
docker run --detach \
    --name linkstack \
    --publish 80:80 \
    --publish 443:443 \
    --restart unless-stopped \
    --mount source=linkstack,target=/htdocs \
    linkstackorg/linkstack

The detached flag runs it in the background. The name gives the container a handle you can stop and start. The two publish flags expose the web server on both schemes. The restart policy brings it back after a reboot. And the mount is the important one: it attaches the named volume to the directory the application writes to.

That directory is the application. For a link page, the state is a small database and a set of uploaded files, and both of them live there, so a container started without the volume starts with an empty site and a container whose volume is not mounted loses the site on the next recreation.

This is the failure mode the badge is warning about, and it is a common one: the container starts, the page works, somebody adds a dozen links, and then the container is recreated during a deployment and every link is gone. The named volume is what prevents it, and the readme puts the volume creation step before the run step for exactly that reason.

The custom deployment variant then shows the same command with the environment variables filled in, and it is worth noticing what it adds: a hostname, the two server names, the administrator address, the timezone, and the two runtime limits. Nothing else. The whole difference between the shortest possible deployment and a configured one is eleven lines of flags.

Two compose files, and the database question

There are two different compose examples in the documentation and the repository, and the difference between them is the most instructive thing in this project.

The readme's example has one service and a named volume.

yaml
version: "3.8"

services:

  linkstack:
    hostname: 'linkstack'
    image: 'linkstackorg/linkstack:latest'
    environment:
      TZ: 'Europe/Berlin'
      SERVER_ADMIN: '[email protected]'
      HTTP_SERVER_NAME: 'example.com'
      HTTPS_SERVER_NAME: 'example.com'
      LOG_LEVEL: 'info'
      PHP_MEMORY_LIMIT: '256M'
      UPLOAD_MAX_FILESIZE: '8M'
    volumes:
      - 'linkstack_data:/htdocs'
    ports:
      - '8190:443'
    restart: unless-stopped

volumes:
  linkstack_data:

One service, a named volume, and one published port. That is the minimal installation, and it implies that the application stores its data in a file inside that volume, which is what a self-hosted personal application wants: no database to run, no separate service to back up, and a backup is one file.

The repository's own compose file is the other arrangement, and it adds a database service, a dependency on it, the user the container runs as, and a second published port. It also has a root password written into the file, a legacy way of expressing the dependency, and a commented-out flag for running the container with a read-only root filesystem, which is the kind of hardening that is nearly free to write down and rarely done.

So the application supports two storage arrangements, and the documentation recommends the simple one. That is worth saying explicitly, because the alternative is the usual trap: a self-hosted project that shows you a compose file with a database in it, and a user who assumes that is the only way and ends up with two services to run for a page with twelve links.

There is one instruction in the readme about the reverse proxy, and it is a note under the compose heading: use the secure scheme for your reverse proxy to avoid issues. Which is the kind of warning that exists because somebody shipped it the other way and got a report.

The repository, and what is in the image

The top level of the repository is short, and each entry explains a part of the build.

There is the container file, the entry point script, a directory of configuration files, a directory holding the application itself, a compose file, and a script that runs the tests automatically. Plus the usual governance files and the directory of continuous integration configuration.

The container file is the interesting one, and the part worth reading is the long list of packages added to the base image. The base is a pinned Alpine release, and the list runs to about twenty entries: the web server, its secure module, a download tool, and then one package per language runtime extension the application needs. Arithmetic, compression, calendar, common, type checking, the database driver, the file information extension, the image extension, the character set library, the JSON extension, the multibyte string library, and so on.

Listing them one by one is the right way to do it, and it is the honest way: each package is a capability, and an extension that is not on the list is a feature that will fail at run time with a message about a missing function. A single aggregated package would be shorter and would obscure which capability costs what.

The entry point script is the other half of the design, and its existence is what makes the environment variables work. A container image cannot be configured at build time if the configuration is per deployment, so the values have to be applied when the container starts, which is exactly what an entry point is for. The directory of configuration files holds the templates that the entry point fills in, which is why the readme never tells you to edit a configuration file inside the container: there is nothing to edit, because the configuration is generated from the environment every time the container starts.

And the script that runs the tests is what tells you the image is built by a pipeline that checks it. A container project with a test script and a continuous integration directory is a project where a broken image is caught before it is published, rather than by the person who pulls it.

Which is the last piece: the readme's own summary of the image is that it is a simple solution containing everything needed, and the repository is the evidence that the simplicity is the result of doing the unglamorous parts properly, one extension at a time.

Editorial conclusion

The container image fits somebody who wants a link page on their own server with their own domain and no third party between the visitor and the links. It does not fit somebody who wants a hosted service with a managed database, because the whole point of the image is that the data lives in a volume you control. Verify first that you create the named volume before the first run, because the deployment steps in the readme say so and the data is the application, that you set the administrator address and the server names before the first launch rather than afterwards, since the first launch is what creates the account, and that you put a reverse proxy in front of it with the secure scheme, which the readme warns about explicitly.

Frequently asked questions

What is the LinkStack container image?

It is the official container edition of the LinkStack link sharing platform, and it contains everything needed to run it: an Alpine base, the Apache web server, PHP 8.3, and the application itself, with all the features and customisation options of the original retained.

Which processor architectures does the LinkStack image support?

Four: the standard 64-bit desktop architecture, the 64-bit ARM architecture, and the two 32-bit ARM versions, six and seven, which are what single board computers and older embedded devices run. The image is published to two public registries, so a deployment is not dependent on one of them.

How do I configure the LinkStack container?

Entirely through environment variables. There are seven optional ones, each with a default and a link to the documentation of the setting it maps to: the administrator address, the two server names, the log level, the timezone, the language runtime memory limit and the maximum upload size. The published ports are set with flags on the run command rather than by a variable.

Why does the LinkStack deployment need a named volume?

Because the volume is the application. The state of a link page is a small database and a set of uploaded files, and both live in the directory that is mounted, so a container started without the volume comes up with an empty site and a container whose volume is not mounted loses the site the next time it is recreated. The readme puts the volume creation step before the run step for that reason.

Does the LinkStack container need its own database service?

Not in the arrangement the readme recommends, which has one service and a named volume, storing the data in a file inside the volume. The repository's own compose file takes the other approach with a separate database service, a dependency on it and a hardcoded root password, and it also carries a commented-out flag for running with a read-only root filesystem.

Official sources

  1. Issues
  2. License: AGPL-3.0
  3. LinkStackOrg/linkstack-docker on GitHub
  4. Project website
  5. README
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/linkstackorg-linkstack-docker.svg)](https://hysenlabs.com/projects/linkstackorg-linkstack-docker)

Was this useful?