Self-hosted service
ericls/imgdd avatar
ericls/imgdd

IMGDD: a self-hostable image host with pluggable storage and a GraphQL API

simple image hosting program

550 stars81 forksGoAGPL-3.0

At a glance

What is it?
IMGDD is a Go image hosting program that grew out of a running service handling over a terabyte of traffic and 4.2 million requests a day, and was open-sourced when its author decided growth might force him to stop accepting images. It is a single binary with a React admin interface, pluggable storage across S3, local disk, WebDAV and IPFS, a GraphQL API and an in-browser image editor, and it is AGPL-3.0 licensed.
Who is it for?
Adopt IMGDD if you want to run your own image host rather than depend on a public one, and if you need the parts a simple upload script does not have, accounts and organisations, roles and a site-owner privilege, a GraphQL API with per-field authorisation, an image editor that keeps the original intact and an admin interface.
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?
Yes. The repository last received commits 20 days ago.
What is it written in?
Mainly Go, according to GitHub's language statistics.

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

Editorial analysis

Why it was open-sourced

IMGDD is described as a simple, self-hostable image hosting program, and the introduction explains the circumstances rather than the features.

It powers an image hosting project that began in early 2023. After some users requested the source code, the author decided to make it available following a bit of refactoring and cleanup. And as of February 2025, the project handles over one terabyte of traffic and 4.2 million requests per day. Since continued growth may force the author to stop accepting new images, the project was open-sourced so that anyone can run their own instance and serve their images.

That is a genuinely different reason for open-sourcing something than the usual one, and it shapes what the project is. There is no cloud provider behind it and no paid tier. The live instance is imgdd.com, and the software is the point.

The scale figures are worth taking seriously, because they are the difference between a weekend project and something that has been load-tested by reality. One terabyte of traffic and 4.2 million requests a day on an instance with 256 megabytes of memory is a remarkable ratio, and the README attributes it to the design: a single Go binary serves the API, the web client and the image proxying, and nothing more is needed.

The primary language is Go, the licence is AGPL-3.0, the default branch is main, and the recent releases are 0.10.0 from 2026-10-03, 0.9.1 and 0.9.0 from 2026-10-02. Three releases in two days tells you the project is in active development, and the last push was on 2026-10-03 with the project not archived.

The AGPL licence is a deliberate choice for a hosted service, and it is the right one from the author's point of view: it means anyone who wants to run a modified version as a service has to publish the modification. For a project that exists so other people can take over serving images, that is the correct balance.

Storage backends and URL formats

The first feature is pluggable storage backends, and it is the one that determines whether this fits your infrastructure.

The list is S3 and S3-compatible services such as MinIO, the local filesystem, WebDAV, and IPFS MFS. Multiple backends can be enabled at once with configurable priority, defined in the database or the config file. And images can be replicated between backends with the replicate CLI command.

So the model is a priority list rather than a single choice, and the definition of that list can live in the database as well as in the config file. That matters in practice: and it means the priority order is a piece of operational state rather than a deployment artefact.

Replication is a separate operation rather than a side effect of upload, which is the right design. A write goes to one backend, and copying to another is a deliberate act, because a service that wrote to three backends on every upload would triple both the storage cost and the failure modes.

The second feature is flexible URL formats, and there are two of them. The canonical form is proxied from the best available backend, and the direct form is routed through a specific backend. Per-image delivery then chooses the highest-priority enabled backend.

So a canonical URL is a stable identifier that resolves to whichever backend is currently best, and a direct URL pins the request to one backend. The distinction matters in a design where you have a local disk cache in front of an S3 origin, or where you want to move traffic between providers without breaking the links you have already handed out.

The go.mod confirms the storage story in the dependency list. The AWS SDK at version 1.55.8 is the S3 client, and the indirect list is enormous, with cloud.google.com/go/storage, the Azure blob SDK and the Azure autorest packages all present. Those are transitive dependencies of the AWS SDK's own module, which is the usual reason an indirect list looks like that, but they do tell you the S3 client is the full-featured one rather than a minimal implementation.

The upload pipeline and the safety features

The upload pipeline is the third feature and the one with the most individual parts, and each part addresses a specific failure.

Automatic EXIF stripping comes first, and it is a privacy feature as much as a size one. EXIF data in a photo can carry the GPS coordinates of where it was taken, the device model and sometimes a serial number, and an image host that keeps it is publishing the location of the photographer to anyone who requests the file.

MIME-type detection with declared and detected mismatch rejection is next. The client declares a content type and the server detects the actual one, and if they disagree the upload is refused. That closes a straightforward attack: without the check, a file could be uploaded with an image content type and served with something the browser will execute.

