Post

Deploy Image Extender AI outpainting with Docker, Traefik, and Authentik

Run Image Extender as a private AI outpainting and 2D game-art studio behind Traefik and Authentik, with OpenRouter BYOK stored in the browser or an optional server-side fallback key.

Deploy Image Extender AI outpainting with Docker, Traefik, and Authentik

Image Extender is a small open-source web studio for AI outpainting and 2D game-art generation. It can extend images in any direction, generate parallax backgrounds, build tiles, create sprite sheets, and produce transparent props.

The upstream project is designed around BYOK: bring your own OpenRouter key. By default, the key is entered in the browser and stored in that browser’s localStorage. For a private homelab deployment, that is usually the safest default because the server does not need to persist an API key at all.

This guide deploys Image Extender as a standalone Docker Compose service behind Traefik and protects the UI with Authentik.

1
2
3
4
5
6
7
8
9
10
11
12
13
Browser
  ↓ HTTPS
2d.example.com
  ↓
Cloudflare DNS
  ↓
Traefik HTTPS router
  ↓ forward-auth
Authentik Embedded Outpost
  ↓ after login
Image Extender Next.js container on port 3000
  ↓ when generating
OpenRouter → Gemini image model

All domains, usernames, API keys, tokens, client secrets, and account details in this post are placeholders. Replace 2d.example.com with your own hostname and never publish real .env files, public server IPs, OAuth secrets, OpenRouter keys, or private infrastructure details.


What this service does

Image Extender provides a browser interface for creative AI image operations:

  • outpaint an uploaded image left, right, up, or down;
  • choose the best result from multiple generated variants;
  • blend seams between the original image and AI-generated extension;
  • generate parallax background layers for 2D games;
  • generate autotile sets, sprites, and standalone props;
  • use OpenRouter-hosted Gemini image models from the web UI.

In a homelab, the useful pattern is:

  • keep the app in its own project directory;
  • build a reproducible production Next.js container;
  • expose only Traefik publicly;
  • require Authentik login before the app loads;
  • keep the OpenRouter key in browser storage unless you intentionally want a shared server-side fallback key.

Network model:

1
Internet → Traefik :443 → image-extender:3000 on the private Docker proxy network

The Image Extender container should not publish a host port. Traefik should be the only public entry point.


This service uses the same homelab foundation as the other private web apps:

Read the Traefik and Authentik guides first if you do not already have a shared Docker network, HTTPS routing, and forward-auth middleware.


Folder layout

Use one dedicated service directory.

1
2
sudo mkdir -p /home/ubuntu/image-extender
cd /home/ubuntu/image-extender

The finished layout will look like this:

1
2
3
4
5
6
7
8
9
/home/ubuntu/image-extender/
├── .env
├── docker-compose.yml
└── app/
    ├── Dockerfile.novelox
    ├── package.json
    ├── package-lock.json
    ├── next.config.js
    └── app/

The app/ folder is the upstream Image Extender repository. The parent folder keeps deployment-specific files such as Compose and .env.


Clone Image Extender

From the service directory, clone the upstream repository into app/:

1
2
cd /home/ubuntu/image-extender
git clone --depth 1 https://github.com/boona13/image-extender.git app

For updates later, pull inside that folder:

1
2
cd /home/ubuntu/image-extender/app
git pull --ff-only

Environment file

Create this file:

1
/home/ubuntu/image-extender/.env

Add the following content:

# Optional server-side fallback key for Image Extender.
# Leave blank to use BYOK from the web UI: Settings / first-run prompt stores it in browser localStorage.
OPENROUTER_API_KEY=

There are two valid key modes:

ModeWhere the key livesBest for
Browser BYOKBrowser localStoragePersonal/private use, no server-side key persistence
Server fallback/home/ubuntu/image-extender/.envShared internal demo where trusted users should not bring their own key

If OPENROUTER_API_KEY is blank, the app asks for a key in the web UI. The key stays in the browser and is sent with generation requests.

If OPENROUTER_API_KEY is set, the server can use it for requests that do not include a client-provided key.

