Post

Deploy Open Design with Docker, Traefik, Authentik, Gemini CLI, and Codex CLI

Run Open Design as a private AI design workspace behind Traefik and Authentik, with Gemini CLI and Codex CLI installed inside the container and persisted across rebuilds.

Deploy Open Design with Docker, Traefik, Authentik, Gemini CLI, and Codex CLI

Open Design is a local-first design workspace that can connect to terminal AI agents. In a homelab, it fits nicely as a private operator tool: expose the web UI through Traefik, protect it with Authentik, and keep the CLI agent state in persistent Docker volumes.

This guide deploys Open Design with two AI CLIs installed inside the same container:

  • Gemini CLI for Google Gemini / Gemini Code Assist workflows.
  • Codex CLI for OpenAI Codex workflows.

The result is a web design workspace reachable at a private HTTPS hostname, while the application container itself stays on the Docker network and does not publish a host port.

All domains, usernames, tokens, client secrets, and account details in this post are placeholders. Replace design.example.com with your own hostname and never publish real OAuth codes, API keys, session files, or private infrastructure details.


What this service does

Public route:

1
https://design.example.com

Access model:

1
2
3
4
5
6
7
8
9
10
11
Browser
  ↓
Cloudflare DNS
  ↓
Traefik HTTPS router
  ↓
Authentik forward-auth
  ↓
Open Design container
  ↓
Gemini CLI / Codex CLI inside persistent container home

Main components:

1
Open Design web/daemon process, Docker volumes for app state and CLI home, Gemini CLI, Codex CLI, Traefik, Authentik, and optional Cloudflare DNS automation.

Network model:

1
Internet → Traefik :443 → open-design:7456 on the private Docker proxy network

Open Design should not bind a public host port. Traefik is the only public entry point.


This service builds on the same homelab base as the rest of the stack:

Read the Traefik and Authentik guides first if you do not already have a shared reverse-proxy network and forward-auth middleware.


Folder layout

Create one service directory for Open Design.

1
2
sudo mkdir -p /home/ubuntu/open-design
cd /home/ubuntu/open-design

The finished folder will look like this:

1
2
3
/home/ubuntu/open-design/
├── Dockerfile
└── docker-compose.yml

Runtime state is stored in Docker named volumes, not loose host folders:

1
2
open-design_open_design_data  → /app/.od
open-design_open_design_home  → /home/open-design

That keeps Open Design data and CLI authentication state alive across container rebuilds.


Build a custom Open Design image

Open Design can run from its upstream image, but this deployment needs Gemini CLI and Codex CLI available inside the same runtime. Create this file:

1
/home/ubuntu/open-design/Dockerfile

Add the following Dockerfile:

1
2
3
4
5
6
7
8
9
FROM docker.io/vanjayak/open-design:latest

USER root

RUN npm install -g @google/gemini-cli @openai/codex \
  && mkdir -p /home/open-design/.gemini /home/open-design/.codex \
  && chown -R open-design:open-design /home/open-design

USER open-design

Why this matters:

  • Open Design can discover the CLIs from PATH.
  • CLI state lives under /home/open-design.
  • The custom image is reproducible and can be rebuilt later.

Compose file

Create this file:

1
/home/ubuntu/open-design/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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
services:
  open-design:
    build:
      context: .
      dockerfile: Dockerfile
    image: local/open-design-agents:latest
    container_name: open-design
    restart: unless-stopped
    environment:
      NODE_ENV: production
      NODE_OPTIONS: --max-old-space-size=512
      OD_BIND_HOST: 0.0.0.0
      OD_ALLOWED_ORIGINS: https://design.example.com
      OD_PORT: 7456
      OD_WEB_PORT: 7456
      HOME: /home/open-design
    volumes:
      - open_design_data:/app/.od
      - open_design_home:/home/open-design
    tmpfs:
      - /tmp:rw,exec,nosuid,size=512m,mode=1777
    read_only: true
    security_opt:
      - no-new-privileges:true
    mem_limit: 1g
    pids_limit: 512
    networks:
      - proxy
    labels:
      - "traefik.enable=true"
      - "traefik.docker.network=proxy"

      - "traefik.http.middlewares.design-https-redirect.redirectscheme.scheme=https"

      - "traefik.http.routers.design.entrypoints=http"
      - "traefik.http.routers.design.rule=Host(`design.example.com`)"
      - "traefik.http.routers.design.middlewares=design-https-redirect"

      - "traefik.http.routers.design-secure.entrypoints=https"
      - "traefik.http.routers.design-secure.rule=Host(`design.example.com`)"
      - "traefik.http.routers.design-secure.tls=true"
      - "traefik.http.routers.design-secure.tls.certresolver=cloudflare"
      - "traefik.http.routers.design-secure.middlewares=authentik@docker"
      - "traefik.http.routers.design-secure.service=open-design"

      - "traefik.http.services.open-design.loadbalancer.server.port=7456"
    healthcheck:
      test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:7456/api/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"]
      interval: 30s
      timeout: 5s
      retries: 5
      start_period: 30s