Size limits are the obvious one, and deduplication across stored images is the interesting one: if the same file is uploaded twice, it is stored once, and the revisions are linked back to a root image. So a revision, which is what the editor produces, does not duplicate the bytes, and the relationship is recorded rather than implied by a filename.

Then the two optional safety features. The safe-image check delegates NSFW and abuse screening to an external HTTP endpoint, and the README is clear that it is optional and delegated rather than built in, which is a sensible division: the detection models live elsewhere and the host decides whether to call them.

Captcha protection supports Google reCAPTCHA or Cloudflare Turnstile, wired into GraphQL through an @captchaProtected directive. That detail is worth noting, because it tells you the GraphQL layer is where the protection is applied rather than in a separate authentication middleware, which is a consequence of GraphQL's single-endpoint shape.

Rate limiting applies to uploads and other sensitive endpoints, and the ratelimit/ directory at the repository root is where that lives.

The identity and access control feature is the largest single item. There is email and password authentication, organizations, a role and permission system with built-in roles, a site-owner privilege, and password reset by email. The organisation model is what makes this usable by more than one team, and the site-owner role is what makes it operable: a single account that can administer the whole instance.

The GraphQL API, the editor and the web client

The API is GraphQL, generated with gqlgen, and the feature list names what it covers: images, viewer, users, organizations, roles and permissions, and storage definitions. It also names the two directives that do the access control, @isAuthenticated and @isSiteOwner.

Using GraphQL directives for authorisation rather than middleware is the natural fit here, because a single GraphQL endpoint serves queries of wildly different sensitivity, and a per-field directive can express that a query needs an authenticated user while another needs site ownership. It is the same reason the captcha check is a directive.

The go.mod pins gqlgen at 0.17.90, and the presence of the gqlparser at 2.5.33 is the schema parser that gqlgen uses. The gqlgen.yml file at the repository root is the generator configuration, and the graph/ directory is where the generated code lives.

The image editor is the feature that makes this more than a storage service. It is in-browser editing with watermark support, and the important detail is that edits are saved as new images with full lineage tracking, as a parent and child directed acyclic graph, so the original is never modified.

That is the right design and it is not an obvious one. A watermark feature that edits in place destroys the original, which is a problem when the original is what a user uploaded and what the terms of service said would be served. With lineage, the original is immutable, the watermark version is a derived revision, and the deduplication and root-image linking described earlier means the bytes are not duplicated either.

The web client is React with Tailwind, and it includes a site-admin area for managing users, roles and storage definitions. So the admin interface is part of the same binary rather than a separate application to deploy.

There is one more feature worth calling out because it is unusual. The client plugin hooks are a lightweight window.registerPlugin API that lets custom JavaScript inject content into named UI slots without rebuilding the frontend. So an operator can add a banner or a panel to the web client by dropping in a script, which is a pragmatic answer to the usual problem of a bundled frontend that cannot be extended without a fork and a rebuild.

Setting it up locally, and the commands the project gives you

The development section is unusually complete, which is a good sign for a project expecting contributors.

The prerequisites are Go 1.25 or later, Node.js 20 for the frontend, Docker and Docker Compose for local services, and lefthook for pre-commit hooks. If you use nix, the README says you can just do nix develop, and the flake.nix and flake.lock in the repository back that up.

The setup is six numbered steps. Copy the environment file and adjust if needed:

bash
   cp .env.template .env

Start local services, which are PostgreSQL, Redis and a MinIO of an ancient version:

bash
   docker compose up -d

The parenthetical about MinIO being an ancient version is the kind of detail that saves an afternoon, because the latest MinIO has changed in ways that break older test setups.

Run the database migrations:

bash
   go run . migrate

Populate the built-in roles:

bash
   go run . populate-built-in-roles

Create a user, with an is-site-owner flag:

bash
   go run . create-user --email [email protected] --password yourpassword --is-site-owner

And install the pre-commit hooks if you plan to contribute:

bash
   lefthook install

Building and running is three commands, and the frontend is a separate build step from the server:

bash
# Build the frontend
cd web_client && pnpm install && pnpm build && cd ..

# Start the server
go run . serve

# Start with auto-reload (requires air)
go run . dev-server

The dev-server variant requires air, which the .air.toml at the repository root configures, and it is the same hot-reload setup used by a lot of Go projects.

There is also a code generation section, and it names three generators. The gql command regenerates the GraphQL code with gqlgen. The jet command regenerates the Jet ORM code and requires a running PostgreSQL, which makes sense because the Jet generator inspects the live schema. And in the web_client directory, pnpm gen regenerates the frontend GraphQL types.

So the type contract between the three layers, the GraphQL server, the Jet ORM and the React client, is generated rather than hand-maintained, which is what keeps them in step.

The database commands are a longer set, covering migrate, migrate to a specific version with a --version flag, make-migration with a name, and reset-db which is marked dev only. All commands accept a -c or --config flag for the config file path and a --log-level flag that defaults to info.