If you set a server-side OpenRouter key, treat this .env file as a secret. Do not commit it, paste it into docs, or expose it through a public repository.


Production Dockerfile

The upstream app is a Next.js project. For a homelab deployment, build it into a production image instead of running npm run dev.

Create this file:

1
/home/ubuntu/image-extender/app/Dockerfile.novelox

Add this Dockerfile:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
FROM node:20-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci

FROM node:20-alpine AS builder
WORKDIR /app
ENV NEXT_TELEMETRY_DISABLED=1
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build

FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production \
    NEXT_TELEMETRY_DISABLED=1 \
    HOSTNAME=0.0.0.0 \
    PORT=3000

COPY --from=builder /app/package*.json ./
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/.next ./.next
COPY --from=builder /app/next.config.js ./next.config.js

EXPOSE 3000
CMD ["npm", "run", "start"]

This uses three stages:

  1. deps installs dependencies with npm ci.
  2. builder compiles the Next.js app.
  3. runner starts the production server on port 3000.

If the upstream repository later adds a public/ folder and your app needs it, copy it into the runner stage too:

1
COPY --from=builder /app/public ./public

Only add that line when the folder exists, otherwise Docker will fail with file does not exist.


Docker ignore file

Create this file:

1
/home/ubuntu/image-extender/app/.dockerignore

Add:

1
2
3
4
5
6
node_modules
.next
.git
.env*
npm-debug.log*
Dockerfile*

This keeps the Docker build context small and prevents local environment files from being copied into the image.


Docker Compose service

Create this file:

1
/home/ubuntu/image-extender/docker-compose.yml

Use this Compose file:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
services:
  image-extender:
    build:
      context: ./app
      dockerfile: Dockerfile.novelox
    image: local/image-extender:latest
    container_name: image-extender
    restart: unless-stopped
    env_file:
      - .env
    environment:
      NODE_ENV: production
      NEXT_TELEMETRY_DISABLED: "1"
      PORT: "3000"
      HOSTNAME: 0.0.0.0
    networks:
      - proxy
    labels:
      - "traefik.enable=true"
      - "traefik.docker.network=proxy"
      - "traefik.http.middlewares.image-extender-https-redirect.redirectscheme.scheme=https"
      - "traefik.http.routers.image-extender.entrypoints=http"
      - "traefik.http.routers.image-extender.rule=Host(`2d.example.com`)"
      - "traefik.http.routers.image-extender.middlewares=image-extender-https-redirect"
      - "traefik.http.routers.image-extender-secure.entrypoints=https"
      - "traefik.http.routers.image-extender-secure.rule=Host(`2d.example.com`)"
      - "traefik.http.routers.image-extender-secure.tls=true"
      - "traefik.http.routers.image-extender-secure.tls.certresolver=cloudflare"
      - "traefik.http.routers.image-extender-secure.middlewares=authentik@docker"
      - "traefik.http.routers.image-extender-secure.service=image-extender"
      - "traefik.http.services.image-extender.loadbalancer.server.port=3000"
    healthcheck:
      test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:3000 >/dev/null || exit 1"]
      interval: 30s
      timeout: 5s
      retries: 5
      start_period: 30s

networks:
  proxy:
    external: true

Replace 2d.example.com with your real hostname on your server.

Important details:

  • The container joins the external proxy network used by Traefik.
  • No host port is published.
  • Traefik sends HTTPS traffic to container port 3000.
  • The secure router uses the authentik@docker middleware.
  • The healthcheck confirms the Next.js server is answering locally.

Build and start the app

From the service directory, run:

1
2
cd /home/ubuntu/image-extender
docker compose up -d --build

Check status:

1
docker compose ps

Expected result:

1
2
NAME             IMAGE                         STATUS
image-extender   local/image-extender:latest   Up (healthy)

Check logs:

1
docker logs --tail 80 image-extender

A healthy startup should include something like:

1
2
3
4
> [email protected] start
> next start

✓ Ready

Authentik application and provider