volumes:
  open_design_data:
  open_design_home:

networks:
  proxy:
    external: true

Important details:

  • OD_ALLOWED_ORIGINS should match the real public URL.
  • The service is attached only to the shared proxy network.
  • The app listens on container port 7456.
  • Traefik forwards to port 7456; there is no ports: block.
  • Authentik protects the HTTPS router using the existing authentik@docker middleware.

Start the service

From the service directory, build and start Open Design:

1
2
cd /home/ubuntu/open-design
sudo docker compose up -d --build

Check the container:

1
2
sudo docker compose ps
sudo docker logs --tail=80 open-design

Check the local health endpoint inside the container:

1
sudo docker exec open-design node -e "fetch('http://127.0.0.1:7456/api/health').then(async r=>{console.log(r.status, await r.text())})"

Expected result:

1
200 {"ok":true,"version":"..."}

Authentik setup

If your Authentik deployment already exposes a reusable forward-auth middleware named authentik@docker, the Open Design router can use that directly.

For clean application naming and correct callback handling, create a dedicated Authentik application/provider for Open Design:

1
2
3
4
5
6
Application name: Design
Slug: design
External host: https://design.example.com
Provider type: Proxy Provider
Proxy mode: Forward auth / single application, depending on your Authentik layout
Cookie domain: example.com

The callback path should point back to the Open Design hostname:

1
https://design.example.com/outpost.goauthentik.io/callback?X-authentik-auth-callback=true

If your embedded outpost routes callback paths through Traefik labels, add a route like this to the Authentik server labels:

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

Then recreate Authentik so Traefik sees the labels:

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

DNS

Create a DNS record for the public hostname:

1
design.example.com → your reverse proxy / Cloudflare target

If you use Traefik Cloudflare Companion, the Docker labels can create the record automatically. If not, create it manually in Cloudflare.

Check DNS from a machine outside the server:

1
nslookup design.example.com 1.1.1.1

Verify Traefik can reach Open Design

From the Docker host, test the service from inside the shared network:

1
2
sudo docker run --rm --network proxy alpine:3.20 \
  sh -lc 'wget -S -O- -T 10 http://open-design:7456/api/health'

Expected result:

1
2
HTTP/1.1 200 OK
{"ok":true,"version":"..."}

If this fails, Traefik will not be able to proxy the app either. Check that the Open Design container is attached to the same Docker network named in traefik.docker.network.


Verify public access

Before logging in, the public URL should redirect to Authentik:

1
curl -I https://design.example.com

Expected behavior:

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

After Authentik login, you should land back on Open Design.


Verify agent discovery

Open Design exposes an agents endpoint that should discover installed CLIs. Run this inside the container:

1
sudo docker exec open-design node -e "fetch('http://127.0.0.1:7456/api/agents').then(async r=>console.log(await r.text()))"

Look for these agents in the JSON:

1
2
Gemini CLI available: true
Codex CLI available: true

You can also check versions directly:

1
sudo docker exec open-design sh -lc 'gemini --version && codex --version'

Authenticate Gemini CLI

Gemini CLI needs a Google auth method before it can serve requests. The simplest interactive path is Google login.

Start an interactive shell command:

1
sudo docker exec -it open-design sh -lc 'GOOGLE_GENAI_USE_GCA=true gemini --skip-trust'

Follow the URL shown by the CLI, approve access in Google, and paste the authorization code back into the terminal.

After login, test non-interactive execution:

1
sudo docker exec open-design sh -lc 'GOOGLE_GENAI_USE_GCA=true gemini -p "Reply exactly OK" --output-format json --skip-trust'

Expected result includes:

1
{"response":"OK"}

The exact JSON includes extra session and usage data, so do not hard-code the full output.


