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.
What is Slink?
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.
Why Self-Host Slink?
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.
Self-Hosting Slink with Docker
Pre-Requisites - Docker
Install Docker on your system before proceeding:
- Linux: Official Docker Engine install guide
- Windows / Mac: Docker Desktop
Verify installation: docker --version && docker compose version
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.
When Slink Fits
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
Does Slink need a database container?
/app/var/data.
Which port should I publish?
3000. The API and Mercure service listen internally on 8080.
What should I back up?
/app/var/data and /app/slink/images.
Can Slink use object storage?
Was the Docker test isolated?
/tmp/foss-post. Existing containers were not modified, and only the Slink trial container was removed.
Comments