If your Traefik setup uses Authentik forward-auth, create a dedicated Authentik application/provider for Image Extender.

Recommended values:

1
2
3
4
5
6
7
8
Application name: Image Extender
Application slug: image-extender
Provider type: Proxy Provider
Proxy mode: Forward auth single application
External host: https://2d.example.com
Cookie domain: example.com
Authorization flow: default provider authorization flow
Invalidation flow: default provider invalidation flow

Then attach the provider to the embedded outpost.

The callback path must be routed back to Authentik. If you use Docker labels on the Authentik server container for embedded outpost callbacks, add a router for the Image Extender hostname.

Edit this file:

1
/home/ubuntu/authentik/docker-compose.yml

Inside the server: service labels, add:

1
2
3
4
5
6
      - "traefik.http.routers.authentik-image-extender-outpost.entrypoints=https"
      - "traefik.http.routers.authentik-image-extender-outpost.rule=Host(`2d.example.com`) && PathPrefix(`/outpost.goauthentik.io/`)"
      - "traefik.http.routers.authentik-image-extender-outpost.priority=100"
      - "traefik.http.routers.authentik-image-extender-outpost.tls=true"
      - "traefik.http.routers.authentik-image-extender-outpost.tls.certresolver=cloudflare"
      - "traefik.http.routers.authentik-image-extender-outpost.service=authentik"

Recreate the Authentik server after changing labels:

1
2
cd /home/ubuntu/authentik
docker compose up -d server

If this route is missing, the first redirect to Authentik may work, but the callback back to /outpost.goauthentik.io/ will not be handled correctly.


Cloudflare DNS automation

If you run Traefik Cloudflare Companion, restart it after adding a new Traefik hostname so it can create the DNS record:

1
2
cd /home/ubuntu/companion
docker compose restart cloudflare-companion

Check its logs:

1
docker logs --tail 120 traefik-cloudflare-companion

You want to see that the new hostname was found and that the DNS record exists or was created.


Verify the deployment

First verify the container directly:

1
docker exec image-extender wget -qO- http://127.0.0.1:3000 | head

You should see HTML from the Next.js app.

Then verify the route through Traefik from another container on the proxy network. This confirms the Authentik middleware is intercepting unauthenticated requests:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
docker exec image-extender node - <<'NODE'
const https = require('https')
const opts = {
  host: 'traefik',
  port: 443,
  path: '/',
  method: 'HEAD',
  headers: { Host: '2d.example.com' },
  servername: '2d.example.com',
  rejectUnauthorized: false,
  timeout: 10000,
}
https.request(opts, (res) => {
  console.log(res.statusCode, res.headers.location || '')
}).on('error', (err) => {
  console.error(err)
  process.exit(1)
}).end()
NODE

Before login, the expected result is a redirect to Authentik:

1
302 https://auth.example.com/application/o/authorize/...

Finally, open the service in a browser:

1
https://2d.example.com

Expected browser flow:

  1. Authentik login page appears.
  2. After login, the browser returns to Image Extender.
  3. The Image Extender UI loads with the upload area and settings button.
  4. On first generation, the app asks for an OpenRouter key unless you set a server fallback key.

Add the service to Homepage

If you use Homepage, add the new service to your services file.

Edit:

1
/home/ubuntu/homepage/config/services.yaml

Example entry:

1
2
3
4
5
6
- AI & Automation:
    - Image Extender:
        icon: mdi-image-size-select-large
        href: https://2d.example.com
        description: AI outpainting and 2D game-art studio using OpenRouter BYOK
        siteMonitor: https://2d.example.com

Restart Homepage:

1
2
cd /home/ubuntu/homepage
docker compose restart homepage

Using the OpenRouter API key

Image Extender supports two key paths.

Option 1: Web UI BYOK

This is the default and recommended private-user setup.

  1. Open the app.
  2. Click settings or follow the first-run prompt.
  3. Paste an OpenRouter API key.
  4. The key is stored in browser localStorage.

This means:

  • the key is not written to the server disk;
  • each browser can use its own key;
  • clearing browser storage removes the key;
  • the server still proxies requests, but does not need a stored fallback key.

