Image sharing is one of those tiny workflows that quietly leaks control.

You upload a screenshot to a chat app, paste a photo into a hosted service, or hand a public URL to somebody and hope the defaults match what you meant.

Slink is a self-hosted alternative: an image sharing platform built with Symfony and SvelteKit, designed around private links, public galleries, collections, tags, comments, bookmarks, API keys, and storage you control.

Slink is a browser-based image sharing service for homelabs, small teams, creators, and developers who want a private image hub.

It handles the common image-hosting jobs:

  • Upload images from the browser.
  • Share individual images through controlled links.
  • Protect shares with passwords.
  • Set share expiration.
  • Group images into collections.
  • Browse public images through an explore view.
  • Organize media with nested tags.
  • Keep upload history.
  • Bookmark and comment on public images.
  • Use API keys for integrations such as ShareX.
  • Manage users, approvals, moderation, storage, image settings, SSO, and security from an admin UI.

The storage story is also practical. Slink supports local storage, SMB shares, and S3-compatible object storage, so it can start as a single-machine app and later move the image backend elsewhere.

Tech Overview

Slink is split into two main services inside one repository.

The backend lives under services/api. It is a PHP/Symfony application with Doctrine, SQLite defaults, Redis, Symfony Messenger, Mercure, JWT auth, S3 support, SMB support, and image processing through libvips/GD.

The frontend lives under services/client. It is a SvelteKit app using Svelte 5, Vite, Tailwind CSS, i18n support, PWA pieces, and a route tree for uploads, explore, shares, collections, tags, bookmarks, notifications, integrations, profile pages, and admin settings.

The production Docker image bundles the moving parts into one container:

  • SvelteKit web frontend on port 3000.
  • Symfony API served internally by FrankenPHP/Caddy on port 8080.
  • Redis for runtime coordination.
  • Mercure for live updates.
  • Scheduler worker for background jobs.
  • SQLite databases under /app/var/data.
  • Generated JWT, Mercure, and app secrets under /app/var/data/keys.
  • Uploaded images under /app/slink/images.

That makes Slink simpler to start than a multi-service stack, but it also means the persistent volumes matter. Treat app data, generated keys, and uploaded images as one backup unit.

Slink makes sense when you want image sharing without turning every upload into a third-party dependency.

The strongest use cases are:

  • Developer screenshots for issues, docs, blogs, and demos.
  • Private media shares with revocation.
  • Small community image galleries.
  • Internal team image uploads.
  • ShareX-backed screenshot publishing.
  • A public/private image hub behind your own domain.
  • Object-storage-backed image hosting with a normal web UI.

It is not trying to replace a full photo library. If you mainly need phone backup, timeline browsing, people search, albums from mobile devices, and large personal photo management, compare Slink with Immich. Slink is more focused on sharing and publishing images.

The upstream repository provides a minimal compose file that starts anirdev/slink:latest, names the container slink, and enables the API.

For a real homelab, I would make the port explicit, persist the important directories, and keep admin credentials out of the compose file.

services:
  slink:
    image: anirdev/slink:latest
    container_name: slink
    restart: unless-stopped
    ports:
      - "${SLINK_HTTP_PORT:-3000}:3000"
    environment:
      TZ: ${TZ:-UTC}
      API_ENABLED: "true"
      STORAGE_PROVIDER: ${STORAGE_PROVIDER:-local}
      USER_ALLOW_REGISTRATION: ${USER_ALLOW_REGISTRATION:-true}
      USER_APPROVAL_REQUIRED: ${USER_APPROVAL_REQUIRED:-true}
      USER_ALLOW_UNAUTHENTICATED_ACCESS: ${USER_ALLOW_UNAUTHENTICATED_ACCESS:-true}
      USER_ALLOW_GUEST_UPLOADS: ${USER_ALLOW_GUEST_UPLOADS:-false}
      IMAGE_MAX_SIZE: ${IMAGE_MAX_SIZE:-15M}
      ADMIN_USERNAME: ${ADMIN_USERNAME:-admin}
      ADMIN_EMAIL: ${ADMIN_EMAIL:-}
      ADMIN_PASSWORD: ${ADMIN_PASSWORD:-}
    volumes:
      - slink-data:/app/var/data
      - slink-images:/app/slink/images
      - slink-cache:/app/slink/cache

volumes:
  slink-data:
  slink-images:
  slink-cache:

Create a .env next to the compose file:

TZ=Europe/Warsaw
SLINK_HTTP_PORT=18090
STORAGE_PROVIDER=local
USER_ALLOW_REGISTRATION=true
USER_APPROVAL_REQUIRED=true
USER_ALLOW_UNAUTHENTICATED_ACCESS=true
USER_ALLOW_GUEST_UPLOADS=false
IMAGE_MAX_SIZE=15M
ADMIN_USERNAME=admin
ADMIN_EMAIL=
ADMIN_PASSWORD=