How to evaluate it, and the questions to ask first

There are five things to consider.

First, the licence, because it is the one hard constraint. AGPL-3.0 means that if you run a modified version as a network service, you have to offer the source of your modification to your users. For self-hosting your own instance for yourself, that is not a burden. For running a public service on a modified fork, it is a real one, and the author chose it deliberately, having already run a public instance at imgdd.com.

Second, the operational shape. A single Go binary serves the API, the client and the proxying, and the storage backends are pluggable, so the minimum viable deployment is one container and one database. But there are also Redis, PostgreSQL and the S3-compatible storage in the local development setup, so the full feature set is a bigger footprint than the single-binary story suggests. The development compose file is the honest picture of what runs.

Third, whether you need the parts beyond hosting. The image editor with lineage tracking, the organisations and role system, the plugin hooks, the captcha protection and the safe-image check are all real work that a simpler image host would not have. If you want a plain upload and serve service, this is more than you need. If you want an image service with accounts, an admin interface, a moderation hook and an editor, this is a great deal of work you do not have to do.

Fourth, the GraphQL decision. It gives you a typed API with per-field authorisation, which is genuinely better than a set of REST endpoints for this shape of application. It also means every client you write has to speak GraphQL, and the generated types are the contract. If your team is comfortable with that, it is an advantage; if not, it is a barrier.

Fifth, the project's stage. The version is 0.10.0, which is pre-1.0, and there is a BREAKING_CHANGES.txt file at the repository root, which tells you that breaking changes are happening and are tracked in a file rather than only in a changelog. Three releases in two days suggests active work, and pre-1.0 with a breaking-changes log means you should pin a version and read that file before upgrading.

What you get is a service that has handled four million requests a day on 256 megabytes of memory, with a feature set that is well past the usual image-host script, and a codebase that is small enough to read and documented enough to contribute to. For anyone who wanted to run their own image host rather than depend on a public one, it is exactly the artefact that needed to exist.

Editorial conclusion

Adopt IMGDD if you want to run your own image host rather than depend on a public one, and if you need the parts a simple upload script does not have, accounts and organisations, roles and a site-owner privilege, a GraphQL API with per-field authorisation, an image editor that keeps the original intact and an admin interface. Do not adopt it expecting a stable 1.0 release, because the version is 0.10.0 and there is a BREAKING_CHANGES.txt file at the root, so pin a version and read that file before upgrading. Before you commit, check the AGPL-3.0 terms, which require you to publish the source of any modification you run as a network service, plan for more than the single binary, since the development setup runs PostgreSQL, Redis and an S3-compatible store, and decide on your storage backend first, because the priority list and the canonical versus direct URL choice determine how the rest is deployed.

Frequently asked questions

What is IMGDD?

It is a simple, self-hostable image hosting program written in Go, which powers the imgdd.com project. The project began in early 2023 and was open-sourced after users asked for the source code, and it is AGPL-3.0 licensed with a React and Tailwind web client.

Why was IMGDD open-sourced?

Because continued growth on the author's own instance may have forced him to stop accepting new images, so the code was released so that anyone could run their own instance and serve their images. As of February 2025 the project handled over one terabyte of traffic and 4.2 million requests per day on an instance with 256 megabytes of memory.

Which storage backends does IMGDD support?

S3 and S3-compatible services such as MinIO, the local filesystem, WebDAV and IPFS MFS. Multiple backends can be enabled at once with a configurable priority defined in the database or the config file, and the replicate CLI command copies images between backends.

What does the IMGDD upload pipeline do?

It strips EXIF data automatically, detects the MIME type and rejects an upload when the declared and detected types disagree, applies size limits, and deduplicates identical files across stored images with revisions linked back to a root image. There is also an optional safe-image check that delegates NSFW and abuse screening to an external HTTP endpoint.

How do I set up IMGDD for local development?

Copy .env.template to .env, start PostgreSQL, Redis and MinIO with docker compose up -d, run go run . migrate, populate the built-in roles with go run . populate-built-in-roles, create a user with go run . create-user, and install the pre-commit hooks with lefthook install. You need Go 1.25 or later, Node.js 20, Docker Compose and lefthook, or you can use nix develop.

What is the IMGDD image editor and how does it handle the original file?

It is in-browser editing with watermark support. Edits are saved as new images with full lineage tracking as a parent and child directed acyclic graph, so the original is never modified. Combined with deduplication, the derived version does not duplicate the stored bytes either.

Official sources

  1. ericls/imgdd on GitHub
  2. Issues
  3. License: AGPL-3.0
  4. README
  5. Releases
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/ericls-imgdd.svg)](https://hysenlabs.com/projects/ericls-imgdd)

Was this useful?