Option 2: Server fallback key

Use this only if you want the server to provide a key for trusted users.

Edit:

1
/home/ubuntu/image-extender/.env

Set:

OPENROUTER_API_KEY=your_openrouter_key_here

Restart the app:

1
2
cd /home/ubuntu/image-extender
docker compose restart image-extender

This is convenient, but less isolated than BYOK. Anyone who can use the app may be able to spend credits through that server-side key, so keep the UI behind Authentik and limit access to trusted users.


Updating Image Extender

To update the app:

1
2
3
4
5
cd /home/ubuntu/image-extender/app
git pull --ff-only

cd /home/ubuntu/image-extender
docker compose up -d --build

Then verify:

1
2
docker compose ps
docker logs --tail 80 image-extender

If dependencies changed, the Docker build will run npm ci again from the updated package-lock.json.


Backup notes

Image Extender does not need a large persistent data volume for normal use. The important deployment files are:

1
2
3
/home/ubuntu/image-extender/docker-compose.yml
/home/ubuntu/image-extender/.env
/home/ubuntu/image-extender/app/Dockerfile.novelox

If you use browser BYOK, the OpenRouter key is not part of server backup. It lives in the user’s browser storage.

If you use a server fallback key, include .env in your private server backup process but exclude it from public repositories and documentation.


Troubleshooting

The app redirects to Authentik but never returns

Check the Authentik provider and callback route:

  • provider external host must match https://2d.example.com;
  • the provider must be attached to the embedded outpost;
  • the Authentik server labels must include the /outpost.goauthentik.io/ router for the hostname;
  • the browser must be able to reach both the app hostname and the Authentik hostname.

The app is reachable without login

Check the secure Traefik router labels on Image Extender:

1
- "traefik.http.routers.image-extender-secure.middlewares=authentik@docker"

Also confirm the request is hitting the image-extender-secure router, not another router with the same hostname.

Traefik returns 404

A Traefik 404 usually means no router matched the request.

Check:

  • the hostname in the router rule;
  • the container is attached to the proxy network;
  • Traefik can see Docker labels;
  • the Compose project was recreated after changing labels.

Run:

1
docker inspect image-extender --format ''

The container is unhealthy

Check the local app response:

1
docker exec image-extender wget -qO- http://127.0.0.1:3000 | head

Then check logs:

1
docker logs --tail 120 image-extender

Common causes:

  • Next.js build failed;
  • package-lock.json changed but the image was not rebuilt;
  • Dockerfile tries to copy a missing folder such as public/;
  • port or hostname environment variables were changed incorrectly.

Generation fails with API-key errors

If using BYOK:

  • open settings in the web UI;
  • confirm the OpenRouter key is saved;
  • clear and re-enter it if needed;
  • verify the OpenRouter account has credits.

If using server fallback:

  • confirm OPENROUTER_API_KEY is set in /home/ubuntu/image-extender/.env;
  • restart the container after editing .env;
  • check logs for OpenRouter response errors.

Security checklist

  • Image Extender has no public host port.
  • Traefik is the only public entry point.
  • HTTPS router uses authentik@docker or equivalent access control.
  • Authentik provider external host matches the service hostname.
  • Embedded outpost callback route exists for /outpost.goauthentik.io/.
  • OPENROUTER_API_KEY is blank unless a shared fallback key is intentional.
  • .env is excluded from Git and public documentation.
  • Homepage points to the HTTPS hostname, not a raw container port.

Final check

A clean deployment should satisfy all of these:

1
2
3
4
5
Container: image-extender is healthy
Internal app: http://127.0.0.1:3000 returns HTML
Public route: https://2d.example.com redirects unauthenticated users to Authentik
After login: Image Extender UI loads
API key: stored in browser localStorage by default, or provided by OPENROUTER_API_KEY intentionally

That gives you a private AI outpainting studio without exposing raw app ports or storing an OpenRouter key on the server unless you explicitly choose to.

This post is licensed under CC BY 4.0 by the author.