Validate and start:

docker compose config
docker compose up -d

Open:

http://localhost:18090

If you want Slink to create the first admin account on startup, set both ADMIN_EMAIL and ADMIN_PASSWORD before first boot. If ADMIN_EMAIL is set but ADMIN_PASSWORD is empty, Slink warns and skips admin creation.

For production, consider pinning a versioned image tag instead of using latest, once you have tested the version you want to run.

Field Note: Docker Smoke Test

I ran a bounded Docker test on 2026-08-28 without touching existing containers.

The trial used:

  • Container name: slink-foss-post-trial-1787902395
  • Image: anirdev/slink:latest
  • Host port: 18090
  • Container port: 3000
  • Temporary data path: /tmp/foss-post/slink-foss-post-trial-1787902395/data
  • Temporary image path: /tmp/foss-post/slink-foss-post-trial-1787902395/images
  • Storage provider: local
  • API: enabled

The container became healthy. The internal healthcheck probes http://127.0.0.1:8080/api/health, and the external API check returned:

OK

The root page responded with:

HTTP/1.1 302 Found
location: /explore

Logs showed Slink 1.13.1, successful SQLite provisioning, generated JWT/Mercure/app secrets, local storage enabled, EXIF stripping enabled, image deduplication enabled, and the frontend listening on http://0.0.0.0:3000.

The only caveat from the trial was a Symfony cache permission warning when using host bind mounts:

Cannot rename ... /services/api/var/cache/prod/... Permission denied

The app still became healthy. For that reason, the compose example above uses named Docker volumes. If you prefer bind mounts, make sure ownership and write permissions match the container user.

After validation, I removed only the trial container.

One workflow note: /home/jalcocert/Desktop/Home-Lab was not available on this machine, so I did not add a reusable Home-Lab compose snippet for this post.

Configuration Notes

These are the settings I would decide before exposing Slink beyond localhost:

Setting Why it matters
USER_ALLOW_REGISTRATION Controls whether visitors can create accounts
USER_APPROVAL_REQUIRED Lets an admin review users before they upload
USER_ALLOW_UNAUTHENTICATED_ACCESS Controls anonymous browsing
USER_ALLOW_GUEST_UPLOADS Controls whether users can upload without an account
IMAGE_MAX_SIZE Sets upload size limits
IMAGE_STRIP_EXIF_METADATA Reduces accidental metadata leaks
IMAGE_DEDUPLICATION_ENABLED Avoids storing duplicate images
STORAGE_PROVIDER Chooses local, SMB, or S3-compatible storage
ADMIN_EMAIL and ADMIN_PASSWORD Optional first-admin bootstrap

For S3-compatible storage, keep credentials in .env, not in the compose file. Slink exposes variables for region, bucket, access key, secret key, custom endpoint, path-style URLs, custom providers, and IAM role usage.

For SMB storage, configure the host, share, workgroup, username, and password through environment variables.

Reverse Proxy Notes

Publish only port 3000 from the container. The API service on 8080 is internal to the container and is used by the bundled frontend and healthcheck.

Behind a reverse proxy:

  • Terminate HTTPS at the proxy.
  • Forward to slink:3000.
  • Keep upload size limits in sync with IMAGE_MAX_SIZE.
  • Back up the data and image volumes together.
  • Avoid exposing the Docker socket or extra internal ports.

Slink is a good fit if you want:

  • A self-hosted image sharing web app.
  • Private links by default.
  • Public sharing when you choose it.
  • Collections and nested tags.
  • API keys and ShareX integration.
  • SSO/OIDC support.
  • Local, SMB, or S3-compatible storage.
  • A single-container deployment.

It may not be the right choice if you need:

  • Full personal photo-library automation.
  • Mobile camera roll backup.
  • Heavy DAM workflows.
  • Video hosting.
  • Fine-grained enterprise compliance tooling.

Alternatives to Know

For adjacent self-hosted media workflows, compare:

  • Immich for personal photo backup and gallery management.
  • Chevereto for a more established image hosting platform.
  • Photoview for viewing an existing photo library.
  • File Browser for general browser-based file management.
  • copyparty for a compact file server with uploads and sharing.

Slink stands out when you want a dedicated image sharing workflow rather than a general file manager or mobile photo backup system.

Conclusion

Slink is a strong homelab candidate if you share screenshots, design assets, documentation images, or public/private gallery links often.

The architecture is modern but operationally simple: one Docker image, one public port, SQLite defaults, generated secrets, local or externalized storage, and an admin UI for the important knobs.

The isolated Docker smoke test worked on a non-default host port. Health passed, /api/health returned OK, the root route redirected to /explore, and the trial container was removed afterward.

FAQ