Skip to main content
Anchor provides a single Docker image that includes everything you need to run your self-hosted instance. The image uses embedded PostgreSQL by default, making setup as simple as running a single container.

Quick Start

The fastest way to get started is using the pre-built Docker image:
1

Create docker-compose.yml

Create a docker-compose.yml file with the minimal configuration:
docker-compose.yml
services:
  anchor:
    image: ghcr.io/zhfahim/anchor:latest
    container_name: anchor
    restart: unless-stopped
    ports:
      - "3000:3000"
    volumes:
      - anchor_data:/data

volumes:
  anchor_data:
This configuration uses embedded PostgreSQL with auto-generated credentials. Data is persisted in the anchor_data volume.
2

Start the container

Launch Anchor with Docker Compose:
docker compose up -d
The container will:
  • Initialize embedded PostgreSQL in /data/postgres
  • Auto-generate a JWT secret (persisted in /data/.jwt_secret)
  • Run database migrations
  • Start the API server (port 3001 internally)
  • Start the web frontend (port 3000 exposed)
3

Access your instance

Open your browser and navigate to:
http://localhost:3000
You can now create your first account and start using Anchor.

Deployment Options

Use the official image from GitHub Container Registry:
docker-compose.yml
services:
  anchor:
    image: ghcr.io/zhfahim/anchor:latest
    container_name: anchor
    restart: unless-stopped
    ports:
      - "3000:3000"
    environment:
      - APP_URL=https://notes.example.com
      - JWT_SECRET=your-secret-key-here
    volumes:
      - anchor_data:/data

volumes:
  anchor_data:

Option 2: Build from Source

If you want to build from source or customize the image:
1

Clone the repository

git clone https://github.com/zhfahim/anchor.git
cd anchor
2

Build and start

The included docker-compose.yml builds from source:
docker compose up -d

Architecture

The Anchor Docker image uses a unique single-container architecture:
  • Base Image: PostgreSQL 18 Alpine with Node.js runtime
  • Process Management: Supervisord manages multiple services
  • Services:
    • Embedded PostgreSQL (optional, port 5432)
    • NestJS API server (internal port 3001)
    • Next.js web frontend (exposed port 3000)
FROM postgres:18-alpine AS runner

# Node runtime copied from node:24-alpine
COPY --from=base /usr/local/bin/node /usr/local/bin/node

# Server (NestJS + Prisma)
COPY --from=server_builder /app/server/dist ./server/dist
COPY --from=server_prod_deps /app/server/node_modules ./server/node_modules

# Web (Next.js standalone)
COPY --from=web_builder /app/web/.next/standalone ./web
COPY --from=web_builder /app/web/.next/static ./web/.next/static

Volume Management

Data Directory Structure

The /data volume contains all persistent data:
/data/
├── postgres/          # PostgreSQL database files (if using embedded)
└── .jwt_secret        # Auto-generated JWT secret
Always back up the /data volume before upgrading or migrating your instance.
volumes:
  - anchor_data:/data

volumes:
  anchor_data:

Bind Mount

For easier backups, use a bind mount:
volumes:
  - ./anchor-data:/data

Reverse Proxy Setup

Nginx

server {
    listen 443 ssl http2;
    server_name notes.example.com;

    ssl_certificate /path/to/cert.pem;
    ssl_certificate_key /path/to/key.pem;

    location / {
        proxy_pass http://localhost:3000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_cache_bypass $http_upgrade;
    }
}

Traefik

docker-compose.yml
services:
  anchor:
    image: ghcr.io/zhfahim/anchor:latest
    environment:
      - APP_URL=https://notes.example.com
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.anchor.rule=Host(`notes.example.com`)"
      - "traefik.http.routers.anchor.entrypoints=websecure"
      - "traefik.http.routers.anchor.tls.certresolver=myresolver"
      - "traefik.http.services.anchor.loadbalancer.server.port=3000"

Caddy

Caddyfile
notes.example.com {
    reverse_proxy localhost:3000
}
Remember to set APP_URL to your public URL when using a reverse proxy, especially for OIDC authentication.

Health Checks

The Docker image includes a built-in health check:
HEALTHCHECK --interval=30s --timeout=10s --retries=3 --start-period=30s \
  CMD curl -f http://localhost:3000/api/health || exit 1
Check container health:
docker ps
# Look for "healthy" status

docker inspect anchor | jq '.[0].State.Health'

Common Issues

Container won’t start

Check logs for errors:
docker compose logs -f anchor

Permission issues with volumes

Ensure the data directory is writable:
sudo chown -R 1000:1000 ./anchor-data

Port conflicts

If port 3000 is already in use, change the mapping:
ports:
  - "8080:3000"  # Access via http://localhost:8080

Next Steps

Configuration

Configure environment variables and settings

Database Options

Use external PostgreSQL for production

Updating

Keep your instance up to date

Build docs developers (and LLMs) love