Authenticate Codex CLI

Codex CLI can authenticate with ChatGPT device login.

Start the login flow:

1
sudo docker exec -it open-design sh -lc 'codex login --device-auth'

Open the device URL shown by the CLI and enter the one-time code. Do not paste device codes into public logs.

Check login status:

1
sudo docker exec open-design sh -lc 'codex login status'

Expected result:

1
Logged in using ChatGPT

Then run a simple execution probe:

1
sudo docker exec open-design sh -lc 'codex --ask-for-approval never --sandbox read-only exec --skip-git-repo-check "Reply exactly OK"'

If Codex reports a usage limit, authentication still worked; the account is just quota-blocked until the reset time or until the account is upgraded.


Keep CLI authentication persistent

The Compose file mounts this volume:

1
open_design_home → /home/open-design

That is where the CLIs keep their state:

1
2
/home/open-design/.gemini
/home/open-design/.codex

Because this is a named volume, rebuilds should not wipe authentication:

1
sudo docker compose up -d --build

Do not remove the named volume unless you intentionally want to reset the app and CLI logins:

1
sudo docker volume rm open-design_open_design_home

That command deletes saved CLI state.


Homepage dashboard entry

If you use Homepage, add Open Design to your services file.

Edit this file:

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

Add an entry under your AI or tools group:

1
2
3
4
5
- Open Design:
    icon: figma.png
    href: https://design.example.com
    description: Local-first design workspace with Gemini CLI and Codex CLI
    siteMonitor: https://design.example.com

Restart or reload Homepage if your deployment does not auto-reload config:

1
2
cd /home/ubuntu/homepage
sudo docker compose up -d

Updating Open Design

To update the upstream Open Design image and rebuild the CLI-enabled image:

1
2
3
4
cd /home/ubuntu/open-design
sudo docker compose pull
sudo docker compose build --pull
sudo docker compose up -d

Then verify health and agents again:

1
2
sudo docker inspect -f '{{.State.Health.Status}}' open-design
sudo docker exec open-design sh -lc 'gemini --version && codex --version'

Troubleshooting

Public URL returns 404

Check whether the Traefik router exists and the hostname matches exactly:

1
sudo docker inspect open-design --format '{{json .Config.Labels}}'

Common causes:

  • typo in Host(...) label;
  • container not attached to the proxy network;
  • Traefik is watching a different Docker network;
  • Authentik outpost callback route is missing.

Authentik redirects to the wrong domain

Check the Authentik provider’s external host and callback URL. The callback must use the same hostname users visit:

1
https://design.example.com/outpost.goauthentik.io/callback?X-authentik-auth-callback=true

If you previously used another hostname, remove the old application/provider and old DNS record to avoid confusing login redirects.

Gemini says no auth method is configured

Set GOOGLE_GENAI_USE_GCA=true for Google account auth, or provide another supported Gemini auth method.

For the Google account flow:

1
sudo docker exec -it open-design sh -lc 'GOOGLE_GENAI_USE_GCA=true gemini --skip-trust'

Codex says no credentials were found

Run device login again:

1
sudo docker exec -it open-design sh -lc 'codex login --device-auth'

Then verify:

1
sudo docker exec open-design sh -lc 'codex login status'

Codex is logged in but cannot run

If the CLI says a usage limit has been reached, the login is valid but the account is quota-blocked. Wait for the reset time shown by Codex, switch account, or upgrade the plan.


Security notes

  • Keep Open Design behind Authentik or another trusted SSO layer.
  • Do not expose container port 7456 directly to the internet.
  • Treat /home/open-design/.gemini and /home/open-design/.codex as sensitive state.
  • Do not commit OAuth codes, API keys, or CLI auth files to Git.
  • Use named volumes or encrypted backups if the workspace matters.
  • Remove old hostnames from Traefik, Authentik, and DNS when you rename the service.

Final checklist

  • DNS points design.example.com to the reverse proxy.
  • Open Design container is healthy.
  • Traefik routes HTTPS to open-design:7456.
  • Authentik redirects unauthenticated users to login.
  • Authentik callback returns to design.example.com.
  • /api/agents detects Gemini CLI and Codex CLI.
  • Gemini CLI returns a small test response.
  • Codex CLI login status is valid.
  • Homepage points to the final hostname.
  • Any old hostname has been removed from DNS and Authentik.
This post is licensed under CC BY 4.0 by the author.